Adding a Simple Search to an Astro Blog

I’ll show you how to add a search like the one on my site to your own Astro project.

We’ll use a package called PageFind to index our Astro site and provide an instant fuzzy search.

No third party services. No complex setup. Totally free.

This should work no matter where your data comes from - Markdown, MDX, or a CMS like Prismic. Best of all, it auto-updates on every page build, so you can set it and forget it.

We’re going to create a /search page, mainly so that we don’t need to worry about styling modals or anything else. It’ll look something like this when we’re done.

A screenshot of the search results on Trost.Codes for the query 'writing'

We’re going to use the astro-pagefind package with a bit of extra customization.

This tutorial assumes you already have an Astro site with content to search for.

Installing and configuring PageFind

To get started you’ll want to install both astro-pagefind and pagefind as we’ll reference that directly in our custom search component.

npm i astro-pagefind pagefind

Then add the integration to the integrations array in your astro.config file.

//astro.config.ts

import { defineConfig } from "astro/config";
import pagefind from "astro-pagefind";

export default defineConfig({
  integrations: [
  ...,
  pagefind()
  ],
});

I put it at the end of the array, as I think that makes it run last.

When Astro builds, this integration indexes the pages that were created. It generates that search index as static files for lightning fast access client-side.

We need to build our site once locally to generate those files. If you don’t build you won’t get any results from localhost.

npm run build

Then you can start your dev server.

npm run dev

Creating the search page

Create a file at src/pages/ called search.astro. Bring over your layout and whatever else you are using across the other pages of your website. Here’s an example structure, but yours will look different depending on your setup.

// src/pages/search.astro

import BaseLayout from "@layouts/BaseLayout.astro"; const title = "Search | Alex
Trost"; const description = "Search the website of Alex Trost";

<BaseLayout title={title} description={description}>
  <h1>Search</h1>
  <!-- Search field will go here -->
</BaseLayout>

If you visit http://localhost:3000/search you should see your layout with your “Search” heading.

Now let’s create the search component itself.

Adding the component

In your components/ directory, create a file named SearchField.astro.

This component handles the text input field and the rendering of the results.

// src/components/SearchField.astro

import "@pagefind/default-ui/css/ui.css"; import { join } from "node:path";
interface Props { readonly id?: string; readonly className?: string; } const {
id, className } = Astro.props as Props; const bundlePath =
join(import.meta.env.BASE_URL, "pagefind/"); const divProps = { ...(id ? { id }
: {}), ...(className ? { class: className } : {}), };

<div {...divProps} data-pagefind-ui data-bundle-path={bundlePath}></div>
<script>
  import { PagefindUI } from "@pagefind/default-ui";
  window.addEventListener("DOMContentLoaded", () => {
    const allSelector = "[data-pagefind-ui]";
    for (const el of document.querySelectorAll(allSelector)) {
      const elSelector = [
        ...(el.id ? [`#${el.id}`] : []),
        ...[...el.classList.values()].map((c) => `.${c}`),
        allSelector,
      ].join("");
      const bundlePath = el.getAttribute("data-bundle-path");
      new PagefindUI({
        element: elSelector,
        bundlePath,
        showImages: false,
        debounceTimeoutMs: 100,
      });

      const input = el.querySelector<HTMLInputElement>(`input[type="text"]`);
      input?.focus();

      // Check if the current URL has any query params
      const url = new URL(window.location.href);
      const params = new URLSearchParams(url.search);
      const query = params.get("q");

      // If query exists on page load
      if (query && input) {
        input.value = query;
        input.dispatchEvent(new Event("input", { bubbles: true }));
      }

      // Add Listener to update the URL when the input changes
      input?.addEventListener("input", (e) => {
        const input = e.target as HTMLInputElement;
        const url = new URL(window.location.href);
        const params = new URLSearchParams(url.search);
        params.set("q", input.value);
        window.history.replaceState({}, "", `${url.pathname}?${params}`);
      });
    }
  });
</script>

I copied the Search component out of astro-pagefind and customized it with a few extra features.

First I made the input get the page focus on load. That’s what input?.focus() does for us.

Next I wanted to handle query parameters so that I could link to search results like this: https://trost.codes/search?q=writing. I added comments to the code where those changes were made.

I also changed the properties that PageFindUI gets instantiated with.

new PagefindUI({
  element: elSelector,
  bundlePath,
  showImages: false,
  debounceTimeoutMs: 100,
});

Don’t change element and bundlePath as those are necessary for it to work. But if you want PageFind to pull in images from your pages, you can set showImages to true. Only some of my content has images, so it was mostly empty space for me. You can find the other properties in the PageFindUI docs.

The rest of the component comes from the astro-pagefind package.

Now add the component to your search page.

//src/pages/search.astro

import BaseLayout from "@layouts/BaseLayout.astro"; import SearchField from
"@components/SearchField.astro"; const title = "Search | Alex Trost"; const
description = "Search the website of Alex Trost";

<BaseLayout title={title} description={description}>
  <h1>Search</h1>
  <SearchField />
</BaseLayout>

Save the file, and you should see the search box on your page.

Screenshot showing the simple search on Trost.Codes

Styling

Luckily they’re using CSS custom properties for styles, so we can easily add our own values. Check the docs on customizing the styles for more details.

Here are the default values.

:root {
  --pagefind-ui-scale: 1;
  --pagefind-ui-primary: #034ad8;
  --pagefind-ui-text: #393939;
  --pagefind-ui-background: #ffffff;
  --pagefind-ui-border: #eeeeee;
  --pagefind-ui-tag: #eeeeee;
  --pagefind-ui-border-width: 2px;
  --pagefind-ui-border-radius: 8px;
  --pagefind-ui-image-border-radius: 8px;
  --pagefind-ui-image-box-ratio: 3 / 2;
  --pagefind-ui-font: sans-serif;
}

I changed out most of the colors for my own CSS custom properties so they change with my site’s dark mode.

If you want to have a completely unstyled search input you can remove the css import at the top of SearchField.astro.

Now deploy your site and it’ll run that build script and index your content.

Wrap up

While services like Algolia and ElasticSearch are super powerful, I like these self-hosted, static solutions for smaller projects.

Is there any additional functionality you want to add to that Search component? Any ways to improve it? I’d love to hear about it.

For more information about the PageFind package, check out the Pagefind docs.