Skip to content

feat(site): host the repository's docs, rendered at build time (phase 3) - #22

Merged
TMHSDigital merged 2 commits into
mainfrom
site/phase-3-docs
Sep 22, 2026
Merged

TMHSDigital merged 2 commits into
mainfrom
site/phase-3-docs

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

Phase 3 of the Pages site: the repository's docs, hosted on the same origin and rendered from the commit being deployed. Nothing deploys yet (Phase 4).

What is hosted. README, METHODOLOGY, docs/PLAN.md, docs/example-report.md, CONTRIBUTING, SECURITY, CHANGELOG, and datasets/public/README.md. Each becomes docs/<slug>.html, with a verbatim copy at docs/<slug>.md, plus a docs/ index page. Nothing is fetched at runtime, from raw.githubusercontent.com or anywhere else.

Renderer: markdown-it 15.0.2, vendored in site/vendor/markdown-it/. LICENSE holds its MIT license, and THIRD-PARTY-LICENSES holds the licenses of the five packages its browser build inlines (entities is BSD-2-Clause, so its notice has to ship). Why this one:

  • It is maintained.
  • It escapes raw HTML by default, and it refuses javascript:, vbscript:, file: and non-image data: link targets.
  • marked passes raw HTML through unsanitized. snarkdown is unmaintained.

I checked the tarball against the registry's sha512. The file's sha256 is pinned in render_docs.mjs, which refuses to run if the file changes, and .gitattributes marks it -text so a Windows checkout cannot rewrite its line endings and break the hash.

One deviation from the brief, on purpose: the docs render at build time, not in the browser. scripts/render_docs.mjs runs the vendored file under the runner's own Node (no install), and build_site.py wraps the result in the page template. The pages carry no script at all, so they work with JS off, and there is no renderer that can fail to load. That is stronger than the "raw markdown if the renderer fails" fallback, which I'd otherwise have built. The vendored file is excluded from the build output because nothing on the site loads it.

Script injection. html: true is on only so raw HTML can be checked against an allowlist of exact tag strings: <details>, <details open>, <summary>, <b>, and their closers, which README's install sections use. Anything else fails the build; nothing is quietly cleaned up.

Links.

  • A relative link to another hosted doc becomes a link to that doc's page, and its #fragment must match a heading there. Heading IDs follow GitHub's rules, so the same anchors work on both.
  • A relative link to any other tracked file goes to github.com/.../blob/<built sha>/... (or tree/ for a directory).
  • A relative link to anything else fails the build.
  • Remote images are rendered as their alt text. README's CI badge is the only one, so the site makes no third-party request.
  • All problems are reported together, with file:line.

Provenance. Each doc page opens with "Rendered from <path> at commit <sha>, built <UTC time>", linking the file at that SHA and the commit. A local build with uncommitted changes to a doc says so on that page. SOURCE_DATE_EPOCH is honoured if set.

Navigation. Every doc page links back to the explainer and to every other doc, with the current page marked by aria-current. There is a skip link. The explainer's nav and footer now link to docs/, and its example-report link goes to the site page instead of GitHub.

Shared styles. The explainer's tokens and base typography moved to site/base.css, which both page types load. docs.css styles the rendered prose. The explainer looks unchanged.

Also fixed: the explainer's footer said "MIT licensed". plumbline is Apache-2.0; only the vendored JevBench fixture is MIT.

CI. site.yml runs node scripts/render_docs.mjs --self-test (14 cases: each refusal, a tampered vendored file, the link rewrites, the allowlist) before building. The build now renders the docs in the same job as the parity check, so the Phase 4 deploy job only needs that job to pass. The job's name is unchanged, so required checks are not affected.

Checked locally:

  • ruff, format, mypy --strict, the self-test, and the build all pass. floor.js still agrees with the Python and reproduces the example report's line.
  • I injected a broken link, a bad anchor, and a <script>: the build refused all three in one message.
  • In a real browser, all nine doc pages plus the explainer have zero horizontal overflow at 375px and 1200px. I viewed them in light and dark, the <details> blocks work, and every request was same-origin.

This branch was cut from main before #21 (em dash cleanup) merged. The two may conflict trivially in CHANGELOG.md.

🤖 Generated with Claude Code

Comment thread scripts/render_docs.mjs Fixed
…he deployed commit

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…tags

CodeQL flagged the strip-then-inspect pattern as incomplete multi-character
sanitization. The output was never the stripped string, so nothing leaked, but
the rule is now stated directly: every '<' must open an allowed tag.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@TMHSDigital
TMHSDigital merged commit 0a3077c into main Sep 22, 2026
14 checks passed
@TMHSDigital
TMHSDigital deleted the site/phase-3-docs branch September 22, 2026 23:34
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.

2 participants