Fuzzy search for Astro using fzf and Preact
Tags: astro, javascript, tutorial, search
Published: 2023-05-25 04:34:09

Static site generators (SSG), like Astro, are great for building static pages. However, they rarely come with site search functionality out of the box. Usually, there is no content management system (CMS) or database that holds the contents and which you could query for search terms.

In this tutorial, we are going to build a simple search for Astro that allows us to query the frontmatter of Markdown/MDX posts. We will create a Preact hook that performs the search. For the UI, we will create a Preact component to let the user enter a search term as well as a Preact component to display the search results in a list.

**Note**: This post was originally published on [my blog](https://blog.stefankolb.de/posts/dev-2-fuzzy-search-for-astro-using-fzf-and-preact/)

## Tools

On the command line, I have been using [fzf](https://github.com/junegunn/fzf) - in combination with [fd](https://github.com/sharkdp/fd) - for quite some time now. I use it to search for directories, previous commands in the shell history and other search/filter related tasks. Luckily, there is also [FZF for JavaScript](https://www.npmjs.com/package/fzf), which is an unofficial port of fzf for the browser. We will use this module to power our search.

For the user interactions, we need some HTML and client side JavaScript. To make things easier for us, we will use [Astro's Preact integration](https://docs.astro.build/en/guides/integrations-guide/preact/), which you need to [setup](https://docs.astro.build/en/guides/integrations-guide/preact/#installation), if you haven't done so already.

### Install fzf

To add fzf to our Astro project, we need to add it as a dependency in our `package.json`:

```json
{
  "dependencies": {
    "fzf": "0.5.1"
  }
}
```

After that, we install it using yarn - or whatever is your package manager of choice:

```shell
yarn install

# If you use npm, use this command instead
npm install
```


## `useFuzzySearch` hook

To perform the actual search, we create a hook that handles searching through the frontmatter fields of our Markdown/MDX posts. We memoize the search result to not perform the same search over and over again, but search only, if either the posts, the search term or the frontmatter field in which we want to search changes.

```js
import { useMemo } from 'preact/hooks';
import { Fzf } from 'fzf';

/**
 * Search for search term in post's frontmatter
 *
 * @param {array} posts List of posts to search in
 * @param {string} searchTerm Term to search for
 * @param {string} field Frontmatter item in which to search
 * @param {object} [fzfOptions={ }] Additional options to pass to fzf
 */
function useFuzzySearch(posts, searchTerm, field, fzfOptions = { }) {
  const fzf = useMemo(() => {
    return new Fzf(posts, {
      selector: post => post.frontmatter[field],
      ...fzfOptions
    }, [ posts, field, fzfOptions]);
  });

  const searchResults = useMemo(() => {
    return fzf.find(searchTerm);
  }, [ fzf, searchTerm ]);

  return searchResults;
}
```


## User Interface

For this tutorial, we are going to keep the UI quite simple. We'll need a component with an input element for the user to enter a search term as well as a component to display the actual search results.

**Note**: I have intentionally left out all CSS stylings, error handling etc. to make it as easy as possible to comprehend this tutorial.

### `Search` component

The search component expects - as prop - the posts whose frontmatter are to be searched and the field that should be searched in. Whenever the `searchTerm` changes, the `useFuzzySearch` hook is executed to retrieve the search results. If there are search results, these will be passed to the `SearchResults` component that we will create in the next step.

```javascript
import { useState } from 'preact/hooks';
import useFuzzySearch from './useFuzzySearch';
import SearchResults from './SearchResults';

const DEFAULT_SEARCH_TERM = '';

function Search(props) {
  // Initialize state variables
  const [ searchTerm, setSearchTerm ] = useState(DEFAULT_SEARCH_TERM);
  const searchResults = useFuzzySearch(
    props.posts,
    searchTerm,
    props.field,
    { fuzzy: false }  // We only search for full word matches. See
                      // https://fzf.netlify.app/docs/latest#api
                      // for available options
  );

  // Called whenever the value of the input field changes.
  function onInput(e) {
    const searchTerm = e?.target?.value;
    if (searchTerm && searchTerm.length > 0) {
      setSearchTerm(searchTerm);
    } else {
      setSearchTerm(DEFAULT_SEARCH_TERM);
    }
  }

  return (
    <div>
      <input
        onInput={ onInput }
        type="text"
      />
      {
        searchResults &&
          <SearchResults
            searchResults={ searchResults }
          />
      }
    </div>
  )
}
```

### `SearchResults` component

The `SearchResults` component will display a simple list with the posts whose frontmatter field matches the `searchTerm`.

```javascript
function SearchResults(props) {
  <div>
    {
      props.noOfSearchResults === 0 &&
        <span>No search results</span>
    }
    {
      props.noOfSearchResults > 0 &&
        props.searchResults.map(result => (
          <p>
            { result.item.frontmatter.title }
          </p>
        ))
    }
  </div>
}
```


## How to use

Now that we have both the logic and UI for our search ready, all we need to do is add our `Search` component to any Astro page. We add the `client:visible` [directive](https://docs.astro.build/en/reference/directives-reference/#clientvisible) to hydrate our Preact component once the Search becomes visible to the user.

```javascript
---
import Search from `Search.jsx`;

const posts = await Astro.glob('@posts/**/*.mdx');
---
<Search
  field="title"
  posts={ posts }
  client:visible
/>
```


## Final words

This is just a basic example on how to add search capabilities to your Astro powered website. As mentioned, I left out all CSS stylings, error handling etc. to make this tutorial easy to understand. However, it should be a good starting point if you want to implement a search on your website yourself.

With the addition of some CSS, the end result might look like the image below. For an interactive demo, go to the [archive page](https://blog.stefankolb.de/archive/) of my blog.

![Example of a fuzzy search with CSS applied](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2v21v0tszf8ixuft3h9.jpg)