Easy global search with Sanity, Next JS & React

Peter Rolfsen
4 min readNov 2, 2023

--

This guide will propose a solution to write a custom search function for your structured Sanity content. For anyone who does not have a lot of money to spend on third party search plugins.

Technologies: Next 13, React, TypeScript, Sanity, GROQ

This tutorial also assumes that you have already set up a Next Sanity project. I have used the default blog-template for this project.

If not. Check out this tutorial.

figure 1. Search page

In this tutorial we will cover the following topics of search:

  • Writing a simple and working search function for Sanity content using GROQ and Javascript.

Step 1: Set up a simple search page in pages

Figure 2

In your pages folder. Create a directory called search and add index.tsx to it.

This is the page where your search will be executed. You can see mine in figure 1.

Access it by going to localhost:3000/search.

The search page has two hooks for storing searchString and searchResults.
It also has two functions.

getResponse()

  • Query: creates the query string for the search.
  • response: fetches a response from sanity
  • data: returns the response and stores it in searchResults

handleClickUser()

  • Checks that the searchString is not empty.
  • runs getResponse()
  • pushes the searchString to the path

Using the path to store the search string allows you to use the variable even if you do the search from another page. I case you want to use the search on the frontpage or in the menu.

// search/index.tsx

import SearchInput from "@/components/SearchInput";
import getCardByType from "@/utils/getCardbyType";
import router, { useRouter } from "next/router";
import { useEffect, useState } from "react";

export const SearchPage = () => {
const {
query: { query: queryFromUrl },
} = useRouter();

// a state to store the search string
const [searchString, setSearchString] = useState(
typeof queryFromUrl === "string" ? queryFromUrl : ""
);
const [searchResults, setSearchResults] = useState([] as any[]);

async function getResponse() {
// query should be the URL that your search will be executed on.
const query = `http://localhost:3001/api/search?query=${searchString}`;
const response = await fetch(query, {
method: "GET",
});

const data = await response.json(); // Extracting data as a JSON Object from the response
setSearchResults(data);
}

const handleClickUser = async () => {
if (searchString === "" || searchString.trim() === "") return;
getResponse();
router.push({
pathname: "../search",
query: { query: searchString },
});
};

useEffect(() => {
if (searchString !== "") {
handleClickUser();
}
}, []);

return (
<div className="pageWrapper">
<h1>Search Page</h1>
<SearchInput
value={searchString}
onChange={(e) => setSearchString(e.target.value)}
onClick={handleClickUser}
/>
<div className="wrapper">
{searchResults.map((result) => getCardByType(result))}
</div>
</div>
);
};

export default SearchPage;

2. Create the search query

Also create an api/folder in pages where you will write the GROQ to query for the search page. I will create a file search.tsx to write the GROQ and SearchQueries.js to hold all my variables for accessing the different fields of the content.

// api search.ts


import client from "../../client";
import { groq } from "next-sanity";

export default async function search(req, res) {
const { query } = req.query;

const searchQuery = groq`
*[_type in ["post", 'author', 'category']
&& (
title match $queryString + '*'
|| name match $queryString + '*')]
| order(publishedAt desc){
title,
name,
bio,
body,
'slug' : slug.current,
description,
'type': _type,

}
`;

const data = await client.fetch(searchQuery, {
queryString: query,
});

res.status(200).json(data);
}

Here we use GROQ to find the content that matches the queryString.
To access all contenttypes we use _type in [] syntax. Then we have to tell the grow what to match on. I want it to match on name (authors) and title (post, category).

We extract the query from the request.

This will now result in your simple search being complete.

Extra

My getCardByType component:

// utils/getCardByType.tsx

import Card from "../components/Card/Card";

export const getCardByType = (data: any) => {
const { title, body, type, slug, ptComponents, description, bio, name } =
data;
console.log(type);
switch (type) {
case "post":
return (
<Card
title={title}
excerpt={body}
type={type}
slug={slug}
key={slug}
ptComponents={ptComponents}
/>
);

case "author":
return (
<Card
title={name}
excerpt={bio}
type={type}
slug={slug}
key={slug}
ptComponents={ptComponents}
/>
);
case "category":
return (
<Card
title={title}
excerpt={description}
type={type}
slug={slug}
key={slug}
ptComponents={ptComponents}
/>
);

default:
return "hello";
}
};

export default getCardByType;

Card is just a component I created to showcase the results.

Repo for finished project: https://github.com/peterrolfsen/SanityGlobalSearch

--

--