Build browsable HTML for docs/guides in CI

High todo
2026-06-09 agentics feature High effort

Make every Markdown guide under docs/guides/ render as a styled, browsable page on the published GitHub Pages site — by converting them to self-contained HTML with a generated index during the deploy workflow, and never committing the generated output.

Implement Read and implement all steps in the plan at docs/plans/build-guides-html-in-ci.md — Build browsable HTML for docs/guides in CI. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/build-guides-html-in-ci.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
More ways to run this plan — goal & workflow prompts, file path
Pursue as goal — optimize for the outcome, in parallel
Achieve this goal: Build browsable HTML for docs/guides in CI. The plan at docs/plans/build-guides-html-in-ci.md describes one approach — use it as reference, but optimize for the outcome. Fan out across parallel subagents where that serves the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/build-guides-html-in-ci.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
Run as workflow — launch parallel subagents
Run a workflow to implement the plan at docs/plans/build-guides-html-in-ci.md — Build browsable HTML for docs/guides in CI. Brief subagents with the plan file at docs/plans/build-guides-html-in-ci.md. Reserve a final verification phase for the lead agent, not a subagent. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/build-guides-html-in-ci.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
File build-guides-html-in-ci.html
Path docs/plans/build-guides-html-in-ci.html
Spec docs/plans/build-guides-html-in-ci.md
Definition of done 0 / 7 done

Context

The story behind this plan — what prompted the work and why it matters now.

The docs/guides/ directory holds roughly 63 Markdown files. GitHub Pages serves this site with Jekyll disabled ( docs/.nojekyll — its presence is asserted by the deploy workflow), so .md files are delivered as raw text or a download prompt rather than rendered as formatted pages. The guides are effectively un-browsable on the published site.

The Deploy to GitHub Pages workflow ( .github/workflows/deploy-pages.yml ) currently uploads docs/ verbatim via upload-pages-artifact . We will insert a build step that converts every guide to a self-contained HTML page and emits a guides index before the artifact is packed — mirroring the self-contained-HTML philosophy of the plan-interview:markdown-to-html skill. That skill is LLM-driven and cannot run inside GitHub Actions, so we implement a real converter script instead of invoking it.

