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.
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 slug6 title7 date(formatString: $formatString)8 excerpt9 timeToRead10 description11 tags {12 name13 slug14 }15 }16 }17 }18`
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<Input2 ...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 state7 const [searchQuery, setSearchQuery] = useState('');8
9 // filter posts by search query10 const filteredPosts = posts.filter((post) => {11 // search in title12 const postTitle = post.title.toLowerCase();13
14 // return an array with filtered posts if search query is not empty15 // if search query is empty, return all posts16 return postTitle.includes(searchQuery);17 });18
19 return (20 <Layout>21 ...22
23 <Input24 ...25 placeholder="Begin typing to search ..."26 onChange={(e) => setSearchQuery(e.target.value.toLowerCase())}27 />28
29 ...30
31 <ListingByYear32 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 <p8 ...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 posts29 ...30 ) : (31 <NoMatchesFound searchQuery={searchQuery} />32 )}33 </div>34 );35};36
37...
Sign up to get updates when I write something new. No spam ever.
Subscribe to my Newsletter