How to add posts search functionality to your Gatsby blog

I have recently added a new functionality to this site, which is the possibility to search posts. I don’t have a lot of posts right now, but I plan to write a lot more and I want visitors to have a better experience and help them to find what they are looking for.

I have added a text input where you can write any keyword, and the posts are filtered based on that. If a certain post contains that keyword in the title, description or tags, it will be part of the filtered list.

0
The post search functionality I've added to my site

It’s very easy to do and I think it really makes a difference, so let’s see how to add this functionality.

Before we start

I’m going to assume that you already have a page in your blog where you list all your posts. I’m using markdown posts, so I have a page called posts which uses a page query to get all of them:

export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      edges {
        node {
          excerpt
          timeToRead
          fields {
            slug
          }
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            description
            tags
          }
        }
      }
    }
  }
`;

This query provides a prop called data to your component, which contains the result (in this case, the posts list). Then you can access your posts like this:

const allPosts = data.allMarkdownRemark.edges;

You can use a similar query if you use markdown posts. If you use any other source, make sure you have a page where you get all your posts.

Once you’ve that, then you’re ready!

Let’s add the input first

Ok, so we want to filter our posts based on what the user types. To do that, we’ll need a text input.

<input
  type="text"
  aria-label="Search"
  placeholder="Search posts"
/>

Remember to add the aria-label property for accessibility!

Let’s now useState to control the input:

const [state, setState] = useState({
  filteredPosts: [],
  query: "",
});

<input
  type="text"
  aria-label="Search"
  placeholder="Search posts"
  value={state.query}
/>

And finally we will need a function to handle the changes to that value. This will also be the function that will filter the posts. The event we want to listen to is onChange:

const [state, setState] = useState({
  filteredPosts: [],
  query: "",
});

<input
  type="text"
  aria-label="Search"
  placeholder="Search posts"
  value={state.query}
  onChange={handleInputChange}
/>

Now let’s filter the posts!

Now that we have all we need, all that’s left is actually filtering the posts.

Our function handleInputChange receives an event object. The first thing we need to do is get the value from it.

const handleInputChange = event => {
  const query = event.target.value;
};

Now that we have the value that the user typed, we can start filtering. To do this we will use the filter function on our array of posts (which for me it’s called allPosts). This function will return a new array containing only the posts that pass the filter.

const handleInputChange = event => {
  const query = event.target.value;
  const filteredPosts = allPosts.filter(post => {
    // Filtering goes here
  });
};

We want to check if the keyword the user typed is in the title, description or tags of each post. So first, let’s get that information:

const handleInputChange = event => {
  const query = event.target.value;
  const filteredPosts = allPosts.filter(post => {
    const { description, title, tags } = post.node.frontmatter;
  });
};

Now, let’s check if the description contains the keyword, and return the post to be included in the filtered list only if it does:

const handleInputChange = event => {
  const query = event.target.value;
  const filteredPosts = allPosts.filter(post => {
    const { description, title, tags } = post.node.frontmatter;
    return (
      description.toLowerCase().includes(query.toLowerCase())
    );
  });
};

We can then do the same for the post title:

const handleInputChange = event => {
  const query = event.target.value;
  const filteredPosts = allPosts.filter(post => {
    const { description, title, tags } = post.node.frontmatter;
    return (
      description.toLowerCase().includes(query.toLowerCase()) ||
      title.toLowerCase().includes(query.toLowerCase())
    );
  });
};

And once again, the same for the tags:

const handleInputChange = event => {
  const query = event.target.value;
  const filteredPosts = allPosts.filter(post => {
    const { description, title, tags } = post.node.frontmatter;
    return (
      description.toLowerCase().includes(query.toLowerCase()) ||
      title.toLowerCase().includes(query.toLowerCase()) ||
      (tags && tags.join("").toLowerCase().includes(query.toLowerCase()))
    );
  });
};

Great! We’re only missing one step for this function, which is to update the state with this new, filtered posts list (and also the query, which is the value for the input):

const handleInputChange = event => {
  const query = event.target.value;
  const filteredPosts = allPosts.filter(post => {
    const { description, title, tags } = post.node.frontmatter;
    return (
      description.toLowerCase().includes(query.toLowerCase()) ||
      title.toLowerCase().includes(query.toLowerCase()) ||
      (tags && tags.join("").toLowerCase().includes(query.toLowerCase()))
    );
  });
  setState({
    query,
    filteredPosts,
  });
};

Awesome! The last step for the whole component would be to set a variable posts which takes either the filtered list or the complete list of posts.

const posts = state.query ? state.filteredPosts : allPosts;

And that’s all! The whole component should look like this:

import React, { useState } from "react";
import { graphql } from "gatsby";
import Layout from "../components/Layout";

const Posts = ({ data }) => {
  const [state, setState] = useState({
    filteredPosts: [],
    query: "",
  });

  const allPosts = data.allMarkdownRemark.edges;

  const handleInputChange = event => {
    const query = event.target.value;
    const filteredPosts = allPosts.filter(post => {
      const { description, title, tags } = post.node.frontmatter;
      return (
        description.toLowerCase().includes(query.toLowerCase()) ||
        title.toLowerCase().includes(query.toLowerCase()) ||
        (tags && tags.join("").toLowerCase().includes(query.toLowerCase()))
      );
    });
    setState({
      query,
      filteredPosts,
    });
  };

  const posts = state.query ? state.filteredPosts : allPosts;

  return (
    <Layout>
      <h1>Posts</h1>
      <input
        type="text"
        aria-label="Search"
        placeholder="Search posts"
        value={state.query}
        onChange={handleInputChange}
      />
      {posts.map(post => {
        // Render posts
      })}
    </Layout>
  );
};

export default Posts;

export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      edges {
        node {
          excerpt
          timeToRead
          fields {
            slug
          }
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            description
            tags
          }
        }
      }
    }
  }
`;

That’s it!

I hope this post was useful and you can add this search functionality to your blog!

Thanks for reading ❤️