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:
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ā.