How to add full-text search to an Astro site with Pagefind
Add zero-infra full-text search to any Astro static site using Pagefind 1.5: install, build integration, Component UI, data attributes, CSP, and deploy.
By Ethan
1,753 words · 9 min read
Pagefind is the right choice for search on a static Astro site. It produces a chunked, lazily-loaded index alongside your dist/ output — zero server config, zero SaaS account, zero per-query cost. You run one extra command at build time; Cloudflare Pages, Netlify, and Vercel serve the index files as regular static assets.
This guide wires Pagefind into an Astro project from scratch. If you’re already running Lunr or Fuse.js and wondering whether to switch, skip to the alternatives comparison, read one paragraph, then come back to the steps.
Who this is for
Astro developers deploying to Cloudflare Pages, Netlify, or Vercel (or any CDN that serves static files). The approach works for blogs, docs sites, and marketing pages. You need Node ≥ 18 and a working Astro build (astro build produces a dist/ folder).
If you’re on Astro 2 or earlier, note that the Component UI below requires Astro 3+; the legacy PagefindUI class-based approach works on older versions.
What we tested
| Package | Version |
|---|---|
| astro | 6.3.7 (2026-05-21) |
| pagefind | 1.5.2 (2026-04-12) |
| @pagefind/component-ui | 1.5.2 |
Node 22.x, pnpm 9.x. Deployment target: Cloudflare Pages (static output, no adapter).
Why Pagefind over the alternatives
Three options come up repeatedly for static-site search:
Lunr.js / Fuse.js — in-browser search. The entire index loads on page load. For a small site (under ~200 pages) both work fine. For anything larger, you’re sending megabytes of JSON to every visitor who visits a page that includes the search widget. Index size scales with content volume.
Algolia — hosted SaaS. Fast, excellent UI, great developer experience. Free only for open-source projects (DocSearch). Paid tier has per-operation pricing and requires an account, API keys, and index sync on every deploy.
Pagefind — static, chunked, wasm-powered. The index lives in dist/pagefind/. At query time the browser fetches only the chunks that match the first few characters of the query. A 10k-page site doesn’t slow it down the way an upfront-loaded index would. No account required; the files deploy alongside your HTML.
Pick Pagefind if you want search that scales past a few hundred pages without operational overhead. The rest of this article shows you how.
Step 1: Install Pagefind
pnpm add -D pagefind @pagefind/component-ui
pagefind is the CLI that builds the index. @pagefind/component-ui provides the <pagefind-modal-trigger> and <pagefind-modal> custom elements introduced in Pagefind 1.5. If you’re staying on Pagefind < 1.5, skip the second package and use the legacy PagefindUI class instead.
Failure mode: pnpm add puts these in devDependencies. Don’t move them to dependencies — the index builder is a build-time tool; it has no runtime in production.
Step 2: Wire Pagefind into the Astro build
You have two paths. Choose one.
Option A: npm script (simpler)
Update package.json:
{
"scripts": {
"build": "astro build && npx pagefind --site dist"
}
}
This runs Pagefind after Astro writes dist/. The --site dist flag tells Pagefind where to find the HTML to index. It writes the index back into dist/pagefind/.
Failure mode: if your build output directory isn’t dist/, adjust --site to match. Check astro.config.mjs for an outDir override.
Option B: Astro integration (more control)
Use Pagefind’s Node API inside an astro:build:done hook. This gives you access to the build dir at runtime and avoids hardcoding dist/:
// astro.config.ts
import { defineConfig } from 'astro/config';
import type { AstroIntegration } from 'astro';
function pagefindIntegration(): AstroIntegration {
return {
name: 'pagefind',
hooks: {
'astro:build:done': async ({ dir }) => {
const { createIndex } = await import('pagefind');
const { index } = await createIndex({});
await index!.addDirectory({ path: dir.pathname });
await index!.writeFiles({ outputPath: new URL('pagefind/', dir).pathname });
},
},
};
}
export default defineConfig({
integrations: [pagefindIntegration()],
});
Failure mode: dir.pathname on Windows produces a path starting with /C:/.... Pagefind handles this, but if you hit path resolution errors on Windows, use fileURLToPath(dir) from 'node:url' instead.
Step 3: Scope what Pagefind indexes
By default Pagefind indexes every text node it finds in your HTML. That includes nav, footer, sidebar, cookie banners — anything you don’t want in search results.
Add data-pagefind-body to your main content element to restrict indexing to that subtree:
<!-- src/layouts/ArticleLayout.astro -->
<article data-pagefind-body>
<slot />
</article>
Elements outside data-pagefind-body are excluded. Elements inside that you still want to exclude can be marked data-pagefind-ignore:
<aside data-pagefind-ignore>
Related articles sidebar — excluded from search
</aside>
If no data-pagefind-body element is found in a page, Pagefind falls back to indexing the full <body>. Prefer explicit opt-in.
Failure mode: if search results surface nav text or repeated footer copy, you haven’t added data-pagefind-body. Check your layout HTML with View Source — if the element wrapping <slot /> doesn’t have the attribute, add it.
Step 4: Add the search UI
Pagefind 1.5 ships <pagefind-modal-trigger> and <pagefind-modal> as custom elements. Drop them in your layout:
<!-- src/layouts/BaseLayout.astro -->
---
import '@pagefind/component-ui';
---
<html>
<head>
<meta charset="utf-8" />
<title>Your site</title>
</head>
<body>
<header>
<nav><!-- nav links --></nav>
<pagefind-modal-trigger>Search</pagefind-modal-trigger>
<pagefind-modal></pagefind-modal>
</header>
<slot />
</body>
</html>
<pagefind-modal-trigger> renders a button that opens the modal on click. <pagefind-modal> is the search overlay itself. Both elements are defined by the custom element script bundled with @pagefind/component-ui.
Styling: the component UI uses a shadow DOM, so global CSS won’t bleed into the modal. Use the --pagefind-ui-* CSS custom properties to theme it:
:root {
--pagefind-ui-scale: 1;
--pagefind-ui-primary: #1d4ed8;
--pagefind-ui-text: #1e293b;
--pagefind-ui-background: #ffffff;
--pagefind-ui-border: #e2e8f0;
--pagefind-ui-border-radius: 0.5rem;
--pagefind-ui-font: inherit;
}
Legacy (Pagefind < 1.5): if you’re not ready to upgrade to the Component UI, the PagefindUI class still works. Load the bundle and instantiate it:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
Step 5: Verify locally
Build and preview:
pnpm build # runs astro build + pagefind index
pnpm preview # or: npx pagefind --site dist --serve
astro preview serves dist/ statically, including the dist/pagefind/ folder Pagefind wrote. Open the site, click your search trigger, type a term that appears in an article. Results should surface within ~150ms.
If you want to test indexing in isolation before wiring the UI:
npx pagefind --site dist --serve
This starts a simple HTTP server at http://localhost:1414 that serves your built output. Point your browser there and verify the search widget loads.
Failure mode: if the modal opens but shows no results, check the browser Network tab for requests to /pagefind/pagefind-index-*.pf_meta. If those 404, the index wasn’t written to dist/pagefind/ — likely the build script didn’t run Pagefind after astro build. Confirm your package.json build script includes the && chained command.
Step 6: Deploy
No extra config needed. Pagefind’s output is standard static files — HTML, JSON, and WASM modules in dist/pagefind/. Your CDN serves them unchanged.
Cloudflare Pages: build command pnpm build, output directory dist. The dist/pagefind/ subdirectory deploys automatically alongside your HTML.
Netlify: same pnpm build + dist. Netlify copies everything in dist/ to the CDN.
Vercel: Framework preset Astro, build command pnpm build, output directory dist. No changes needed.
If you haven’t settled on a deployment platform yet, see our static site deploy platform comparison for a side-by-side breakdown of Cloudflare Pages, Netlify, and Vercel on bandwidth limits, free-tier builds, and CDN reach.
Failure mode: if search results work locally but 404 in production, check that your CI/CD build command runs Pagefind (not just astro build). Some platforms let you override the build command in vercel.json, netlify.toml, or wrangler.jsonc — make sure the override matches your local package.json build script.
Caveats
i18n and multiple languages
Pagefind generates a single index by default. Pages in different languages share the same index unless you configure separate per-language builds.
To split indexes, run Pagefind twice with the --language flag:
npx pagefind --site dist --glob "en/**/*.html" --force-language en
npx pagefind --site dist --glob "vi/**/*.html" --force-language vi
The Component UI respects the lang attribute on the <html> element and loads the correct index. Set it in your layout:
<html lang={locale}>
Language coverage: ~60% of languages have full UI translations in Pagefind’s translation catalog. Vietnamese is covered. CJK languages (Chinese, Japanese, Korean) require the extended Pagefind binary (pagefind-extended) to tokenize correctly — the standard binary treats CJK text as a single token.
If you’re building a bilingual EN+VI site, see how to build a multilingual Astro site for the full locale routing, language switcher, and hreflang setup that pairs with Pagefind’s per-language index.
CSP headers
Pagefind loads a WASM module and a web worker at runtime. Add these CSP directives on any page that hosts the search widget:
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:;
wasm-unsafe-eval allows Pagefind’s WASM to compile. worker-src blob: allows the search worker spawned from a blob URL. Without these, Pagefind fails silently in a strict CSP environment.
For Cloudflare Pages, add headers in public/_headers:
/*
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:;
Open issues to watch
- #1157 — Safari WASM compilation error in some environments. Appears environment-specific; most deployments are unaffected. Check the thread if search is broken on Safari and working everywhere else.
- #1126 — Component UI duplicate instance on re-render in SPA-like transitions. Low severity for static sites; relevant if you’re using Astro’s
<ViewTransitions />. Check status before going to production.
If you outgrow Pagefind
Pagefind scales to 10k+ pages without issues — index chunking handles the size. The limit is query complexity: Pagefind is a keyword index, not a semantic search engine. Fuzzy matching and substring search work; ML-based relevance ranking does not.
At that point, Algolia DocSearch (free for open-source) is the natural next step. For a full breakdown of hosted and self-hosted options including Typesense and Meilisearch, see our Search-as-a-Service comparison.
References
- Pagefind documentation — configuration reference, Node API, custom attributes
- Pagefind Component UI —
<pagefind-modal>and<pagefind-modal-trigger>options, CSS custom properties - Pagefind Node API —
createIndex,addDirectory,index.writeFiles - Pagefind changelog — v1.5.0 — Component UI release notes
- Astro integrations guide —
astro:build:donehook,dirparameter - Pagefind i18n — per-language indexes,
--force-languageflag, CJK extended binary - Pagefind GitHub #1157 — Safari WASM issue tracking
- Pagefind GitHub #1126 — Component UI duplicate instance on transitions