Full-Text Search with Pagefind
Search is one of those features that traditionally drags a backend, a hosted service, or a hefty JavaScript index into your otherwise static site. Pagefind flips that around: it runs after your Astro build, crawls the generated HTML, and produces a fragmented search index that the browser loads lazily — only the chunks it needs for a given query. The result is fast, fully static, zero-backend full-text search that scales to thousands of pages while shipping almost no JavaScript on initial load. This fits Astro’s zero-JS-by-default philosophy perfectly.
How Pagefind works
Pagefind operates as a post-build step. Once Astro emits your dist/ directory, the Pagefind CLI walks the static HTML, extracts indexable text, and writes a pagefind/ folder containing a tiny entry script plus many small index fragments. At runtime, the browser downloads the entry script (a few KB), then fetches only the fragments relevant to what the user typed. A 10,000-page site might index to several MB on disk, but a single search transfers only tens of KB.
Because indexing happens against rendered HTML, it works regardless of how you authored content — Markdown, MDX, content collections, or hand-written .astro pages all index the same way. There is no need to maintain a separate search document or feed it your raw source.
Pagefind reads the built output, so it cannot index pages that are server-rendered on demand. Keep search-targeted pages in static (
output: 'static', or prerendered) mode.
Installing and indexing
Add Pagefind as a dev dependency and run it against your build output. Wire it into your build script so the index regenerates on every deploy.
npm install --save-dev pagefind
Update package.json so the index is built right after Astro:
// package.json
{
"scripts": {
"build": "astro build && pagefind --site dist"
}
}
Running the build now produces the index in dist/pagefind/:
npm run build
Output:
Running Pagefind v1.x
Indexed 1 site
Indexed 248 pages
Indexed 9 languages
Indexed 14,302 words
Finished in 0.642 seconds
Controlling what gets indexed
Pagefind indexes the main content of each page by default, but you can guide it with data-pagefind-* attributes in your layout. Mark the region to index with data-pagefind-body, and exclude noise (nav, footers, sidebars) by leaving them outside that region or tagging them with data-pagefind-ignore.
---
// src/layouts/DocLayout.astro
const { title } = Astro.props;
---
<html lang="en">
<body>
<nav data-pagefind-ignore>
<!-- site navigation, never searched -->
</nav>
<main data-pagefind-body>
<h1 data-pagefind-meta="title">{title}</h1>
<slot />
</main>
<footer data-pagefind-ignore>...</footer>
</body>
</html>
Useful indexing attributes:
| Attribute | Purpose |
|---|---|
data-pagefind-body | Marks the region to index; if present anywhere, only tagged regions are indexed |
data-pagefind-ignore | Excludes an element (and its children) from the index |
data-pagefind-meta="title" | Captures metadata returned with each result |
data-pagefind-filter="tag" | Registers a filterable facet (e.g. category, author) |
data-pagefind-sort="date" | Captures a value usable for sorting results |
data-pagefind-weight="2" | Boosts relevance of an element’s text |
Adding the search UI
The simplest integration is Pagefind’s prebuilt UI component. It is a self-contained island: drop a container, load the bundled CSS and JS, and instantiate it on the client. Because the assets live in dist/pagefind/, they only exist after a real build — they are absent during astro dev.
---
// src/components/Search.astro
---
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
<div id="search"></div>
<script>
// The bundle is generated at build time, so import it dynamically.
window.addEventListener('DOMContentLoaded', async () => {
// @ts-expect-error - generated at build time, no types
const { PagefindUI } = await import('/pagefind/pagefind-ui.js');
new PagefindUI({
element: '#search',
showSubResults: true,
showImages: false,
});
});
</script>
Use the component anywhere in a page or layout:
---
import Search from '../components/Search.astro';
---
<Search />
During local development the
/pagefind/assets don’t exist yet, so the widget renders empty. Runnpm run build && npm run previewto test search, or runnpx pagefind --site dist --serveafter a build.
Building a custom search experience
For full control over markup and styling, skip the prebuilt UI and call the JavaScript API directly. pagefind.search() returns lightweight result handles; calling .data() on one lazily loads its fragment, so you only pay for results the user actually sees.
// src/scripts/search.ts
// @ts-expect-error - generated at build time
const pagefind = await import('/pagefind/pagefind.js');
await pagefind.init();
const input = document.querySelector<HTMLInputElement>('#q')!;
const list = document.querySelector<HTMLUListElement>('#results')!;
input.addEventListener('input', async () => {
const { results } = await pagefind.search(input.value);
list.innerHTML = '';
// Load data for the top 5 matches only.
for (const result of results.slice(0, 5)) {
const data = await result.data();
const li = document.createElement('li');
li.innerHTML = `<a href="${data.url}">${data.meta.title}</a>
<p>${data.excerpt}</p>`;
list.appendChild(li);
}
});
The data() payload includes url, excerpt (with <mark>-highlighted matches), meta (your captured title and custom fields), and sub_results for in-page anchors.
Best practices
- Keep
pagefind --site distin yourbuildscript so the index never drifts from your content; never commitdist/pagefind/to source control. - Wrap real content in
data-pagefind-bodyand tag chrome withdata-pagefind-ignoreto keep navigation and boilerplate out of results. - Load the Pagefind bundle dynamically (
await import(...)) — it does not exist duringastro dev, and a static import would break the dev server. - Test search against
astro preview(orpagefind --serve), not the dev server, since indexing requires built HTML. - Use
data-pagefind-metaanddata-pagefind-weightto enrich and tune results instead of post-processing on the client. - For multilingual sites, set
langon<html>so Pagefind builds per-language indexes and applies the right stemming.