Repo constraints shape the approach: build scripts live in scripts/ as dependency-light ESM run via node scripts/*.mjs (there is no root package.json ), and node_modules/ is already gitignored. The generated guide HTML must stay out of git — produced only in CI — yet still ship to the site, which works because upload-pages-artifact packs the working-tree docs/ directory after the build step runs.

Files that change

Every file this plan touches, and what happens to each one.

agentics/
  • .github/workflows/deploy-pages.yml modified add build + smoke-test before artifact upload
  • .gitignore modified ignore generated guides HTML
  • docs/index.html modified add Guides card to landing hub
  • docs/guides/
    • index.html generated guides index (CI-only)
    • <guide>.html generated one page per guide (CI-only)
  • scripts/build-guides-html.mjs new render guides + build index
  • tests/publish/test-build-guides-html.mjs new smoke-test generated output

Steps

The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.

1
todo Write scripts/build-guides-html.mjs — the guide converter and index builder
Why
A dependency-light ESM script (mirroring scripts/build-dist.mjs : shebang, node:fs / node:path / node:url imports, ROOT via import.meta.url ) that imports marked , globs every docs/guides/*.md , renders each to GFM HTML, and wraps it in a shared self-contained template whose inlined CSS reuses the :root design tokens from docs/index.html . It derives each page title from the first # heading (HTML-escaped, filename fallback), writes docs/guides/<name>.html , then emits docs/guides/index.html linking every guide alphabetically. One idiomatic script renders all pages and the index; marked handles real-world GFM (tables, fenced code, nested lists) that a hand-rolled parser would get wrong across 63 varied files.
Verify
Run npm install marked@13 --no-save && node scripts/build-guides-html.mjs ; confirm docs/guides/index.html plus one .html per source .md are created. Open the index in a browser, click a guide, and confirm formatted output (headings, code blocks, lists) with no external or CDN asset requests in the network panel.
2
todo Add the smoke test tests/publish/test-build-guides-html.mjs
Why
Mirror the check(name, cond) pass/fail harness in tests/publish/test-dist-transforms.mjs . The test runs the converter, then asserts: (a) docs/guides/index.html exists and links to a known guide; (b) a representative per-guide page exists, starts with <!DOCTYPE html> , and contains an inlined <style> block plus rendered content (an <h1> ); (c) no external/CDN assets are referenced (no <link rel=stylesheet> or remote <script src> ). It exits non-zero on any failure. This locks the objective — browsable, self-contained, indexed guides — into an executable gate so a converter regression fails the build instead of silently shipping broken pages.
Verify
Run node scripts/build-guides-html.mjs then node tests/publish/test-build-guides-html.mjs ; it prints PASS lines and exits 0. Temporarily make the template emit an external stylesheet link and confirm the test fails with exit 1.
3
todo Gitignore the generated guide HTML
Why
Append a docs/guides/*.html ignore rule to .gitignore under an explanatory comment (e.g. # Generated guides HTML — built in CI, never committed ). Confirm no .html is currently tracked under docs/guides/ so the rule only affects generated output. The objective requires generated HTML stay out of git; node_modules/ is already ignored, and because the Pages artifact is packed from the working tree after the build, CI-generated files still ship despite being ignored.
Verify
Run a local build, then git status --porcelain docs/guides/ shows no new tracked files, and git check-ignore docs/guides/index.html docs/guides/add-code-reviewer-agent.html echoes both paths.
4
todo Wire the build + smoke test into deploy-pages.yml before artifact upload
Why
In .github/workflows/deploy-pages.yml , inside the build job, add (1) actions/setup-node pinned to a commit SHA (matching the repo's action-pinning convention) at Node 20, and (2) a Build guides HTML step running npm install marked@13 --no-save && node scripts/build-guides-html.mjs , followed by node tests/publish/test-build-guides-html.mjs as a guardrail — all placed after Assert .nojekyll exists and strictly before Upload Pages artifact . Extend the on.push.paths filter to also include scripts/build-guides-html.mjs . upload-pages-artifact packs the working-tree docs/ , so the generated guides must exist by the time it runs; the extended paths filter redeploys when the converter or template changes.
Verify
Validate the YAML ( actionlint if available, else a YAML parse) and visually confirm the step order is setup-node → build guides → smoke test → upload. Trigger workflow_dispatch (or push to a branch) and confirm the run builds guides and the deployed site serves /guides/ .
5
todo Add a Guides card to the docs/index.html landing hub
Why
Add a third .gallery-card anchor to the .gallery-grid in docs/index.html with href="guides/index.html" , titled “Guides” with a short card-description , mirroring the existing Plans and Social Media cards. This makes the guides discoverable from the site root, consistent with the existing hub cards; without it the index is only reachable by typing the /guides/ URL.
Verify
Open docs/index.html locally after a build (so guides/index.html exists), confirm three cards render and the Guides card navigates to the guides index; keyboard-tab to it and confirm a visible focus ring.

Tests

The tests that prove the change does what it promises.

Tier 1 — Code-touching plan
Objective Guides convert to self-contained, indexed HTML File: tests/publish/test-build-guides-html.mjs Type: smoke test Asserts: running node scripts/build-guides-html.mjs produces docs/guides/index.html (linking the guides) and a per-guide page that is self-contained — starts with <!DOCTYPE html> , carries an inlined <style> block and rendered content (an <h1> ), and references no external or CDN assets. Run: node scripts/build-guides-html.mjs && node tests/publish/test-build-guides-html.mjs
Unit Converter helpers — title extraction, escaping, wrapping File: tests/publish/test-build-guides-html.mjs Targets: exported helpers in scripts/build-guides-html.mjs — extractTitle() , escapeHtml() , renderGuide() Key cases: a title containing <script> is HTML-escaped; a fenced code block renders as <pre><code> ; a guide with no # heading falls back to the filename as its title.
Integration Full build over the real docs/guides/ tree File: tests/publish/test-build-guides-html.mjs Targets: build-guides-html.mjs + marked + node:fs writing into docs/guides/ Key cases: the generated .html count equals the source .md count plus one (the index); every link in index.html resolves to an emitted file.
E2E Browse the published guides from the hub File: tests/pages/ (extend the existing landing-hub smoke tests) Targets: docs/index.html → Guides card → guides/index.html → a guide page, driven in a browser Key cases: the Guides card is visible on the hub and links to the index; a guide page shows rendered headings and code blocks.

Definition of done

The plan counts as done when every statement below is true — check each one off as you verify it.

Final check

One last pass to confirm the whole change works end to end.

Locally: run npm install marked@13 --no-save && node scripts/build-guides-html.mjs , then ls docs/guides/*.html | wc -l should show one page per guide plus index.html . Open docs/guides/index.html in a browser, follow a link to any guide, and confirm it renders with formatted headings, code blocks, and lists — and issues no external or CDN network requests. Open docs/index.html and confirm the Guides card navigates to the guides index.

Guards: run node tests/publish/test-build-guides-html.mjs and confirm it exits 0. Run git status --porcelain docs/guides/ and confirm no generated HTML is staged or tracked. Validate the workflow YAML and confirm the build job's step order is checkout → assert .nojekyll → setup-node → build guides → smoke test → upload.

In CI: push to a branch or run the workflow via workflow_dispatch ; confirm the build job installs marked , runs the converter and smoke test before upload-pages-artifact , and that the deployed Pages site serves /guides/ with every guide reachable as a formatted page from the hub's Guides card.

Wrapping up

Three gates that must all pass before this plan is marked completed.

Required

Completion Report

No items to report — all requirements met.

Next steps

Follow-up ideas that came up along the way — none of them are required to finish this plan.

Group the guides index by topic instead of a flat list

Paste this prompt into Claude to execute this follow-up:

Update scripts/build-guides-html.mjs so docs/guides/index.html groups guides into sections by a derived topic (e.g. the leading verb/prefix of each filename such as add-, fix-, audit-, or a front-matter category when present) instead of one flat alphabetical list. Render each group under a heading, sorted alphabetically within the group. Keep the output self-contained, then re-run `node scripts/build-guides-html.mjs` and `node tests/publish/test-build-guides-html.mjs` to confirm nothing regresses.
Show a last-updated date on each guide

Paste this prompt into Claude to execute this follow-up:

Extend scripts/build-guides-html.mjs to stamp each generated guide page (and its index row) with a last-updated date read from git via `git log -1 --format=%ad --date=short -- <file>`, falling back to the file mtime when git history is unavailable. Note that actions/checkout fetches depth 1 by default, so set `fetch-depth: 0` on the checkout step in .github/workflows/deploy-pages.yml if full history is needed. Re-run the build and `node tests/publish/test-build-guides-html.mjs` to confirm the output stays self-contained.
Client-side full-text search across all guides Wish List

Speculative / blue-sky idea — not on the critical path. Paste into Claude when ready to explore:

Paste this prompt into Claude to execute this follow-up:

Add a lightweight, dependency-free client-side search box to docs/guides/index.html that filters and full-text searches across all guides. Generate a search index (titles plus extracted text) as an inlined JSON blob in index.html at build time inside scripts/build-guides-html.mjs, and implement the filter in a small inline <script> — no external library and no CDN, since the site uses .nojekyll and ships self-contained HTML. Keep the page usable without JavaScript.