#The docs site
docs.planet4.me publishes the documents this repository already keeps, and
nothing else. scripts/build-docs.mjs reads README, ARCHITECTURE, DECISIONS,
BACKLOG and CLAUDE, plus the assistant's capabilities document out of
functions/src/capabilities.ts, and writes a static site to docs-dist/:
one page per document, ARCHITECTURE cut into a page per ## section with a
contents page in front, and the capabilities document as the household's own
guide — verbatim, under a note saying so, because it is the claim the app
makes to the household and CLAUDE.md is what keeps it current. The home
page's opening is the README's, lifted rather than rewritten. Nothing on the
site is prose written for it, so there is no fifth copy of the product's
story to drift. See DECISIONS, "The household's guide is the capabilities
document, verbatim".
Three files under scripts/docs/. markdown.mjs is the renderer: the
subset of Markdown these documents use — headings, paragraphs, fenced code,
pipe tables, nested lists with a fence inside an item, blockquotes, rules;
inline code, bold, *italic*, links — pure, and tested by
markdown.test.mjs, which npm test runs alongside layer 1. Its last block
renders every source document and fails if anything comes out still wearing
its markup, which is how a document that starts using syntax the renderer
does not know is caught before it ships as a page with ** printed on it;
CI runs the build on every push for the same reason. site.mjs is the plan
and the shell — which files, how each is cut, the nav, the on-page contents,
the search index, the page frame — and takes the sources as strings, so the
whole site is buildable in a test. assets/ holds the site's own stylesheet
and script, copied out verbatim. See DECISIONS, "The docs site is a script
in the repository, not a framework".
The palette is the app's: src/styles/tokens.css is copied into the output
at build time, and the docs stylesheet defines only what the app has no use
for (a reading measure, a three-column shell, prose rhythm), so the site is
emerald on sage in light and dark for the same reason the app is, and a
palette change reaches it on the next build.
Search is client-side over one index — a record per heading with the plain
text under it, ids assigned by the same walk that assigns them to the page,
so a hit lands on its section's anchor — written as assets/search-index.js
and loaded by a script tag on the first keystroke, never up front: it is the
largest thing the site has (~430 kB) and most visits never type. A script tag
rather than fetch() because the site also has to work opened straight off
the disk, where a fetch of a local file is blocked; every link on the site
is a real .html file for the same reason, and Hosting's cleanUrls stays
off so those links are served rather than redirected. Results rank a title
hit above a body hit and cap at thirty. The on-page contents ships open and
is folded by the script on narrow screens; the document list is a checkbox,
not a script; nothing on the site needs JavaScript to be read.
Hosting is a second Firebase Hosting site — target docs in firebase.json
and .firebaserc, site smart-list-app-docs, custom domain docs.planet4.me —
rather than a /docs path under the app. The app's service worker owns
every navigation on its origin (navigateFallback to the shell), so a path
under it would be served the app by any phone that had ever opened it; and
Vite copies public/ into the bundle the worker precaches, so a site emitted
there would ride into every install. Its own origin costs one console step
and buys a site the app cannot break and that cannot break the app. npm run
docs:build is the target's predeploy hook. The site id and the domain are
set up once by hand — see README, "The docs site" — and until the site
exists firebase deploy fails on the docs target; --only hosting:app
ships the app alone. See DECISIONS, "The docs site is its own Hosting site,
not a path under the app".