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:
@@ -3,3 +3,4 @@
|
||||
/resources/_gen/
|
||||
.hugo_build.lock
|
||||
__pycache__/
|
||||
node_modules/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Search
|
||||
layout: search
|
||||
---
|
||||
@@ -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,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,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
@@ -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,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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user