Skip to content

Edvins Antonovs

Add search to a Gatsby blog

Congratulations, you have your Gatsby blog up and running. Now you start wondering how you can add search functionality so it would be easier to find specific blog posts? Well, that’s an easy task to do and won’t take more than 5 minutes of your time.

I’ve implemented it for my own blog and you can see it in action.


Before we start

First of all, let’s define the search scope. In my case, I’m interested in searching by title field.

I use pretty standard Gatsby MDX starter template and I write all my content in Markdown. I also use MDX to enhance my experience so I could include React components in my blog posts and pages.

Finally, here is a quick peek into the posts GraphQL query which I use. Looks pretty straightforward, right?

1export const query = graphql`
2 query($formatString: String!) {
3 allPost(sort: { fields: date, order: DESC }) {
4 nodes {
5 slug
6 title
7 date(formatString: $formatString)
8 excerpt
9 timeToRead
10 description
11 tags {
12 name
13 slug
14 }
15 }
16 }
17 }
18`

Implementation

We are going to use React’s useState hook to store the search value.

1const [searchQuery, setSearchQuery] = useState('');

Now we need to create an input field and handle the onChange event. Which will trigger setSearchQuery whenever the input value is changed.

We use toLowerCase() to make our search case insensitive.

1<Input
2 ...
3 placeholder="Begin typing to search ..."
4 onChange={(e) => setSearchQuery(e.target.value.toLowerCase())}
5/>

filteredPosts returns an array of filtered posts based on the provided search query. For cases where the search query is empty, it returns all posts.

1const filteredPosts = posts.filter((post) => {
2 const postTitle = post.title.toLowerCase();
3
4 return postTitle.includes(searchQuery);
5});

Now let’s apply everything what we have so far to our <Blog /> page. Which accepts an array of posts as a property which we receive from allPost GraphQL query.

<ListingByYear /> component handles rendering grouped posts by year. Notice that we pass filteredPosts and searchQuery to that component. In a moment, you will see why we do this.

1...
2
3const Blog = ({ posts }) => {
4 ...
5
6 // search state
7 const [searchQuery, setSearchQuery] = useState('');
8
9 // filter posts by search query
10 const filteredPosts = posts.filter((post) => {
11 // search in title
12 const postTitle = post.title.toLowerCase();
13
14 // return an array with filtered posts if search query is not empty
15 // if search query is empty, return all posts
16 return postTitle.includes(searchQuery);
17 });
18
19 return (
20 <Layout>
21 ...
22
23 <Input
24 ...
25 placeholder="Begin typing to search ..."
26 onChange={(e) => setSearchQuery(e.target.value.toLowerCase())}
27 />
28
29 ...
30
31 <ListingByYear
32 posts={filteredPosts}
33 searchQuery={searchQuery}
34 />
35 </Layout>
36 );
37};
38
39...

Perfect, so at this stage filtering functionality works as expected. Yet there is a white space for cases when no blog posts found matching the search query. It’s a poor user experience which leads to a dead-end scenario. To improve that behavior I've added <NoMatchesFound /> component. It acts as a hint to a user to redefine his search as well as renders a Sorry, no blog posts were found matching ... message.

1...
2
3const NoMatchesFound = ({ searchQuery }) => (
4 <section>
5 <h2>No matches found</h2>
6 <div>
7 <p
8 ...
9 >
10 {searchQuery && (
11 <>
12 Sorry, no blog posts were found matching your search for <strong>{searchQuery}</strong>.
13 </>
14 )}
15 </p>
16 </div>
17 </section>
18);
19
20...
21
22const ListingByYear = ({ posts, searchQuery = '', ... }) => {
23 ...
24
25 return (
26 <div sx={{ mb: [5, 6, 7] }} className={className}>
27 {posts.length ? (
28 // render filtered posts
29 ...
30 ) : (
31 <NoMatchesFound searchQuery={searchQuery} />
32 )}
33 </div>
34 );
35};
36
37...
© 2024 by Edvins Antonovs. All rights reserved.