One of the fun things about running your own personal website is that you can add things to it that are not strictly needed, but might be interesting to add anyway. While I try to not actually break the basic functionality of the website, I feel more free to add miscellaneous new stuff.
This is how I ended up deciding to add Pagefind-powered search to my website. I assume that people do not regularly visit my website and then find themselves wishing it had a search feature, so that they could use it to locate a particular web log article, considering I have published fewer than 25 of those over the entire time this website has existed. But, it was possible for me to add such a feature, and so I did.
How Pagefind works
Search engine implementations for static websites can generally be divided into two categories: server-side and client-side. With server-side, something has to build an index of all the pages within the site, and then something has to handle search-related HTTP requests by consulting that index. Unlike a static website, such functionality cannot be hosted off a server that simply serves static files over HTTP—the thing that consults the index needs to be able to execute code on the back end, which makes hosting more complicated. This kind of search functionality often ends up outsourced to a third party.
Pagefind solves the index size problem by splitting the index into chunks. While the index is still mostly the whole website in a different shape, the whole index does not have to be downloaded to perform a search. Instead, the client ends up downloading only the chunks it needs to search for the given terms, and to display the relevant results.
Running Pagefind from Eleventy
Since I run Eleventy directly, and not from a build system or task runner, I wanted to run Pagefind as part of the Eleventy build process. After version 1.0, Pagefind comes with a Node.js interface, which runs the Pagefind binary, and talks to it over standard input and output. I, however, started working on this problem before Pagefind 1.0 was released, and so I went with the simpler method of invoking the Pagefind binary directly.
Eleventy emits a number of events during a build process. One of those events is
eleventy.after, which happens after a build is done (predictably enough). We can hook into this event to run Pagefind on the site, after it's done building:
The npm package for Pagefind pulls in a pre-compiled binary (at least for the supported platforms), so we are calling it through
npx. Since we do not use Pagefind's Node.js API, we could also provision the Pagefind binary in some other way, get it into
PATH, and call it directly. The npm approach is easier, but might be undesirable for situations where building such things locally is preferable.
Pagefind can store arbitrary key–value pairs of metadata for each page it indexes, and the client API can then be used to access that data for each result it finds. Pagefind stores some metadata by default—for example, the title of a result—and then uses that metadata when rendering results with the default results interface.
Since Pagefind indexes the finished HTML output of a static site, this is also where it gets its metadata from. To indicate to Pagefind where this metadata is, specific
data-* attributes can be used. For example, if we wanted to store the
datetime attribute of a
<time> element under the key
date, we would add
data-pagefind-meta="date[datetime]" to that
The Pagefind documentation has detailed instructions on how these attributes are used.
Building a search interface
Building a custom search interface requires use of the Pagefind API. Fortunately, the API is fairly simple. A basic query can look like this:
init() call and the
search() call involve network requests.
init() causes Pagefind to load general metadata and configuration relating to the search database, as well as the relevant WebAssembly bytecode. Once
init() is done, Pagefind can figure out which chunks of index it should download for a given query, and these are the requests that
The object returned (within a promise) by the
search() call does not, however, contain all the metadata for each result. Each potential result—which is to say, each page—has its own file containing both the metadata which Pagefind gathered, as well as the full searched text of the page. For each result, Pagefind can be asked to make a request for the corresponding file. The downside of this approach is the need to potentially make more requests to build a search results page, but the upsides are the fact that the client does not have to download metadata for pages that are not relevant. When using pagination, the client can also delay loading data for further pages, until it is needed.
Fetching the data for a single result looks like this:
Pagefind returns all the metadata fields as strings, which means something like the date field might need to be parsed back into a
Reusing pieces of Eleventy templates
When listing web log posts, my website uses a template fragment to render a post strip—a card which includes the publish date, the title, as well as a short summary of what the post is about. I use this on the front page, as well as on the page which lists all the published web log posts. It looks something like this:
I wanted to reuse the post strip for search results that are blog posts. Using post strips there would both provide consistent design, and also supply some relevant information about what each result is—an excerpt highlighting relevant terms does not always provide that. Pagefind's metadata support provided an obvious means to supply the relevant fields needed for rendering the strip.
A wrapper can then look something like this:
I can then use
renderFile to call the wrapper, from a place like the web log listing page:
For the client side, I use Rollup to pre-compile the Eta template, and then have a function which takes a single Pagefind search result object, and renders a post strip out of it:
In more detail
For a more detailed example I have a branch of my Eleventy example repository that contains enough code to roughly reproduce what my actual website does for Pagefind.