Hugo Website Search with Pagefind

I integrated Pagefind into my website for a website text content search.

Pagefind is quite interesting in that it builds a search index as part of the build process.

  • Generate static website files with Hugo
  • Call Pagefind; specify CSS selector inclusions and exclusions to indicate the main content that searching should discover
  • Pagefind generates a sectioned search index
  • The website loads the Pagefind JavaScript library and provides some display rendering
  • When and while the user types into the search input, Pagefind loads the appropriate search index sections on demand

This means that Pagefind is

  • very cleanly separated from the website logic - with some integration/analysis overhead but no system-integration complexity
  • very efficient search index; no live text search; sectioned index and loading means minimal bandwidth and latency

Pagefind provides a default UI for simple integration, but that felt too heavy and not fitting to me. I implemented my own simple HTML rendering and CSS styling.

I’d like to add better keyboard UX for it, but tabbing works for now. The input could also look a bit better.

I’m glad it’s finally there though, and works well with snappy search and result listing.


GitHub CI Integration of Pagefind

My GitHub CI step calls pagefind --site "public" --root-selector "main#main" --exclude-selectors "aside" --verbose after building into public with a simple hugo call.

The pagefind executable is made available through a cached dependency install into the Ubuntu runner.

With PAGEFIND_VERSION and PAGEFIND_CHECKSUM defined as environment variables, Pagefind is downloaded through wget --no-verbose https://github.com/CloudCannon/pagefind/releases/download/v${{ env.PAGEFIND_VERSION }}/pagefind-v${{ env.PAGEFIND_VERSION }}-x86_64-unknown-linux-musl.tar.gz -O pagefind.tar.gz and then checked with echo "${{ env.PAGEFIND_CHECKSUM }} pagefind.tar.gz" | sha256sum --strict --check --status. The release checksum can be found in the release pagefind-v<VER>-x86_64-unknown-linux-musl.tar.gz.sha256 file.

The executable is then moved into /usr/local/bin through tar -xf pagefind.tar.gz, rm pagefind.tar.gz, mv pagefind /usr/local/bin/pagefind.

jobs:
  full:
    runs-on: ubuntu-latest
    env:
      HUGO_VERSION: '0.139.3'
      HUGO_CHECKSUM: '3e58800d1fee57269208d07d104ae1a6ab886615344099f2dca0c6ad5279bc11'
      PAGEFIND_VERSION: '1.3.0'
      PAGEFIND_CHECKSUM: '5dfb56609c2d08058c3be56a1a2d332d8dc50d9a6c74f20b3619eadb53240af3'
    steps:
      - uses: actions/checkout@v4
      - id: cache-hugo
        uses: actions/cache@v4
        with:
          path: /usr/local/bin/hugo
          key: ${{ runner.os }}-hugo-${{ env.HUGO_VERSION }}
      - if: success() && steps.cache-hugo.outputs.cache-hit != 'true'
        name: Get Hugo
        run: |
          set -e
          wget --no-verbose https://github.com/gohugoio/hugo/releases/download/v${{ env.HUGO_VERSION }}/hugo_extended_${{ env.HUGO_VERSION }}_linux-amd64.deb -O hugo.deb
          echo "${{ env.HUGO_CHECKSUM }}  hugo.deb" | sha256sum --strict --check --status
          sudo dpkg -i hugo.deb
          rm hugo.deb
      - id: cache-pagefind
        uses: actions/cache@v4
        with:
          path: /usr/local/bin/pagefind
          key: ${{ runner.os }}-pagefind-${{ env.PAGEFIND_VERSION }}
      - if: success() && steps.cache-pagefind.outputs.cache-hit != 'true'
        name: Get Pagefind
        run: |
          set -e
          wget --no-verbose https://github.com/CloudCannon/pagefind/releases/download/v${{ env.PAGEFIND_VERSION }}/pagefind-v${{ env.PAGEFIND_VERSION }}-x86_64-unknown-linux-musl.tar.gz -O pagefind.tar.gz
          echo "${{ env.PAGEFIND_CHECKSUM }}  pagefind.tar.gz" | sha256sum --strict --check --status
          tar -xf pagefind.tar.gz
          rm pagefind.tar.gz
          mv pagefind /usr/local/bin/pagefind
      - name: Build
        run: |
          hugo
          pagefind --site "public" --root-selector "main#main" --exclude-selectors "aside" --verbose
      - name: Package
        run: 7zr a public.7z public
      - uses: actions/upload-artifact@v4
        with:
          path: public.7z
          if-no-files-found: error
          retention-days: 2
      - name: Deploy
        if: success() && github.ref == 'refs/heads/master'
        run: curl --silent --show-error --fail-with-body -X POST -F "file=@public.7z" ${{ secrets.CI_API_URL }}?key=${{ secrets.CI_API_KEY}}