monotux.tech

Implementing search on this site

Hugo CI/CD Woodpecker

I decided to add a search function to this blog, but this time I had to accept some browser side JavaScript for it to function. This entry will describe how I implemented this (using pagefind) on this site.

Table of Contents

To make this work, we will essentially:

  1. Build hugo
  2. Run pagefind on this output (pagefind --site public)
  3. Deploy the static site

For testing I’ve just downloaded a prebuilt binary and ran it on my laptop, but for CI/CD we will…do the same thing, but in a container.

Some light testing gives that loading the search view will consume ~100 KB of bandwidth, and searching will consume 100-200 KB more. I know this isn’t a lot these days, and that bandwidth et c is cheap, but I still take some silly pride in keeping this site lean.

diagram

Hugo

Search bits

First up, the list view for searching:

{{ define "main" }}
<h1>Search</h1>

<p>This search <strong>requires</strong> JavaScript, and will consume several hundreds of KBs of bandwidth while in use!</p>

<p />

<div data-pagefind-ignore id="search"></div>

<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>

<script>
  window.addEventListener("DOMContentLoaded", () => {
      new PagefindUI({
          element: "#search",
          showSubResults: true,
          resetStyles: true,
          pageSize: 10,
          showImages: false,
          autofocus: true,
          sort: {
              date: "desc"
          }
      });
  });
</script>
{{ end }}

Then, to make sure this is actually rendered by hugo, we need to add an empty page in the right place. I saved mine as content/search/_index.md and it just contains:

---
title: "Search"
date: 2026-01-26T12:48:48+01:00
url: "/search/"
outputs: ["HTML"]
---

Adding more metadata

This was the tricky part – which metadata to include in the search results?

I went with the following:

This section is a bit hand-wavy as it’s very specific for my blog, but adding them went something like this:

<!-- Part of my baseof.html  -->
{{- $indexable := true }}
{{- if in (slice "home" "taxonomy" "term") .Kind }}
  {{ $indexable = false }}
{{- end }}

{{- if eq (.Params.pagefind | default true) false }}
  {{ $indexable = false }}
{{- end }}

  {{- if eq .Section "search" }}
    {{ $indexable = false }}
  {{- end }}

{{ if and (eq .Kind "section") (eq .Section "posts") }}
  {{ $indexable = false }}
{{ end }}

  <main {{ if $indexable }}
    data-pagefind-body
  {{ else }}
    data-pagefind-ignore
  {{ end }}>
    {{ block "main" . }}{{ end }}
  </main>

For my default single.html:

    {{ partial "chroma-theme.html" . }}
    <article>
      <header>
-       <h1>{{ .Title }}</h1>
+       <h1 data-pagefind-meta="title">{{ .Title }}</h1>
        <p>
-        <time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
+        <time data-pagefind-sort="date:{{ .Date.Format "2006-01-02" }}" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
          {{- with .Params.tags }}
-         <span class="tags">
+         <span class="tags" data-pagefind-meta="tags:{{ delimit . "," }}">
          {{ range . }}
          <a class="tag" href="{{ "/tags/" | relURL }}{{ . | urlize }}">{{ . }}</a>
          {{ end }}

For my series snippet:

{{- if .Params.series -}}
-       <div class="series-info">
+       <div class="series-info" data-pagefind-meta="series:{{ .LinkTitle }}">
      {{- with index (.GetTerms "series") 0 -}}
      <p>This post is part of the <b>{{ .LinkTitle }}</b> series.</p>
      {{- end -}}
        {{- $series := where .Site.RegularPages.ByDate ".Params.series" "intersect" .Params.series -}}
        {{- with $series -}}
        <p>

You get the idea!

Woodpecker

This will just add an intermediate build step before publishing this, below is a partial for my PR workflow (which just publishes to an internal staging environment):

when:
  - event: pull_request

steps:
  # skipping step mermaidcharts due to length...

  build:
    image: hugomods/hugo:go-git-0.150.1
    depends_on:
      - mermaidcharts
    commands:
      - hugo --baseURL=$INTERNAL_URL --minify --buildDrafts --buildFuture
    environment:
      INTERNAL_URL:
        from_secret: internal-url

  pagefind:
    image: alpine:latest
    depends_on:
      - build
    commands:
      - apk add wget
      - wget --quiet https://github.com/Pagefind/pagefind/releases/download/v1.4.0/pagefind-v1.4.0-x86_64-unknown-linux-musl.tar.gz -O /tmp/pagefind.tar.gz
      - tar zxf /tmp/pagefind.tar.gz -C /tmp
      - /tmp/pagefind

  # skipping the publishing step...

…and Bob is your mothers brother!