Implementing blog search functionality with pagefind

🌐

This post has been translated by DeepL . Please let us know if there are any mistranslations!

Pagefind is a client-side search library designed for static websites. it allows you to add powerful search functionality to websites created with Static Site Generator (SSG) or sites built with frameworks like Next.js, Gatsby, Hugo, Jekyll, and more. it can be installed and configured locally without any external APIs or service keys, making it very easy and quick to get up and running.

how it works

Pagefind works in the following process

  1. indexing phase: After your site is built, it analyzes your HTML files to create a search index. during this phase, text content, titles, metadata, etc. are extracted and processed, and a JS file is generated that makes the indexed data available. this means that you can only use it if you generate HTML at build time.
  2. search API: After the index is created, you can use the provided JavaScript API to implement a search interface on your site.
  3. Creating the UI: When a user enters a search query, Pagefind uses the pre-created index to quickly find relevant pages and sections to provide results, and we only need to implement the UI using these results.

you can learn more about the features and how to use them in the Docs.

Getting Started with Pagefind | Pagefind - Static low-bandwidth search at scale

quick start!

write a script

let's cut to the chase and get started. You don't need to install any packages, just follow the steps below.The development environment is using Next.js 15 and pnpm.

first, add postbuild to your package.json

"scripts": {
  // ...
  "postbuild": "npx pagefind --site .next --output-path public/pagefind",
  // ...
},

keep in mind that in order to be indexed, HTML needs to be generated, and in order for HTML to be generated, next needs to be built. after adding this script and running the build, you should see the pagefind-related files under the public folder.

Image.png

now we just need to import pagefind.js and use it.

API Call

first, here's the full code

export default function Search() {
  const [search, setSearch] = useState("");
  const [results, setResults] = useState<PagefindResult[]>([]);
  const [pagefind, setPagefind] = useState<any>(null);

  useEffect(() => {
    const initPagefind = async () => {
      try {
        // Try to load dynamically at runtime

        setPagefind(
          await import(
            // @ts-expect-error
            "./pagefind/pagefind.js"
          ),
        );
      } catch (error) {
        console.error("Pagefind initialization failed:", error);
      }
    };

    // Run only on the client side
    if (typeof window !== "undefined") {
      initPagefind();
    }
  }, []);

  const handleSearch = async (e: any) => {
    setSearch(e.target.value);
    if (!pagefind || e.target.value === "") {
      setResults([]);
      return;
    }

    } const search = await pagefind.search(e.target.value);
    const results = await Promise.all(search.results.map((r) => r.data()));
    console.log(results);
    setResults(results);
  };

  return (
    <div> <input
      <input
        type="text"
        value={search}
        onChange={handleSearch}
        placeholder="Please enter a search term..."
      />

      <div>
        {results.map((result, i) => (
          <div key={result.url}>
            <a href={result.url}>{result.meta.title}</a>
          </div>
        ))}
      </div>
    </div>
  );
}

Create the UI

now that we've made the call, let's see the result.

Image.png

the main things you'll want to look for are

  • excerpt: This is a partial parse of the keyword within the content.
  • meta: Gets the meta information within the content: for title, the first h1 tag encountered, for image, the first image tag encountered after the h1 tag.

all of these can be customized with additional options, so take a look at the docs!

problems

my blog is available in four languages (Korean, English, Chinese, and Japanese), and the search was showing posts in all languages.

Image.png

this is not uncommon for multilingual pages, so of course pagefind provides a multilingual search feature. however, it was based on the value of the lang attribute of the html tag.

but in Next.js, you have to set it in Layout, and as far as I can find, there is no way to get the lang value when building a static page... (If there is, please let me know)

workaround

so, I took a workaround and used the filter function in pagefind. i added the following additional attributes to the h1 tag of each page

<h1
  data-pagefind-filter="lang[data-lang]"
  data-lang={params.lang}
  className="text-3xl md:text-5xl font-bold"
>
  {title}
</h1>

and when we make the API request, we can add filters

const search = await pagefind.search(e.target.value, {
  filters: {
    lang: "en",
  },
});

conclusion

we've seen how to quickly implement a search function using the small and lightweight library pagefind. creating your own search index without relying on external services provides privacy and control, and it also provides a fast and responsive user experience, which is very useful if you're running a blog or documentation site.