Add high-performance search to static sites with PageFind

The concise guide to using Pagefind for your static site’s search functionality.
Great Scott! What a find : a few facts about cocoa ... / J.S. Fry & Sons, Ltd. ; [illustrated by John] Hassall
Great Scott! What a find : a few facts about cocoa ... / J.S. Fry & Sons, Ltd. ; [illustrated by John] Hassall. Public Domain Mark. Source: Wellcome Collection.

Pagefind is a fantastic addition to static site search tools. With very little configuration, it quickly indexes a static build and gives you instant-updating, filterable search. Use the default UI on a search page or, as on this site, build it into a modal. It’s small (as of this writing, just over 2KB minified and gzipped). It has many configuration options, and the maintainer is responsive and open to new ideas. And it has a nice dev mode, running on localhost.

Read Next

I first used Pagefind in Starlight. Check out Comparing docs site builders: VuePress vs Starlight

Here’s everything needed to get going.

Contents

Installation

Install the pagefind package.

Note

The examples here use pnpm. npm run and yarn should work, but I have not tested them.

bash
bash
pnpm add pagefind
bash
pnpm add pagefind

and add the Pagefind CSS and JS to your site.

html
html
<head>
<!-- snip -->
<!-- these files generated by Pagefind -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
</head>
html
<head>
<!-- snip -->
<!-- these files generated by Pagefind -->
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
</head>

Indexing and serving

Add scripts to package.json. Many ways to factor these. I do as below, where build:site is the framework’s build script. Note that Pagefind indexes a local build, so you’ll need to generate one first, and when changing the site source you may need to stop and rerun the dev script. (Replace the ALL CAPS text with your real content.)

package.json
json
json
{
"scripts": {
"build": "pnpm build:site && pnpm build:search",
"build:search": "pnpm pagefind --site _site",
"build:site": "YOUR BUILD COMMAND HERE",
"dev:search": "pnpm build && pnpm pagefind --site BUILD_DIRECTORY_HERE --serve"
}
}
json
{
"scripts": {
"build": "pnpm build:site && pnpm build:search",
"build:search": "pnpm pagefind --site _site",
"build:site": "YOUR BUILD COMMAND HERE",
"dev:search": "pnpm build && pnpm pagefind --site BUILD_DIRECTORY_HERE --serve"
}
}
html
html
<div id="search-box"></div>
<script>
function initializeSearch() {
window.addEventListener('DOMContentLoaded', () => {
const searchBox = document.querySelector('#search-box');
if (!searchBox || !PagefindUI) {
return;
}
new PagefindUI({
element: searchBox,
// other options here. see https://pagefind.app/docs/ui/
});
}, { once: true });
}
initializeSearch();
</script>
html
<div id="search-box"></div>
<script>
function initializeSearch() {
window.addEventListener('DOMContentLoaded', () => {
const searchBox = document.querySelector('#search-box');
if (!searchBox || !PagefindUI) {
return;
}
new PagefindUI({
element: searchBox,
// other options here. see https://pagefind.app/docs/ui/
});
}, { once: true });
}
initializeSearch();
</script>

Tell Pagefind how to scrape your content

Pagefind works well out of the box. If search hits aren’t showing the correct heading, summary, or image, fine tune things with data-pagefind-* attributes.

  • Heading
    Use data-pagefind-meta="heading" to manually specify the heading. Here I assume the heading is visible on the indexed page, but it doesn’t have to be — see the Pagefind docs for alternative approaches.

  • Thumbnail
    Use data-pagefind-meta="image[some-attribute], image_alt[some-other-attribute]" to manually specify the image used by search hits. Here I assume the image you want to show in in the search UI is visible on the indexed page, but it doesn’t have to be — see the Pagefind docs for alternative approaches.

  • Excerpt
    Use data-pagefind-body to manually set a container for Pagefind to index inside of. (Pagefind will still respect data-pagefind-* attributes outside of this container.) Use data-pagefind-ignore to specify content Pagefind should not index.

Putting those together, you might get a page structure similar to this. The highlighted lines are indexed.

html
html
<header>
Not indexed
</header>
<main>
<h1 data-pagefind-meta="heading">Indexed</h1>
<img
alt="YOUR_ALT"
data-pagefind-meta="image[src], image_alt[alt]"
src="YOUR_SRC"
/>
<div data-pagefind-body>
Indexed
<div data-pagefind-ignore>
Not indexed
</div>
Indexed
</div>
</main>
<footer>
Not indexed
</footer>
html
<header>
Not indexed
</header>
<main>
<h1 data-pagefind-meta="heading">Indexed</h1>
<img
alt="YOUR_ALT"
data-pagefind-meta="image[src], image_alt[alt]"
src="YOUR_SRC"
/>
<div data-pagefind-body>
Indexed
<div data-pagefind-ignore>
Not indexed
</div>
Indexed
</div>
</main>
<footer>
Not indexed
</footer>

Add filters to the default Pagefind UI

If your markup has a data-pagefind-filter attribute, the default Pagefind adds a filter pane to the default search UI, with filters grouped into collapsible groups.

With the following markup, Pagefind’s default search UI will have a filter pane with two collapsible filter groups, “Author” and “Tags”.

html
html
<p data-pagefind-filter="author">The author</p>
<section>
<h2>Tags</h2>
<ul>
<li>
<a data-pagefind-filter="Tag" href="…">Search</a>
</li>
<li>
<a data-pagefind-filter="Tag" href="…">Static Sites</a>
</li>
</ul>
</section>
html
<p data-pagefind-filter="author">The author</p>
<section>
<h2>Tags</h2>
<ul>
<li>
<a data-pagefind-filter="Tag" href="…">Search</a>
</li>
<li>
<a data-pagefind-filter="Tag" href="…">Static Sites</a>
</li>
</ul>
</section>

Filters do not have to be visible on the page. One way of indexing filters which do not show on the indexed page’s front end is to use the data-pagefind-filter="name[attribute]" pattern:

html
html
<div
data-filter-tag-1="Some tag"
data-filter-tag-2="Another tag"
data-pagefind-filter="tag[data-filter-tag-1], tag[data-filter-tag-2]"
></div>
html
<div
data-filter-tag-1="Some tag"
data-filter-tag-2="Another tag"
data-pagefind-filter="tag[data-filter-tag-1], tag[data-filter-tag-2]"
></div>

Another is to use hidden:

html
html
<ul hidden>
<li>
<a data-pagefind-filter="Tag">Some tag</a>
</li>
<li>
<a data-pagefind-filter="Tag">Another tag</a>
</li>
</ul>
html
<ul hidden>
<li>
<a data-pagefind-filter="Tag">Some tag</a>
</li>
<li>
<a data-pagefind-filter="Tag">Another tag</a>
</li>
</ul>

See the Pagefind docs for more.

You can see Pagefind in action on this site by clicking “search” in the desktop sidebar or mobile header.

Articles You Might Enjoy

Algolia Search in VuePress Without Joining DocSearch

Or Go To All Articles