Replacing My Custom Pagefind Modal with astro-pagefind
How the astro-pagefind integration replaced three Vite workarounds with a single dependency.
I recommended astro-pagefind at the end of my gotchas post. Then I tried it.
The Problem
The is:inline workarounds from the gotchas post were functional. But they carried costs I didn’t fully appreciate at the time.
35 lines of inline JavaScript sat in the layout — search loading, debounced input, result rendering, a manual trailing-slash fix. A custom <dialog> element handled the search modal. I built both from scratch.
The data-pagefind-body attribute was on the <main> element in the layout. This meant Pagefind indexed the entire site — header, footer, nav — alongside the actual page content. On a small site the noise was manageable. It was still wrong.
The build script ran Pagefind as a post-build step: "astro build && pagefind --site dist". Two commands where one would do.
The gotchas post recommended trying astro-pagefind to avoid all three Vite issues. I wrote that recommendation and moved on. I should have tried it immediately.
What Changed
Integration setup
I added astro-pagefind as a dependency and registered it in the Astro config:
// astro.config.ts
import pagefind from "astro-pagefind";
export default defineConfig({
integrations: [sitemap(), vue(), pagefind()],
});
The integration handles Pagefind’s build step internally. The build script went from "astro build && pagefind --site dist" to "astro build".
Search modal
The custom <dialog> with manual input handling was replaced by the <pagefind-searchbox> component:
<!-- src/layouts/BaseLayout.astro -->
<div id="search-overlay" data-pf-theme="dark" class="fixed inset-0 z-50 hidden bg-black/60 backdrop-blur-sm">
<div class="mx-auto flex h-full w-full items-start justify-center overflow-y-auto p-4 pt-[10vh] sm:max-w-4xl">
<PagefindConfig />
<pagefind-searchbox shortcut="none" placeholder="Search..."></pagefind-searchbox>
</div>
</div>
The overlay open/close logic — click trigger, Cmd+K, Escape, click-outside — stayed as inline JS. The search input, debouncing, result rendering, and import("/pagefind/pagefind.js") loading were removed.
Indexing scope
data-pagefind-body moved from the <main> element in the layout to individual page wrappers. The <article> in blog posts. The <div> wrappers on the about and now pages. Pagefind now indexes only the content that matters.
Trailing slashes
I changed trailingSlash from "never" to "ignore" in the Astro config. The .replace(/\/$/, "") fix from the gotchas post was no longer needed — astro-pagefind handles URL resolution internally.
Dark theme
Pagefind’s component UI ships with its own CSS variables. I mapped them to the existing Gruvbox tokens in global.css:
/* src/assets/css/global.css */
:root {
--pf-text: var(--ui-text);
--pf-background: var(--ui-bg);
--pf-border: var(--ui-border);
--pf-mark: var(--color-yellow-500);
/* ... */
}
Tag pills
I added a # prefix to tag pills across the blog index, blog post, and tag pages. A small visual change bundled into the same changeset.
Results
- Removed ~50 lines of inline JavaScript and the custom
<dialog>markup from the layout - The three gotchas from the previous post — Vite dynamic imports, ES module loading, trailing-slash URLs — are no longer problems I manage manually
- Search indexing is scoped to individual pages instead of the full site layout
- Build script is one command instead of two
The trade-off: the search UI is now Pagefind’s component-based design rather than the custom modal I built. The component handles input, results, keyboard navigation, and theming. I lost direct control over the result rendering — the component renders its own result list.
What I’d Do Differently
I should have tried astro-pagefind before building the custom modal. The gotchas post documented real Vite issues, but the workarounds I built to solve them were unnecessary if I’d started with the integration. The custom modal was the reason I rolled my own — but Pagefind 1.5.x ships its own component UI, which covers the same use case.
References
- Pagefind in Astro: The Gotchas I Hit — the previous post this replaces
- astro-pagefind — the Astro integration
- Pagefind — the search library
- Pagefind Component UI — the built-in search component