Adding a search bar for Hugo with Pagefind

Yesterday I was contemplating the idea of [adding a search bar to this blog]({{< relref "is-it-the-time-to-add-a-search-functionality" >}}). Alright, being honest, this is a tough decision. I really like the idea of my website weighting almost nothing, but I also believe that I should make it easier for people to search for what they are looking after, including myself.

I'll write this as a fun exercise and documentation for the future me. At the end of it, I might have a search bar or not. 🤪

After some research, I decided to settle with Pagefind due to it's "zero-config" nature. I really like almost zero-config software! Now, let the fun begin.

Filtering content and adding metadata

Skimming through the documentation, I found what is needed to only show my posts on "Limiting what sections of a page are indexed". This means that I only need to put the attribute data-pagefind-body on my layouts/_default/single.html layout to have it ignore tags and other Hugo Taxonomies.

It's also possible to add metadata do the pages can also add metadata with the data-pagefind-meta attribute:

<!-- layouts/_default/single.html -->

<!-- ... -->

{{ define "main" }}
  <article data-pagefind-body>
    <h1 data-pagefind-meta="title">{{ .Title }}</h1>
    <!-- ... -->
    <time
      data-pagefind-meta="date[datetime]"
      datetime="{{ .Date.Format "2006-01-02" }}" pubdate>
        {{ .Date.Format "02 Jan 2006" }}
    </time>
    <!-- ... -->
  </article>
{{ end }}

One caveat that I found was that using the attribute data-pagefind-meta="title" would make the output look weird with a bunch of ampersands. This problem is being tracked on the GitHub Issue #459.

Adding the Pagefind UI component

Fortunately, Pagefind does provide a default UI component that is rather easy to use. You can find the JavaScript file for it on the output of the indexing command:

$ pagefind --site public --output-path=static/js/pagefind
# the file will at static/js/pagefind/pagefind-ui.js

One tip I can give is to gitignore the Pagefind output directory, you don't need to commit it to your repository as you can always regenerate it. Now, at the end of my layouts/_default/list.html file I add the needed code to show the search bar.

<!-- layouts/_default/list.html -->

<!-- ... -->

<script src="/js/pagefind/pagefind-ui.js" type="text/javascript"></script>
<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    new PagefindUI({
      baseUrl: "/",
      // search element id
      element: "#search",
      // do not show images
      showImages: false,
      // I want to use my own CSS
      resetStyles: true,
      // do not show subresults of the same page
      showSubResults: false,
    });
  });
</script>
<div id="search"></div>

Building your website

This whole setup does introduce some build complexities to the whole website. This is mostly caused by all the tools not having a common interface to talk to. I'm glad this whole website is orchestrated with a single Makefile1. Let's try to solve this dependency puzzle with the help of our beloved GNU Make.

The first thing we need to build our index, is the public directory Hugo generates. After this, we can run the Pagefind CLI and then publish our website.

public:
	hugo
	pagefind --site public --output-path=static/js/pagefind
	# copies pagefind index to the public directory
	cp -r static/js public

publish: public
	echo "your publishing command"

run: public
	hugo server --buildDrafts --buildFuture

clean:
	rm -rf public
	rm -rf site.tar.gz
	rm -rf static/js/pagefind

.PHONY: clean run publish

This is a nice trick to use, run depending on public ensures that we also have the search bar on our development enviroment with hugo server.

  1. Which is absolutely not POSIX. 😁