How To Add Search Functionality to a Gatsby Blog
Tags: react, javascript, gatsby, tutorial
Published: 2019-12-05 13:36:11

---
title: "How To Add Search Functionality to a Gatsby Blog"
template: "post"
published: true
description: "This tutorial will walk through how to create functionality to allow users to filter posts on a Gatsby site by description, title and, tags. "
tags: React, JavaScript, Gatsby, Tutorial
canonical_url: https://www.aboutmonica.com/blog/create-gatsby-blog-search-tutorial
cover_image: https://thepracticaldev.s3.amazonaws.com/i/4cxn31jfx4aity2uu3vi.png
---

I recently added functionality to my personal site https://www.aboutmonica.com to allow visitors to filter posts based on the posts description, title, and tags in an effort to allow better discovery of content. This tutorial will is based off of how I implemented a basic search on my site and will cover how to create a search filter on a site built with [GatsbyJS](https://www.gatsbyjs.org). In particular, this tutorial walks through how to create an input field that allows users to filter a list of an entire Gatsby site's posts if the description, title or tags matches the input query. The solution proposed in this tutorial leverages GraphQL and React hooks to update the state to show appropriate data when content is filtered.


_<center>Demo of the Search Filter</center>_

![Gif illustrating demo of the search functionality](https://thepracticaldev.s3.amazonaws.com/i/hgoheqtuuyyz8h3nnadh.gif)

- [Getting Started](#getting-started)
  - [Prerequisites](#prerequisites)
  - [Boilerplate Code: Query All Posts](#boilerplate-code-query-all-posts)
  - [Why Query All of The Posts?](#why-query-all-of-the-posts)
- [How to Filter Posts by User Input](#how-to-filter-posts-by-user-input)
  - [Capture User Input with Input Event](#capture-user-input-with-input-event)
  - [Create Input Element with `onChange` event handler](#create-input-element-with-onchange-event-handler)
  - [useState() to Store Filtered Data and Query Information in State](#usestate-to-store-filtered-data-and-query-information-in-state)
  - [Implement `onChange` to Filter Posts by `<input/>` Event Value](#implement-onchange-to-filter-posts-by-input-event-value)
  - [Display Filtered Posts](#display-filtered-posts)
- [Summary](#summary)
  - [Final Code](#final-code)

## Getting Started

### Prerequisites

Although, some of the implementation details can be abstracted and applied in any React application to get the most value out of this tutorial you should have:

- Some knowledge of ES6 and React
- Local Gatsby site with Markdown posts
  - If you have a Gatsby site _without_ Markdown posts check out the [Boilerplate Code](#boilerplate-code) or update the code in this tutorial to query posts from your data source instead.

> If you do not yet have Markdown files on your site then you should start by [adding markdown pages](https://www.gatsbyjs.org/docs/adding-markdown-pages/) to Gatsby. You can also learn more about [ creating an index of markdown posts](https://www.gatsbyjs.org/docs/adding-a-list-of-markdown-blog-posts/) in the Gatsby Docs.

### Boilerplate Code: Query All Posts

If you do **not** already have an index page listing all of your posts then create a new gatsby page for example named "writing.js" in `src` within the `pages` directory. This file will be responsible for rendering information about every post on your site.

We will be using a GraphQL page query which allows the data returned from the query to be available to the component in the `data` prop. The posts are returned by the page query and are equal to `data.allMarkdownRemark.edges` . Once we have the posts we can `.map()` through each of the posts and destructure the `node.frontmatter` with `const { tags, title, date, description, slug } = node.frontmatter`. This will add the title, date, description, and slug to the DOM for each post.

> "Gatsby uses the concept of a page query, which is a query for a specific page in a site. It is unique in that it can take query variables unlike Gatsby’s static queries." Source: [Gatsby Docs](https://www.gatsbyjs.org/docs/page-query/)

Below is the boilerplate code that will be used throughout this tutorial:

```jsx
import React from "react"
import { Link, graphql } from "gatsby"

const BlogIndex = props => {
  const { data } = props
  const posts = data.allMarkdownRemark.edges

  return (
    <>
      {/* in my site I wrap each page with a Layout and SEO component which have 
    been omitted here for clarity and replaced with a React.fragment --> */}

      {/*in-line css for demo purposes*/}
      <h1 style={{ textAlign: `center` }}>Writing</h1>

      {posts.map(({ node }) => {
        const { excerpt } = node
        const { slug } = node.fields

        const { title, date, description, slug } = node.frontmatter
        return (
          <article key={slug}>
            <header>
              <h2>
                <Link to={slug}>{title}</Link>
              </h2>

              <p>{date}</p>
            </header>
            <section>
              <p
                dangerouslySetInnerHTML={{
                  __html: description || excerpt,
                }}
              />
            </section>
            <hr />
          </article>
        )
      })}
    </>
  )
}

export default BlogIndex

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

At this point you should be able to view an index of all of the posts on your site by running `gatsby develop` and going to `http://localhost:8000/${NAME_OF_FILE}`. For example, the file I created is named `writing.js` so I navigate to [http://localhost:8000/writing](http://localhost:8000/writing) to view it. The page output by the boilerplate code above should resemble the below image (i.e., each blog post is listed along with its title, date, and description). Additionally, the header for each article should navigate to the slug for the article and be a valid link.



_<center>Index Page of All Posts</center>_

![screenshot of index page of all posts](https://thepracticaldev.s3.amazonaws.com/i/wxoxvnfmwyvb806qxdue.png)

### Why Query All of The Posts?

Before filtering the posts its helpful fetch all of the posts before we return a filtered subset from all of the posts. On my site, I used a page query on the `/writing/` page to retrieve data for all the blog posts from my site so that I can construct a list of posts. The results of the page query are available to this component within the `data` prop to the component i.e., (`const { data } = props`).

The boilerplate code above is a variation of the GraphQL query that my site uses to pull in each post along with its excerpt, id, frontmatter (title, category, description, date, slug, and tags). The blog posts are in the `allMarkdownRemark` as `edges` and can be accessed like `const posts = data.allMarkdownRemark.edges`.You can use the above-provided query to return metadata and slugs for all posts OR if you already have a query to return an index of all blog posts then feel free to use that.

Below is a photo that shows the data that the above GraphQL query returned for my site. You can view the data returned by that query for your particular site in an interactive format by running `gatsby develop` and navigating to [http://localhost:8000/\_\_\_graphql](<http://localhost:8000/___graphql?query=%7BallMarkdownRemark(sort%3A%20%7B%20order%3A%20DESC%2C%20fields%3A%20frontmatter___date%20%7D)%20%7B%0A%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20excerpt(pruneLength%3A%20200)%0A%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20frontmatter%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20category%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20date(formatString%3A%20%22MMMM%20DD%2C%20YYYY%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20tags%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20timeToRead%0A%20%20%20%20%20%20%20%20%20%20%20%20fields%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%7D%0A&operationName=undefined&explorerIsOpen=false>) and pressing run. If you go to [http://localhost:8000/\_\_\_graphql](<http://localhost:8000/___graphql?query=%7BallMarkdownRemark(sort%3A%20%7B%20order%3A%20DESC%2C%20fields%3A%20frontmatter___date%20%7D)%20%7B%0A%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20node%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20excerpt(pruneLength%3A%20200)%0A%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20frontmatter%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20category%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20date(formatString%3A%20%22MMMM%20DD%2C%20YYYY%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20tags%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20timeToRead%0A%20%20%20%20%20%20%20%20%20%20%20%20fields%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%7D%0A&operationName=undefined&explorerIsOpen=false>) and scroll down you should see that there is metadata being returned for every single post on your site which is exactly what we are trying to capture before we filter posts.


_<center>Sample Data in GraphiQL</center>_
![Sample Data in GraphiQL](https://thepracticaldev.s3.amazonaws.com/i/e56nw7l9szh16bhk8znw.png)

## How to Filter Posts by User Input

### Capture User Input with Input Event

Now that we have the boilerplate code setup let's get back to the task at hand which is to filter the posts based on user input. _How can we capture what query a user is searching for and update the DOM with the appropriate post(s) accordingly?_ Well, there are various types of browser events including, `input`, `keypress`, `click`, `drag` and `drop`. When these events occur JavaScript can be written to respond based on the type and value of the event.

Since we are having users type a search query into a `<input>` we can process their query as they type. We will be focusing on the `input`event which triggers whenever the value in an input field changes. The `input` event changes with each keystroke which is in contrast to the `change` event which is fired once for each submission (i.e., pressing enter) for `<input>`,`<select>` and `<textarea>` elements. You can read more about how [React handles events in the React docs](https://reactjs.org/docs/handling-events.html).

### Create Input Element with `onChange` event handler

We already have the post data we need to filter available in the `data` prop so let's create an element to allow users to type in their search query. `<input/>` will have an `onChange` property that calls a function `handleInputChange` whenever the `<input/>` changes and an `Input` event is fired. In other words, `onChange` calls another function which handles the Input event which fires every time someone types in our `<Input/>`. So if someone typed "React" into an `<input/>`. It will trigger 5 events with the following values ("R", "Re", "Rea", "Reac", "React").

Note: The `<input/>` should go below the `<h1>` and outside of the `posts.map`.

```jsx

        <h1 style={{ textAlign: `center` }}>Writing</h1>
          // code-changes-start
          <input
              type="text"
              id="filter"
              placeholder="Type to filter posts..."
              onChange={handleInputChange}
          />
          // code-changes-end
        {posts.map(({ node }) => {
```

The page should now visibly have an `<input/>` element. However, it will not yet be functional as `handleInputChange` has not been added yet.


_<center>Visible Input Element</center>_
![screenshot of input element](https://thepracticaldev.s3.amazonaws.com/i/5bvdaeilxqf3i9zchsu2.png)

### useState() to Store Filtered Data and Query Information in State

Before implementing `onChange` let's set the default state with `useState()` for our search input with the default `query` as an empty string and `filteredData` as an empty array. You can [read more about the `useState()` hook in the React docs](https://reactjs.org/docs/hooks-state.html).

```jsx
 // code-changes-start
  const posts = data.allMarkdownRemark.edges
  const emptyQuery = ""
  const [state, setState] = React.useState({
    filteredData: [],
    query: emptyQuery,
  })
 // code-changes-end
  return (
```

### Implement `onChange` to Filter Posts by `<input/>` Event Value

This `handleInputChange` function takes the Input event in which the `event.target.value` is the query string that is being searched for. `handleInputChange` also has access to our props which contain all of the posts for the site. So we can filter all of the site's posts based on the `query` and return `filteredPosts`.

In order to process the event (which fires on each keystroke) we need to implement `handleInputChange`. `handleInputChange` receives an Input event. The `target.value` from the `event` is the string that the user typed and we will store that in the `query` variable.

Inside of `handleInputChange` we have access to the posts and the query so let's update the code to `.filter()` the posts based on the query. First, we should standardize the casing of the fields and the query with `.toLowerCase()` so that if someone types "JaVAsCriPt" it should return posts that match "JavaScript". For our `.filter()` if any of the three conditions that check if the post contains the `query` evaluates to true then that post will be returned in the `filteredData` array.

After we filter the data in `handleInputChange` the state should be updated with the current `query` and the `filteredData` that resulted from that query.

```jsx

  const [state, setState] = React.useState({
    filteredData: [],
    query: emptyQuery,
  })

  // code-changes-start
const handleInputChange = event => {
  const query = event.target.value
  const { data } = props

  // this is how we get all of our posts
  const posts = data.allMarkdownRemark.edges || []


   // return all filtered posts
  const filteredData = posts.filter(post => {
    // destructure data from post frontmatter
    const { description, title, tags } = post.node.frontmatter
    return (
      // standardize data with .toLowerCase()
      // return true if the description, title or tags
      // contains the query string
      description.toLowerCase().includes(query.toLowerCase()) ||
      title.toLowerCase().includes(query.toLowerCase()) ||
      tags
        .join("") // convert tags from an array to string
        .toLowerCase()
        .includes(query.toLowerCase())
    )
  })

  // update state according to the latest query and results
  setState({
    query, // with current query string from the `Input` event
    filteredData, // with filtered data from posts.filter(post => (//filteredData)) above
  })
}

  // code-changes-end
return (
    <>
```

Now if you type in the `<Input/>` now it still won't update the list of posts because we are always rendering the same posts regardless of if we have `filteredData` available in the state or not. But if you were to `console.log(event.target.value)` in `handleInputChange` we can confirm that `handleInput` is firing properly by typing "React". Even though the page doesn't visually change the console output should be something like:

```bash
r writing.js:1
re writing..js:1
rea writing..js:1
reac writing.js:1
react writing.js:1
```

### Display Filtered Posts

We are already storing `filteredData` and `query` in state but let's rename `posts` to `allPosts` so that we can make the value of `posts` conditional based on whether or not a user has typed a search query and should see their filtered search query results as `posts` or if they have yet to type a query then we should display all of the blog posts.

```jsx
const BlogIndex = props => {

// code-changes-start
const { filteredData, query } = state
const { data } = props
 // let's rename posts to all posts
const allPosts = data.allMarkdownRemark.edges
 // code-changes-end
const emptyQuery = ""
```

For the `posts` we need to decide whether to return all of the posts or the filtered posts by checking `state` and conditionally rendering either all of the posts OR just the filtered posts based on whether or not we have `filteredData` and the `query != emptyQuery`.

The below code updates our render logic accordingly.

```jsx
const { filteredData, query } = state
// code-changes-start
// if we have a fileredData in state and a non-emptyQuery then
// searchQuery then `hasSearchResults` is true
const hasSearchResults = filteredData && query !== emptyQuery

// if we have a search query then return filtered data instead of all posts; else return allPosts
const posts = hasSearchResults ? filteredData : allPosts
// code-changes-end
```

## Summary

You should now have a working post filter on your blog index page (if not check out the [Final Code](#final-code) below). At a high-level the steps taken to implement filtering were:

1. create a page query to implement a blog index page which lists all of the posts
2. create an input field on the blog index page with an onChange event handler to process keystrokes in our input field
3. filter all of the posts on the blog index page based on the current query (from input event) and use `useState()` to update the state with the search query and filtered data
4. update rendering logic to either display all of the posts or the filtered posts on the blog index page based on whether or not there's a query in state

Below is the final code as outlined in the tutorial. However, this is just the baseline for search and you may want to make the functionality more robust by adding additional features such as autocomplete suggestions, displaying the number of results (based on length of `posts`) and providing an empty state with messaging for when there are zero results (based on filteredData being an empty array).

### Final Code

```jsx
import React from "react"
import { Link, graphql } from "gatsby"

const BlogIndex = props => {
  const { data } = props
  const allPosts = data.allMarkdownRemark.edges

  const emptyQuery = ""

  const [state, setState] = React.useState({
    filteredData: [],
    query: emptyQuery,
  })

  const handleInputChange = event => {
    console.log(event.target.value)
    const query = event.target.value
    const { data } = props

    const posts = data.allMarkdownRemark.edges || []

    const filteredData = posts.filter(post => {
      const { description, title, tags } = post.node.frontmatter
      return (
        description.toLowerCase().includes(query.toLowerCase()) ||
        title.toLowerCase().includes(query.toLowerCase()) ||
        tags
          .join("")
          .toLowerCase()
          .includes(query.toLowerCase())
      )
    })

    setState({
      query,
      filteredData,
    })
  }

  const { filteredData, query } = state
  const hasSearchResults = filteredData && query !== emptyQuery
  const posts = hasSearchResults ? filteredData : allPosts

  return (
    <>
      <h1 style={{ textAlign: `center` }}>Writing</h1>

      <div className="searchBox">
        <input
          className="searchInput"
          type="text"
          id="filter"
          placeholder="Type to filter posts..."
          onChange={handleInputChange}
        />
      </div>

      {posts.map(({ node }) => {
        const { excerpt } = node

        const { slug } = node.fields
        const { tags, title, date, description } = node.frontmatter
        return (
          <article key={slug}>
            <header>
              <h2>
                <Link to={slug}>{title}</Link>
              </h2>

              <p>{date}</p>
            </header>
            <section>
              <p
                dangerouslySetInnerHTML={{
                  __html: description || excerpt,
                }}
              />
            </section>
            <hr />
          </article>
        )
      })}
    </>
  )
}

export default BlogIndex

export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: { order: DESC, fields: frontmatter___date }) {
      edges {
        node {
          excerpt(pruneLength: 200)
          id
          frontmatter {
            title
            description
            date(formatString: "MMMM DD, YYYY")

            tags
          }

          fields {
            slug
          }
        }
      }
    }
  }
`
```

This article was originally published on [www.aboutmonica.com](https://www.aboutmonica.com/blog/create-gatsby-blog-search-tutorial).