Home → The Classics → Farai's Codelab
Half Assing My Static Site’s Search
While I’ve managed to implement search on my static Hugo site twice before (once with lunr.js and another via a search engine), I haven’t implemented in the many redesigns my site has gone through. For this site’s redesign, I decided to add it as a way to make up for the site’s bad information architecture. I’ve also discovered that it’s also an accessibility technique so I guess I get brownie points for doing this? Anyways, it didn’t take as long to implement as I thought it would, though it has a lot of room for improvement.
How I Half Assed The Search Feature
The implementation involved the following:
- Making the index of pages to search,
- Creating the search form,
- making the web worker which actually does the searching using fuse.js, and
- making the code which ties all of this together as well as output the result.
Making the Search Index
For this I created a JSON file which contains templated Go. It iterates through the site’s blog posts and adds them to an array which I then transform into a JSON string. I placed this under /assets/all_pages.json
in my Hugo project directory.
I was going to create a JSON Feed file to do this to limit the duplication since I’ll need to make a JSON Feed anyway, but I was worried it about the caching issues which aren’t warranted in hindsight.
Making the search form
To make the search form, I set up a custom template page. The search form itself looks like this:
There isn’t much to it—just the search form, the search field and it’s input and the search button. Interestingly, it’s all wrapped in a <search>
element that came out the same day I was making this! The search element is just a semantic way of pointing out search functionality. Thanks to progressive enhancement, the search element falls back to being a div
on unsupported browsers, so the role="search"
attribute is used to provide the semantics.
The label will be visually hidden at some point though I’m not sure I need it since the search button describes what it’s for. The input itself uses the standard q
and is of type="search"
which doesn’t do anything besides adding some native UI hints1. In iOS Safari, that means adding a search icon as well as the submit button being called “search” instead of “go”. Again, progressive enhancement means it will fall back on being a regular text input.
The button is well, a button that doesn’t do anything yet. Try and submit that request and you end up on the same page. Before fixing that, we need to implement the search logic. The buttons and inputs are set to 1rem to stop Safari from zooming in.
Underneath the form will be the div containing the results.
<div id="search-results" tabindex="-1" hidden>
<div>
<h2></h2>
<p></p>
</div>
<ul>
</ul>
</div>
- The
div
starts out hidden and hastabindex="-1"
so I can focus on it once the results have been generated. - The child
div
has anh2
and ap
tag. Theh2
relays how many results were found and thep
tag states how long it took to search. - The
ul
will have the actual results.
Actually Searching Content with Fuse.js in a Worker
This is where the search happens though it looks rather boring.
The most interesting parts are those 2 comments on top. The point of them is to fetch the aformentioned all_pages.json
along with getting the fuse.js library and fingerprinting them so I can get a unique URL that I can tell browsers to cache forever. I then import the fuse.js
script wit importScripts
and fetch the pages JSON.
Wait, why am I using Go templates in JavaScript?
Because I can thanks to Hugo’s resource.ExecuteAsTemplate function which executes an asset as a go template.
If so, why don’t I have any errors?
Because I did that logic in a comment as well as within regular strings.
Is this a good idea?
Probably not, but 🤷🏾♂️.
Now with the fuse.js library and the pages JSON, we can now build the index such that it searches the title
and content
.
I then need to receive queries from the main thread as well as send the results which I do using a onmessage
handler and postMessage
. All the handler does it search fuse with the given query and time the search time before using postMessage
to send a response with the results.
All of this is done in an IIFE wrapped with a single error hander which I know to be an amazing idea.
Tying this all together.
With all that set up, we can finally piece together the search functionality.
Remember that ExecuteAsTemplate
fuction I said we would use to get the worker? It’s here and it’s set up such that I can aggresivley cache each unique version of the worker so I don’t have to worry about cachebusting since the file ends with a sha512 hash.
With the worker, we then hook up out DOM elements. I think I’m doing this badly so if anyone knows a better way let me know.
When the form is submitted, I send a message to the worker with the query and I have a message handler to get the query results which then creates the HTML to render onto the page. Once that’s done, I render everything onto the page, along with the time it took to search for the result.
Comparison With My Prior Search Implementations
This implementation seems a lot less involved compared to the last time I did this with lunr.js and there seems to be less code. I gues fuse.js is less work to set up, but I didn’t use “web components”2 this time though I probably should.
Future Improvements
- Properly testing accessibility. While I’d like to think I made a form with the appropriate labels and that moving the focus on search is sufficient, there might be other things I’m missing that I’ll need to fix.
- Add a search form in the footer or on the side.
- Provide options to search via search engine. I mean, they’re probably better at doing this than I am.
- Prebuild the search index. I don’t know if this will make a meaningful improvement, but it’s worth a shot. It will just make my Hugo site a tad bit more complicated3.
- Make the search more useful. I’ve mostly done this for the sake of having it, but the search isn’t well tuned yet. I also need a way to set various filters to make it more useful.
- Evaluate “search” libraries. There wasn’t a good reason for picking fuse.js and there are a lot of libraries availible like lunr.js and Pagefind. Maybe I’ll decide to roll it on my own since they’re just using the levenshtien distance underneath right?
- Make the search results nicer. For now I’m posting the post’s title. Thing is, posts have a lot of metadata like dates and tags which should be shown as well. I should also highlight search terms. This will need me to have a better approach to rendering them, one that works well on the server side as well.
- Implement server side search. This seems like something I can do in Cloudflare Functions as a fallback in case someone doesn’t have JavaScript availible.
Conclusion
In all, making this wasn’t as scary as I thought it would be. Still, there is a lot of room for improvement. Once I’ve figured out what I’m doing, I’ll share the source code. Performance wise, it’s okay for now but I’m wondering what it will be like once I add more pages onto the site. It took my iPhone 12 Pro Max 0.008 seconds to search the site while my co-workers shitty Android tablet took 0.542 seconds, which is a whole 77.2× slower! I guess that’s a good reason for using web workers since this would make a slow browser hang otherwise.
I’m glad this is done as it’s one of the last big pillars for my site along with link indexing.
No, you can’t turn the icon off unless you don’t use a text input to begin with. ↩︎
Only web component-y part about it is that I used the HTML
<template>
tag ↩︎I am so tempted to set up a worker in a test environment which builds the search index throuh a service worker. That’s what it’s good for right? ↩︎