From f4d21ca20de31d0fcee1aac75b44d8ba49b8d424 Mon Sep 17 00:00:00 2001
From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
Date: Wed, 23 Sep 2026 10:41:24 -0400
Subject: [PATCH] feat(site): search the explainer and the docs (phase 6)
A search dialog, from the header button or with / or Ctrl+K, over every
section of the explainer and the docs. Results show page and heading
with a marked snippet; arrow keys move, Enter opens, Esc closes and
returns focus.
build_site.py writes search-index.json from the renderer's per-section
text (and the explainer's sections), and the browser fetches it from
the site only when search first opens. site/search.js ranks without a
library: every term must match the start of a word, case and accents
ignored, headings count more than body text, and body matches are
capped so a long section cannot win on length.
scripts/check_search.mjs holds the ranking to cases that say what a
reader should find first, and, given the built site, every index entry
to a page and id that exist and the files the page scripts fetch to be
present (check_site_links.mjs only follows tags). site.yml runs it.
The renderer now keeps a space for a line break inside a paragraph, so
section text reads as prose rather than running words together.
Co-Authored-By: Claude Opus 5.5 (1M context)
---
.github/workflows/site.yml | 6 ++
CHANGELOG.md | 6 ++
scripts/build_site.py | 77 +++++++++++++++++-
scripts/check_search.mjs | 91 +++++++++++++++++++++
scripts/render_docs.mjs | 17 +++-
site/base.css | 20 +++++
site/index.html | 1 +
site/search.js | 125 +++++++++++++++++++++++++++++
site/site.js | 160 ++++++++++++++++++++++++++++++++++++-
tests/test_build_site.py | 44 ++++++++++
10 files changed, 540 insertions(+), 7 deletions(-)
create mode 100644 scripts/check_search.mjs
create mode 100644 site/search.js
diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml
index d6de84e..d679816 100644
--- a/.github/workflows/site.yml
+++ b/.github/workflows/site.yml
@@ -63,6 +63,12 @@ jobs:
node --version
node scripts/check_floor_parity.mjs _site/example-run.json
+ # search.js against cases that say what a reader should find first, and
+ # every search-index.json entry against the page and id it names. Also
+ # the files the page scripts fetch, which the link check cannot see.
+ - name: Search ranks as it should, and every index entry resolves
+ run: node scripts/check_search.mjs _site
+
# The exact bytes the deploy job publishes. On a pull request this is
# also a downloadable preview of the site.
- name: Keep the assembled site
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35ea855..bac0c8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,12 @@ different event from one that moved because it was wrong.
time, so it works with scripts off; `site/site.js` adds only the conveniences
and a light, dark, or automatic theme that is remembered between visits.
Form borders and the histogram's bars now meet 3:1 contrast in both themes.
+- Search across the explainer and every doc, from the header or with `/` or
+ Ctrl+K. The index (`search-index.json`) is written at build time from the
+ rendered sections and fetched from the site only when search opens; the
+ ranking (`site/search.js`) runs in the browser with no library.
+ `scripts/check_search.mjs` holds the ranking to its cases and every index
+ entry to a page and id that exist.
### Changed
diff --git a/scripts/build_site.py b/scripts/build_site.py
index 694fdc1..2c3903a 100644
--- a/scripts/build_site.py
+++ b/scripts/build_site.py
@@ -20,6 +20,11 @@
came from. Nothing is written by hand and nothing is fetched at runtime, so
the pages cannot drift from ``main``: a deploy re-renders them.
+``search-index.json``
+ Every section of the explainer and the docs as plain text, for the site's
+ search (``site/search.js``). A reader's browser fetches it only when they
+ open search, from the site itself.
+
The example is regenerated from the **mock** adapter and nothing else. The
command is read from the report, and anything other than ``--adapter mock`` is
refused, so a vendor run can never be published through this path. The run
@@ -596,6 +601,7 @@ def social_meta(title: str, description: str, url: str) -> str:
Source on GitHub, Apache-2.0 licensed.
No analytics, no trackers, no external requests.
+