New Nov 21, 2024

Static Search with Pagefind

More Front-end Bloggers All from dbushell.com View Static Search with Pagefind on dbushell.com

My website has been static since ā€” checks new search index ā€” at least July 2014!

Iā€™ve gone through so many static site generators and custom build scripts Iā€™ve lost track. In that time Iā€™ve amassed quite a big blog. Finding old articles has become harder than it should be. I need search.

Finding Pagefind

Because my website is ā€œserverlessā€ā€  I have to implement client-side search. That means JavaScript. That means probably not a database and certainly not a multi-megabyte index of any kind. I could easily roll my own fuzzy JavaScript search but I canā€™t ship all the content required to the front-end.

ā€  ā€œServerlessā€ is such a dumb word.

First I found Tinysearch which is a Rust app that compiles WASM. I tested it on my blog content and it generated an impressively small payload. The downside is that Tinysearch is limited to entire words. A search for ā€œraspā€ will not match ā€œRaspberryā€.

I asked around on social media and Dan Burzo was the first to suggest Pagefind. Pagefind also involves Rust & WASM and works with any static build output. For my website Iā€™m restricting it to my blog with --glob config.

npx pagefind --site "build" --glob "<[0-9]:4>/**/*.html" --root-selector "main"

I also configured the root selector to only index content inside a <main> element.

Fallback

Paul Robert Lloyd suggested the clever fallback of directing the form action to a privacy respecting search engine. This is done by coding the search form something like:

<form role="search" action="https://duckduckgo.com" method="GET">
  <label for="search-for">Search for</label>
  <input id="search-for" type="search" name="q">
  <input type="hidden" name="sites" value="dbushell.com">
  <button type="submit">Search</button>
</form>

If progressive enhancement fails ā€” there are many reasons why ā€” the form is still functional. The trick is the hidden input named sites which allows us to restrict the DuckDuckGo query to a specific domain. Iā€™m not aware of other search engines that support such a query parameter.

In my final code Iā€™ve opted to wrap my <form> and results in <search> which negates the need for role="search". I also wrapped the <search> in a <search-form> custom element.

Web Component

At this stage I found Zach Leatherman had already built a Pagefind Search Web Component. Pagefind comes with a default UI and Zachā€™s component uses this for a drop-in search feature. It doesnā€™t get much easier!

Iā€™m using Pagefindā€™s search API to generate custom UI. In my custom element callback I defer the Pagefind setup until after the search input is first focused:

connectedCallback() {
  const input = this.querySelector("input[type='search']");
  input.addEventListener("focus", () => {
    /* Setup Pagefind... */
  }, { once: true });
}

This prevents around 100 KBs from the initial page load that may never be downloaded.

Iā€™ve merged my new search feature into my existing latest blog posts in the sidebar (footer on mobile). Itā€™s not a prominent feature. If I ever redesign my website Iā€™ll make more effort. Search results replace the latest posts or vice versa if the search is empty.

This is how it looks in action:

screenshot of my website search with results for 'rasp' (Raspberry Pi)

Thatā€™s if itā€™s working. Iā€™m still messing around. It will be a little unstable for a while! I havenā€™t tweaked any Pagefind options yet. The default sorting algorithm seems good enough.

Anyway, thanks to everyone who recommended Pagefind. It only takes a second to generate a new static index so I can append that to my build script. Iā€™ve yet to tidy up the technical debt from my build script addition last week šŸ˜¬. Iā€™ve been working on some pre-deployment tests but those are run manually for now.

Immediate update: as prophesied my progressive enhancement failed! It should be fixed now. My content security policy header required the wasm-unsafe-eval directive. Without this ā€œWebAssembly is blocked from loading and executing on the pageā€.

Scroll to top