Create a custom search for a static site
There are out-of-the-box solutions for searching on a static site. I previously used Pagefind for this site which remains a great option. But if you want fully control of the behavior and appearance of your search feature, then you might have to build your own.
After thinking through how I wanted to use a search feature, I came up with these acceptance criteria:
- Query and filter by type, year, and tag: I want to be able to view all Articles from 2023 tagged JavaScript with the text “class”.
- Fuzzy search: Before Pagefind I had a search that used strict string matching, but the experience was poor.
- URL-powered: I want to use it as a custom search engine in the browser and be able to link directly to filtered search results.
With those criteria established, I could move on to designing and building the solution.
Solution
Without going into too many details about how I accomplished this in Astro, here are the high-level steps I followed to build a custom search:
- Gather all of my content into a normalized array: As I’ve written previously, normalizing data is a good idea.
- Expose the data to client-side JavaScript:
- Astro has a helpful
define:vars
directive for this, but your static-site generator has some kind of solution for hand off data with ascript
tag. - The important part for me was to avoid network requests for the content data; I wanted this to be available at build time.
- Astro has a helpful
- In JS sync HTML with URL: Read the search parameters and then update the in-page form elements with the data. For this solution, the URL is the source of truth.
- Create a function to search and render: I wanted the results to render on load and whenever there was a change to the search form. By encapsulating that logic in a function, I was able to call it whenever I wanted.
- Use Fuse.js to search through content: This is the first time I’ve used Fuse and it was awesome. I added it from a CDN with a script tag, and it worked without any issues.
- Render: Take the results from Fuse and add some markup to an
output
element. - Add event listeners for
change
,input
, andsubmit
: These were all added to theform
element to a) sync URL with form state, and b) render updated results.
You can view the finished product at seanmcp.com/search.
- seanmcp.com/search/?q=JavaScript
- seanmcp.com/search/?type=note
- seanmcp.com/search/?tag=A+Few+Things
- seanmcp.com/search/?year=2024
- seanmcp.com/search/?q=class&type=article&year=2023&tag=JavaScript
Lessons learned
- Pushing search parameters to the URL without reloading is easier than I thought. I really like the idea of using the URL as the state for a feature like this.
- The default
select[multiple]
UI is difficult to design around. I ended up limiting the filters to a single selection because I couldn’t find a layout that I liked with multiple. - Fuse.js was a delight, and I look forward to more opportunities to use it.
- I thought that I would need to debounce changes from the form, but in my testing it handled individual keypresses without any issue.
- Astro will scope styles for you, but you can only reference elements that are
currently in the file. This is a problem when you will be building a UI with
client JavaScript. Thankfully Astro works with
template
s, so you can style a sample response element and style it without resorting to:global()
calls.