Add site search (Pagefind)

Client-side index generated after the Hugo build; search box in the
header, results page with a section filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDRjtmAiNpo5mK5N3d7QwZ
This commit is contained in:
2026-10-11 20:31:00 +02:00
co-authored by Claude Opus 5.5
parent 8883befad8
commit fda5359932
17 changed files with 100 additions and 16 deletions
+1
View File
@@ -3,3 +3,4 @@
/resources/_gen/
.hugo_build.lock
__pycache__/
node_modules/
+4 -1
View File
@@ -1,6 +1,8 @@
OC ?= ../oc
# not public/: a running `hugo server` renders there, with its live-reload script
BUILD ?= build
# search index generator (https://pagefind.app), fetched by npx on first use
PAGEFIND ?= npx -y pagefind@1
# test server: the web share of barbel-lescar, mounted from the file manager (smb://barbel-lescar/web)
DEPLOY_DIR ?= /run/user/$(shell id -u)/gvfs/smb-share:server=barbel-lescar,share=web/oldcomputers
DEPLOY_URL ?= https://barbel.synology.me/oldcomputers/
@@ -10,9 +12,10 @@ DEPLOY_URL ?= https://barbel.synology.me/oldcomputers/
content: ## regenerate content/ and static/media/ from the extraction repo
python3 $(OC)/scripts/05_hugo.py --target $(CURDIR)
build: ## build the site into $(BUILD)/
build: ## build the site and its search index into $(BUILD)/
rm -rf $(BUILD)
hugo --panicOnWarning --minify -d $(BUILD)
$(PAGEFIND) --site $(BUILD)
check: build ## build, then verify every internal link and image
python3 tools/check_links.py $(BUILD)
+10 -1
View File
@@ -29,10 +29,19 @@ banner, date, author and summary (`summary:` in the front matter, or the text be
`<!--more-->`, or the first lines); all posts are listed under News. Posts are never touched by
`make content`.
## Search
Search runs in the visitor's browser on an index generated by [Pagefind](https://pagefind.app/)
at the end of `make build` (fetched by `npx`, so Node.js is needed to build). The header box
sends to `/search/`, where results can be filtered by section. Content pages are indexed
(`data-pagefind-body` on `<main>` in `layouts/baseof.html`); lists, taxonomy pages and the
visitor-comments tabs are not. `hugo server` has no index: use `make build` and serve `build/`
to try the search locally.
## Usage
make content # regenerate from ../oc (OC=path to use another location)
make build # build into build/
make build # build into build/, with the search index
make check # build + verify every internal link and image
make serve # preview on http://localhost:1313
make deploy # build and copy to the test server (share must be mounted; see DEPLOY_DIR/DEPLOY_URL)
+29
View File
@@ -129,3 +129,32 @@ table { border-collapse: collapse; }
.post-summary p:last-child { margin-bottom: 0; }
.post-banner { display: block; width: 100%; max-height: 420px; object-fit: cover; margin: 0 0 16px; border-radius: 4px; }
@media (max-width: 600px) { .post-summary { grid-template-columns: 1fr; } }
.site-header .search { margin-left: auto; }
.site-header .search input {
width: 180px;
max-width: 100%;
padding: 4px 10px;
border: 1px solid transparent;
border-radius: 14px;
background: rgba(255, 255, 255, 0.12);
color: inherit;
font: inherit;
font-size: 0.9rem;
}
.site-header .search input::placeholder { color: inherit; opacity: 0.6; }
.site-header .search input:focus { outline: none; border-color: var(--accent); background: rgba(255, 255, 255, 0.2); }
/* Pagefind result list, in the site colours */
#search {
--pagefind-ui-primary: var(--accent);
--pagefind-ui-text: var(--text);
--pagefind-ui-background: var(--panel);
--pagefind-ui-border: var(--line);
--pagefind-ui-tag: var(--bg);
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 4px;
--pagefind-ui-image-border-radius: 2px;
--pagefind-ui-font: inherit;
--pagefind-ui-scale: 0.85;
}
#search mark { background: none; color: inherit; font-weight: 700; }
+4
View File
@@ -0,0 +1,4 @@
---
title: Search
layout: search
---
+2 -2
View File
@@ -2,13 +2,13 @@
{{ $m := .machine }}
{{ $labels := dict "photos" "Photos" "adverts" "Adverts" "hardware" "Hardware" "connectors" "Connectors" "software" "Software" "emulators" "Emulators" "links" "Links" "docs" "Docs" "videos" "Videos" "comments" "Comments" }}
<header class="machine-header">
<p class="crumbs">
<p class="crumbs" data-pagefind-ignore>
<a href="{{ (site.GetPage "/museum").RelPermalink }}">Museum</a>
{{ with $m.GetTerms "categories" }}{{ range first 1 . }} › <a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>{{ end }}{{ end }}
{{ with $m.Params.company }}{{ with site.GetPage . }} › <a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}{{ end }}
</p>
<h1>{{ with $m.Params.logo }}<img class="logo" src="{{ . }}" alt="">{{ end }}{{ $m.Title }}</h1>
<nav class="tabs">
<nav class="tabs" data-pagefind-ignore>
<a href="{{ $m.RelPermalink }}"{{ if not $.tab }} class="active"{{ end }}>System info</a>
{{ range $m.Params.tabs }}
<a href="{{ $m.RelPermalink }}{{ . }}/"{{ if eq . $.tab }} class="active"{{ end }}>{{ index $labels . }}</a>
+1 -1
View File
@@ -1,6 +1,6 @@
{{ define "title" }}Authors · {{ site.Title }}{{ end }}
{{ define "main" }}
<p class="crumbs">{{ with site.GetPage "/post" }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}</p>
<p class="crumbs" data-pagefind-ignore>{{ with site.GetPage "/post" }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}</p>
<h1>Authors</h1>
<ul class="columns">
{{ range .Data.Terms.Alphabetical }}
+1 -1
View File
@@ -1,6 +1,6 @@
{{ define "title" }}Posts by {{ .Title }} · {{ site.Title }}{{ end }}
{{ define "main" }}
<p class="crumbs">{{ with site.GetPage "/post" }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }} › <a href="{{ .Parent.RelPermalink }}">Authors</a></p>
<p class="crumbs" data-pagefind-ignore>{{ with site.GetPage "/post" }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }} › <a href="{{ .Parent.RelPermalink }}">Authors</a></p>
<h1>Posts by {{ .Title }}</h1>
<div class="posts">
{{ range .Pages.ByDate.Reverse }}{{ partial "post-summary.html" . }}{{ end }}
+8 -1
View File
@@ -18,8 +18,15 @@
<a href="{{ .URL }}"{{ if or ($current.IsMenuCurrent .Menu .) ($current.HasMenuCurrent .Menu .) (eq $current.Section .Page.Section) }} class="active"{{ end }}>{{ .Name }}</a>
{{ end }}
</nav>
<form class="search" role="search" action="{{ "search/" | relURL }}" method="get">
<input type="search" name="q" placeholder="Search…" aria-label="Search the site">
</form>
</header>
<main>
{{/* search index: content pages only (not lists), filterable by section; see layouts/search.html */}}
{{ $indexed := and .IsPage (ne .Layout "search") (ne .Params.tab "comments") }}
{{ $section := "" }}{{ with .FirstSection }}{{ if not .IsHome }}{{ $section = .Title }}{{ end }}{{ end }}
{{ if eq .Section "machine-tabs" }}{{ $section = "Museum" }}{{ end }}
<main{{ if $indexed }} data-pagefind-body{{ with $section }} data-pagefind-filter="Section:{{ . }}"{{ end }}{{ end }}>
{{ block "main" . }}{{ end }}
</main>
<footer class="site-footer">
+1 -1
View File
@@ -1,6 +1,6 @@
{{ define "main" }}
<article>
<p class="crumbs"><a href="{{ .Parent.RelPermalink }}">Companies</a></p>
<p class="crumbs" data-pagefind-ignore><a href="{{ .Parent.RelPermalink }}">Companies</a></p>
<h1>{{ with .Params.logo }}<img class="logo" src="{{ . }}" alt="">{{ end }}{{ .Title }}</h1>
<div class="content">{{ .Content }}</div>
{{ with .Params.machines }}
+2 -2
View File
@@ -1,10 +1,10 @@
{{ define "main" }}
<article>
<p class="crumbs"><a href="{{ .Parent.RelPermalink }}">History</a>{{ with .GetTerms "topics" }} › {{ partial "terms.html" (dict "page" $ "taxonomy" "topics") }}{{ end }}</p>
<p class="crumbs" data-pagefind-ignore><a href="{{ .Parent.RelPermalink }}">History</a>{{ with .GetTerms "topics" }} › {{ partial "terms.html" (dict "page" $ "taxonomy" "topics") }}{{ end }}</p>
<h1>{{ .Title }}</h1>
{{ with .Params.when }}<p class="meta">{{ . }}</p>{{ end }}
<div class="content">{{ .Content }}</div>
<nav class="prev-next">
<nav class="prev-next" data-pagefind-ignore>
{{ with .PrevInSection }}<a href="{{ .RelPermalink }}">← {{ .Title }}</a>{{ end }}
{{ with .NextInSection }}<a href="{{ .RelPermalink }}">{{ .Title }} →</a>{{ end }}
</nav>
+1 -1
View File
@@ -3,7 +3,7 @@
{{ $m := site.GetPage .Params.machine }}
{{ $tab := .Params.tab }}
{{ $p := $m.Params }}
<article class="machine">
<article class="machine" data-pagefind-meta="title:{{ .Title }} – {{ .Params.tab | title }}">
{{ partial "machine-header.html" (dict "machine" $m "tab" $tab) }}
{{ if in (slice "photos" "adverts" "hardware" "connectors") $tab }}
+2 -2
View File
@@ -1,11 +1,11 @@
{{ define "main" }}
<article class="post">
<p class="crumbs"><a href="{{ .Parent.RelPermalink }}">{{ .Parent.Title }}</a></p>
<p class="crumbs" data-pagefind-ignore><a href="{{ .Parent.RelPermalink }}">{{ .Parent.Title }}</a></p>
<h1>{{ .Title }}</h1>
<p class="meta">{{ if not .Date.IsZero }}<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2 January 2006" }}</time>{{ end }}{{ with .GetTerms "author" }}{{ if not $.Date.IsZero }} · {{ end }}{{ partial "terms.html" (dict "page" $ "taxonomy" "author") }}{{ end }}</p>
{{ with partial "banner.html" (dict "page" . "width" 1000) }}<img class="post-banner" src="{{ . }}" alt="">{{ end }}
<div class="content">{{ .Content }}</div>
<nav class="prev-next">
<nav class="prev-next" data-pagefind-ignore>
{{ with .PrevInSection }}<a href="{{ .RelPermalink }}">← {{ .Title }}</a>{{ end }}
{{ with .NextInSection }}<a href="{{ .RelPermalink }}">{{ .Title }} →</a>{{ end }}
</nav>
+31
View File
@@ -0,0 +1,31 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
<div id="search"></div>
<noscript><p>Searching needs JavaScript.</p></noscript>
<p id="search-missing" class="note" hidden>The search index is not available in this preview: it is generated by <code>make build</code>.</p>
<link rel="stylesheet" href="{{ "pagefind/pagefind-ui.css" | relURL }}">
<script src="{{ "pagefind/pagefind-ui.js" | relURL }}"></script>
<script>
if (window.PagefindUI) {
// the site may live in a sub-directory: its root is the parent of this page
const root = new URL("..", location.href).pathname;
const ui = new PagefindUI({
element: "#search",
baseUrl: root,
showSubResults: false,
pageSize: 10,
// images are stored with page-relative addresses: make them absolute
processResult: (result) => {
if (result.meta && result.meta.image) {
result.meta.image = new URL(result.meta.image, new URL(result.url, location.href)).pathname;
}
return result;
},
});
const q = new URLSearchParams(location.search).get("q");
if (q) ui.triggerSearch(q);
} else {
document.getElementById("search-missing").hidden = false;
}
</script>
{{ end }}
+1 -1
View File
@@ -1,6 +1,6 @@
{{ define "main" }}
<article>
{{ with .Parent }}{{ if not .IsHome }}<p class="crumbs"><a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ with $.Params.group }} › {{ . }}{{ end }}</p>{{ end }}{{ end }}
{{ with .Parent }}{{ if not .IsHome }}<p class="crumbs" data-pagefind-ignore><a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ with $.Params.group }} › {{ . }}{{ end }}</p>{{ end }}{{ end }}
<h1>{{ .Title }}</h1>
<div class="content">{{ .Content }}</div>
</article>
+1 -1
View File
@@ -1,6 +1,6 @@
{{ define "main" }}
<article>
<p class="crumbs"><a href="{{ .Parent.RelPermalink }}">Software</a></p>
<p class="crumbs" data-pagefind-ignore><a href="{{ .Parent.RelPermalink }}">Software</a></p>
<h1>{{ .Title }}</h1>
<div class="content">{{ .Content }}</div>
{{ with .Params.specs }}{{ partial "specs.html" . }}{{ end }}
+1 -1
View File
@@ -1,5 +1,5 @@
{{ define "main" }}
<p class="crumbs"><a href="{{ .Parent.RelPermalink }}">By {{ partial "taxonomy-label.html" .Data.Singular }}</a></p>
<p class="crumbs" data-pagefind-ignore><a href="{{ .Parent.RelPermalink }}">By {{ partial "taxonomy-label.html" .Data.Singular }}</a></p>
<h1>{{ .Title }}</h1>
{{ range .Pages.GroupBy "Section" }}
<h2>{{ with site.GetPage (printf "/%s" .Key) }}{{ .Title }}{{ end }} <small>({{ len .Pages }})</small></h2>