Alright, let’s talk about Pagefind. If you’ve just wrestled Lunr.js or Fuse.js into submission, you’re going to look at Pagefind and wonder if it’s a prank. It feels like cheating. Why? Because it does the one thing they don’t: it happens after you build your site.

While the others are client-side libraries you have to manually feed a JSON index you built during your Hugo build, Pagefind takes the opposite approach. It’s a post-processing step. You let Hugo do its thing, spit out a beautifully complete set of static HTML files, and then Pagefind comes along, reads your entire public directory, and builds a search index from the actual final output. This is its killer feature. It means it indexes exactly what your users see, CSS classes and all. No more wrestling with .Plain or .Summary in your index template to avoid dumping Markdown cruft into your searchable content. It just reads the HTML.

Getting started is hilariously simple. You don’t install it as a Hugo module or a theme component. You install it as a standalone binary or via npm. Let’s use npm for this example because it’s dead easy to add to your build process.

First, add it to your project:

npm install --save-dev pagefind

Next, you need to run it after Hugo builds. Your package.json scripts section should look something like this:

{
  "scripts": {
    "build": "hugo --minify && npm run pagefind",
    "pagefind": "pagefind --source public"
  }
}

Now, you run npm run build and watch the magic. Hugo builds your site into public/, and then Pagefind rips through every HTML file in that directory, chunks the content, and spits out a pagefind directory full of its magic (the search index and its tiny WebAssembly library) right into your public/ folder. It’s now a first-class citizen of your built site.

Integrating the Search Interface

Pagefind provides a default UI that is, frankly, pretty good and gets you to a working search in about 30 seconds. You just need to include the CSS and JS wherever you want your search bar to appear. Usually, this is in a partial like layouts/partials/search.html.

<!-- Load the Pagefind style. You can absolutely override this later. -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<!-- This is the element Pagefind will take over -->
<div id="search"></div>
<!-- Load the Pagefind library and instantiate the interface -->
<script src="/pagefind/pagefind-ui.js"></script>
<script>
    window.addEventListener('DOMContentLoaded', (event) => {
        new PagefindUI({
            element: "#search",
            // You can put your index path here if you're hosting in a subdirectory
            // baseUrl: "/my-hugo-site/"
        });
    });
</script>

And that’s it. No, really. You now have a fully functional, styled, accessible search interface that speaks your site’s language. It’s almost absurd how much easier this is than the alternatives.

Why This Approach is Brilliant (and Occasionally Annoying)

The post-build indexing is genius. It sidesteps a whole class of problems. Since it parses the final HTML, it automatically respects your if statements, your partials, your shortcodes—everything. Content hidden behind a display: none? Pagefind won’t index it, because it’s not visible to a user. It creates a 1:1 relationship between what’s on the page and what’s in the index.

But this strength is also its main rough edge. You have to be aware of what’s in your HTML. That giant JSON blob of data you’re dumping into a <script type="application/json"> tag for your interactive map? Yeah, Pagefind might try to index that unless you tell it not to. Which brings us to the most important part: controlling what gets indexed.

Taming the Indexer: Customization and Pitfalls

Pagefind is smart, but sometimes it’s too helpful. You need to be able to tell it to back off. You do this with data-pagefind attributes directly in your HTML.

Say you have a “latest posts” section on your homepage. It’s great for users, but you don’t want every post summary to be indexed on the homepage. That would make the homepage return as a result for almost any query. Wrap it in a data-pagefind-ignore attribute.

<aside data-pagefind-ignore>
    <h3>Latest Posts</h3>
    {{ range first 5 (where site.RegularPages "Type" "in" "posts") }}
        {{ .Summary }}
    {{ end }}
</aside>

You can get more granular. To prevent specific elements from being indexed but still index their children, use data-pagefind-body. To only index a specific area and ignore everything else on the page, use data-pagefind-body on a container and data-pagefind-ignore on its siblings. This level of control is what makes Pagefind powerful enough for complex sites.

The other common pitfall is deployment. Your build process must now include the Pagefind step. If you’re building on Netlify or Vercel, you’ll change your build command from hugo to npm run build. For most folks, this is a trivial change. For some complex CI setups, it’s one more thing to configure. It’s a trade-off: you exchange configuration complexity inside Hugo for a simpler build process that happens outside of it. I’ll take that trade any day.

Pagefind isn’t just a tool; it’s a fundamentally smarter approach to the problem of static search. It acknowledges that the build output is the ultimate source of truth, and by working from there, it eliminates an entire layer of potential bugs and configuration nightmares. Use it.