Skip to content
Astro as patterns 4 min read

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:

AttributePurpose
data-pagefind-bodyMarks the region to index; if present anywhere, only tagged regions are indexed
data-pagefind-ignoreExcludes 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. Run npm run build && npm run preview to test search, or run npx pagefind --site dist --serve after 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 dist in your build script so the index never drifts from your content; never commit dist/pagefind/ to source control.
  • Wrap real content in data-pagefind-body and tag chrome with data-pagefind-ignore to keep navigation and boilerplate out of results.
  • Load the Pagefind bundle dynamically (await import(...)) — it does not exist during astro dev, and a static import would break the dev server.
  • Test search against astro preview (or pagefind --serve), not the dev server, since indexing requires built HTML.
  • Use data-pagefind-meta and data-pagefind-weight to enrich and tune results instead of post-processing on the client.
  • For multilingual sites, set lang on <html> so Pagefind builds per-language indexes and applies the right stemming.
Last updated June 14, 2026
Was this helpful?