Skip to content

feat(site): search the explainer and the docs (phase 6) - #69

Merged
TMHSDigital merged 1 commit into
mainfrom
site/phase-6-search
Sep 23, 2026
Merged

TMHSDigital merged 1 commit into
mainfrom
site/phase-6-search

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

Stacked on #68: its base is site/phase-5-shell, so the diff shows only this phase. It retargets to main when #68 merges.

The site has eight docs and a long explainer, but the only way to find something was the browser's find-in-page on one page at a time. This adds site-wide search with no library and no third-party request.

What a reader gets

  • Open search from the header's Search button, or with / or Ctrl+K.
  • Results show page › heading with a snippet that marks the matched terms.
  • Arrow keys move, Enter opens, Esc closes and returns focus. A search that finds nothing says so.

How it works

  • build_site.py writes search-index.json: one entry per section of every doc and of the explainer, 90 entries and about 106 KB.
    • It is built from the per-section text the renderer started returning in phase 5.
    • The browser fetches it from the site only when search first opens.
  • site/search.js does the ranking with pure functions, UMD like floor.js:
    • every term must match the start of a word, ignoring case and accents;
    • a match in a heading counts 3 and a match in the page name 2;
    • body matches count 1 each, capped at 5 per term;
    • ties keep site order, so a page's own entry leads its sections.
  • The dialog is built by site.js, so a reader without scripts never sees a dead control.
  • Snippets are assembled with textContent and <mark> nodes, never innerHTML.
  • Fix found while testing: section text used to run words together across a markdown line break ("Temperaturescaling"). The renderer now keeps the space, with a self-test case.

CI

scripts/check_search.mjs is standard-library Node, and site.yml now runs it. It checks:

  • 12 ranking and snippet cases;
  • that every index entry names a page and an id that exist in the built site;
  • that the files the page scripts fetch or load (the worker, the example run, the index) exist. check_site_links.mjs only follows HTML tags, so it cannot see these. This covers part of CI checks do not cover everything that ships #54.

Checked

  • ruff, mypy --strict, and pytest pass. Three new tests cover the index: its shape and order, refusing an explainer it cannot read, and the per-entry length cap.
  • render_docs --self-test passes 21 cases. check_search.mjs and check_site_links.mjs pass on the built site.
  • In the browser (Playwright): / and Ctrl+K open search, Enter follows the top result, Esc closes and focus returns to the button that opened it. The phone width fits, and the console shows no errors.

Found while testing: a link to /#planner stops short of the planner, because the worked example renders after load and pushes it down. That happens on the live site today. Phase 7 fixes it, since it rewrites that script.

🤖 Generated with Claude Code

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) <[email protected]>
@TMHSDigital
TMHSDigital merged commit 7193335 into main Sep 23, 2026
16 checks passed
@TMHSDigital
TMHSDigital deleted the site/phase-6-search branch September 23, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant