Add Searching To Your Astro Static Site
Published: 2023-01-05 15:38:16

> This guide was initially published on my blog. Check it out [here](http://blog.otterlord.dev/post/astro-search)

Astro is great for static site generation, but it doesn't come with any built-in search tools out of the box. While some may use third-party tools such as Algolia, I wanted to avoid relying on third-party services.

Enter [Pagefind](pagefind.app/), a static search library for indexing your content and presenting search results on your static site. Pagefind is framework agnostic, but setup can be a little tricky.

## Installing Pagefind

Install Pagefind as a development dependency. The package contains a CLI tool that will generate the actual JavaScript to run on your site.

```sh
npm install --save-dev pagefind
```

## Adding A `postbuild` Script

Pagefind needs to run after your site has been built, because it analyzes the HTML files to generate the search index. Add a `postbuild` script to your `package.json` to run Pagefind after your site has been built. The source directory will be the output of Astro's build (`dist`) and the bundle directory (which is always placed inside the source directory) will be `pagefind`.

```json
{
  ...
  "scripts": {
    ...
    "postbuild": "pagefind --source dist --bundle-dir pagefind"
  },
  ...
}
```

## Adding a Dev Endpoint

A big issue I came across when first solving this, is that there's no way to inject the pagefind bundle into your site at development time, because the site only exists as memory. I solved this by adding a dev endpoint to my site, which will serve a "fake" Pagefile script filled with 0 results. This way, the script will always be available, and the search results will always be empty. It's a little hacky, but it works. Create a new file at `src/pages/pagefind/pagefind.js.ts` with the following contents:

```js
import type { APIContext } from "astro"

export async function get({}: APIContext) {
  return {
    body: 'export const search = () => {return {results: []}}'
  }
}
```

There's probably a better way to do this, but this will prevent your site from screaming at you when you try to access the pagefind script at development time. During build time, since Pagefind is run after the site is built, the actual Pagefind script will replace the dev endpoint.

## Adding a Searchbar

To keep things simple, I'm going to simply use an `<input>` element as a searchbar, just to show how to integrate Pagefind's library. You can choose to put this anywhere on your site. If you're using the default Astro template, you can add it to `src/pages/index.astro` for example.

What we're doing here, is listening to the `input` event on the searchbar, and then loading the Pagefind script if it hasn't been loaded yet. Once the script is loaded, we can use the `search` function to search the index. The `search` function returns the results. Each result has a `data` function, which returns the data for that result. In this case, we're using the `url` and `meta.title` properties to create a link to the result, and the `excerpt` property to show a preview of the result. You can find a reference to the structure returned by `data` [here](https://pagefind.app/docs/api/#loading-a-result).

```astro
<input id="search" type="text" placeholder="Search...">

<div id="results" />

<script is:inline>
  document.querySelector('#search')?.addEventListener('input', async (e) => {
    // only load the pagefind script once
    if (e.target.dataset.loaded !== 'true') {
      e.target.dataset.loaded = 'true'
      // load the pagefind script
      window.pagefind = await import("/pagefind/pagefind.js");
    }

    // search the index using the input value
    const search = await window.pagefind.search(e.target.value)

    // clear the old results
    document.querySelector('#results').innerHTML = ''

    // add the new results
    for (const result of search.results) {
      const data = await result.data()
      document.querySelector('#results').innerHTML += `
        <a href="${data.url}">
          <h3>${data.meta.title}</h3>
          <p>${data.excerpt}</p>
        </a>`
    }
  })
</script>
```

The benefit of asyncronously loading the Pagefind script is that it won't affect the performance of your site. The script is only loaded when the user starts typing in the searchbar. Allowing you to keep all 100s in your Lighthouse score 😎

## Excluding Elements From The Index

Pagefind will index all of the text in the `body` element by default, excluding elements like `nav`, `script`, and `form`. If you want to exclude additional elements from the index, you can add the `data-pagefind-ignore` attribute to the element. I recommend doing this on any lists or archive pages to prevent the index from being bloated with duplicate content.

## Wrapping Up

Now you can expose a good search experience to your users, without a third-party provider. It took me a few hours to get this working, so hopefully this will save you some debugging time. You won't be able to search your site in development, but you can always build your site to test it out.

---

Thanks for reading! If you have any questions, feel free to reach out to me on [Twitter](https://twitter.com/theotterlord) or [Discord](https://dsc.gg/otterlord).