Adding Pagefind to an Eleventy site

Posted Dec 6, 2023

I just added Pagefind search to this site. Although it only just got bumped to stable, Pagefind is an elegant tool that solves a specific problem very well: adding search to static sites. It even comes with a decent default UI component.

This is a quick note on how to integrate it into an Eleventy site. There are already some posts on the subject but they all boil down to npx the Pagefind CLI in the eleventy.after build event. The CLI is fine if the site is large enough that indexing during development is annoying, but at that point it’s simpler to just run it with a package.json script. For 95% of sites, we can do better.

We’re still going to use the eleventy.after event, but we’re going to use the actual Pagefind Node.js API instead of executing shell commands. Pagefind needs to be imported asynchronously as it’s not a CommonJS module, but that’s really the only quirk you have to get past. Eleventy 3.0 will support ESM, so in turn you’ll be able to import it with regular import statements.

const path = require("path");

eleventyConfig.on("eleventy.after", async function ({ dir }) {
  const inputPath = dir.output;
  const outputPath = path.join(dir.output, "pagefind");

  console.log("Creating Pagefind index of %s", inputPath);

  const pagefind = await import("pagefind");
  const { index } = await pagefind.createIndex();
  const { page_count } = await index.addDirectory({ path: inputPath });
  await index.writeFiles({ outputPath });

  console.log(
    "Created Pagefind index of %i pages in %s",
    page_count,
    outputPath
  );
});

The API uses mostly the same defaults as the CLI so little configuration is needed. In this example we’re just indexing a directory of HTML files, but integrating Pagefind like this allows advanced usage as well. You could, for instance, customize file indexing on the fly using a linter, or index arbitrary data with custom records. Here’s an example that combines these to index specific parts of XML templates.

const path = require("path");

let index;

eleventyConfig.on("eleventy.before", async function () {
  const pagefind = await import("pagefind");
  index = (await pagefind.createIndex()).index;
});

eleventyConfig.addLinter("index-content", async function (content) {
  if (this.outputPath?.endsWith(".xml")) {
    await index.addCustomRecord({
      url: this.page.url,
      content: getSearchableText(content), // Some assembly required
      language: "en",
      meta: { title: getTitle(content) },
    });
  }
});

eleventyConfig.on("eleventy.after", async function ({ dir }) {
  const outputPath = path.join(dir.output, "pagefind");
  await index.writeFiles({ outputPath });
});

Then it’s just a matter of adding the default UI somewhere and everything will just work™ when building the site – even in --serve mode.