Skip to content

Phase 3 — Dual-output docs site with Starlight

IMPL 0003: Phase 3 — Dual-output docs site with Starlight

Section titled “IMPL 0003: Phase 3 — Dual-output docs site with Starlight”

Status: Completed Author: Donald Gifford Date: 2026-05-31

Implement the dual-output docs site: keep the existing MkDocs setup intact for Backstage TechDocs and add an Astro + Starlight pipeline that renders the same docs/ tree as a modern public site served from https://claudelint.dev via Cloudflare Pages.

Implements: DESIGN-0003 — refer there for architecture, deploy decisions (Cloudflare Pages + Pagefind + claudelint.dev), and rollback semantics.

  • New site/ directory housing the Astro + Starlight project (astro.config.mjs, package.json, Astro shell pages).
  • Starlight integration reads from the shared root docs/ tree (no doc relocation).
  • Node toolchain pinned via mise.toml; package-lock.json committed.
  • CommonMark audit of existing docs; markdownlint-cli2 config extended to block MkDocs-only syntax.
  • New .github/workflows/docs.yml running astro build on PRs and pushes to main that touch docs/**, site/**, astro.config.mjs, package.json, or package-lock.json.
  • Cloudflare Pages project bound to this repo’s main branch with PR preview deploys.
  • DNS cutover: claudelint.dev → Cloudflare Pages project.
  • Mermaid render parity verified across MkDocs and Starlight.
  • README and CONTRIBUTING updated to document the dual-pipeline workflow.
  • CLAUDE.md updated with the new site/ directory and docs commands.
  • Replacing or migrating MkDocs (Backstage requirement; explicit non-goal in DESIGN-0003).
  • Versioned docs, blog, or MDX/React components (deferred per DESIGN-0003 non-goals).
  • Teaching docz to emit a Starlight nav config (deferred; filesystem auto-discovery covers v1).
  • Algolia DocSearch (Pagefind only for v1).
  • Migration to MkDocs 2.0 or Zensical (future ADR/RFC).

Each phase builds on the previous one. A phase is complete when all its tasks are checked off and its success criteria are met.


Phase 1 — Scaffold the Starlight project

Section titled “Phase 1 — Scaffold the Starlight project”

Stand up a working Astro + Starlight skeleton in site/ that anyone can run locally with npm run dev and see the default Starlight landing page. No content wiring yet — the goal is to prove the toolchain works.

  • Pin Node LTS in mise.toml (# renovate: annotation included so Renovate tracks it).
  • Create site/ directory.
  • Run npm create astro@latest site -- --template starlight --yes --no-install (or equivalent) inside the worktree.
  • Move/rewrite generated files so the project lives at site/ (Astro project root) with package.json at site/package.json.
  • Add .gitignore entries: site/node_modules/, site/dist/, site/.astro/, site/.astro-cache/.
  • Pin Astro + @astrojs/starlight in site/package.json with caret ranges; commit site/package-lock.json.
  • Add a Renovate config entry (or rely on the default npm manager) so Astro/Starlight bumps land as PRs. (default npm manager handles it per Q8)
  • Add npm scripts: npm run dev, npm run build, npm run preview, npm run check.
  • Run cd site && npm install && npm run dev locally to verify the default Starlight site renders at http://localhost:4321. (verified via npm run check + npm run build; dev server starts cleanly)
  • Add a justfile recipe (just docs-dev) that wraps cd site && npm run dev so contributors don’t have to remember the directory.
  • just docs-dev (or cd site && npm run dev) serves the default Starlight site on http://localhost:4321 with no errors.
  • cd site && npm run build produces a site/dist/ directory with HTML.
  • cd site && npm run check (Astro’s type-check) passes.
  • No Node-toolchain artifacts (node_modules/, dist/) are committed.

Phase 2 — Wire shared docs/ source into Starlight

Section titled “Phase 2 — Wire shared docs/ source into Starlight”

Configure Starlight to read Markdown from the existing root docs/ tree, not from site/src/content/docs/. The sidebar should auto-discover docs and group them by type so the navigation is sensible without a hand-maintained config.

  • Configure Astro’s content collection (or srcDir) in astro.config.mjs to point at ../docs/ from site/. (final approach: symlink site/src/content/docs -> ../../../docs + the bundled docsLoader(). The first attempt used glob({ base: '../docs' }), which serves pages but breaks sidebar autogenerate — Starlight strips a hardcoded src/content/docs/ prefix from each entry’s filePath before matching groups. See the comment in site/src/content.config.ts.)
  • Configure Starlight’s sidebar for filesystem auto-discovery, grouping by directory (rfc/, adr/, design/, impl/, plan/, investigation/). (Starlight v0.39 requires items: [{ autogenerate }] wrapping. Later evolved into the shipped five-group layout — Install / Rules / Development / Reference / Changelog — with the six docz type groups nested under Development; see CLAUDE.md §Sidebar structure.)
  • Verify each doc type’s README renders as the group landing page. (renders at /<type>/readme/; sidebar autogenerate exposes it)
  • Verify all existing RFC/ADR/DESIGN/IMPL/PLAN/INV docs are reachable from the sidebar.
  • Confirm <!--toc:start--> / <!--toc:end--> blocks (docz-generated) don’t render as visible content in Starlight. (HTML comments pass through, invisible)
  • Confirm relative cross-doc links (e.g. [DESIGN-0001](../design/0001-...) from an IMPL doc) resolve correctly in Starlight. (custom remark plugin site/src/plugins/remark-md-link-rewriter.mjs rewrites .md relative links to absolute Starlight routes)
  • Add a docs/index.md landing page (or wire Starlight to use the existing docs/README.md) as the public site homepage. (started as the existing docs/index.md + minimal title: frontmatter; later rewritten from the README pitch — Install / Quickstart / Output formats / Linted artifacts. A hero + CardGrid upgrade is designed in DESIGN-0004.)
  • Add editLink config so each doc page links to its GitHub source for “Edit on GitHub”.
  • Spot-check one doc per type by browsing locally; fix any render issues. (verified via build output + grep on rendered HTML)
  • Sidebar shows all six doc-type groups (RFC, ADR, DESIGN, IMPL, PLAN, Investigation) with all docs listed under each.
  • Clicking any doc renders its Markdown content with frontmatter title as the H1 and the body rendered correctly.
  • The site homepage (root /) shows the landing page content (not a 404).
  • “Edit on GitHub” link on a doc page resolves to the correct file in the repo.
  • No relative-link breakage observed during spot checks.

Phase 3 — Markdown audit + CommonMark lint enforcement

Section titled “Phase 3 — Markdown audit + CommonMark lint enforcement”

Sweep the existing docs/ tree for MkDocs-specific Markdown syntax (!!! note, ???, pymdownx features) that Starlight won’t render. Convert any findings to CommonMark + GFM. Then extend markdownlint-cli2 to block reintroduction at lint time.

  • grep -RE '^\s*!!!|^\s*\?\?\?' docs/ — flag admonitions. (no hits)
  • grep -R 'pymdownx' docs/ — flag pymdownx-specific syntax. (no hits)
  • Convert admonitions to standard > [!NOTE] / > [!WARNING] (GFM) or plain blockquotes. (none needed)
  • Convert any tabs/snippets to plain Markdown. (none present)
  • Verify Mermaid fenced blocks (```mermaid ... ```) render in both MkDocs and Starlight after picking a plugin. (deferred to the Phase 6 parity check. As of 2026-06-08 neither pipeline has Mermaid wired — no rehype-mermaid in site/package.json, no pymdownx.superfences in mkdocs.yml — and no doc contains a Mermaid block yet. Wire both plugins before the first Mermaid diagram lands.)
  • Update .markdownlint.json / .markdownlint-cli2.yaml config to enforce CommonMark + GFM only (no MkDocs extensions). (.markdownlint.yaml + custom grep checks in just lint-md; only legitimate relaxations are MD025 front_matter_title: "" for docz title convention and MD024 siblings_only: true for repeated phase sub-headings)
  • Add just lint-md recipe (or fold into just lint) running markdownlint-cli2 on docs/**/*.md. (runs markdownlint plus three regex grep checks for !!!, ???, and pymdownx)
  • Run markdownlint-cli2 --fix on docs/; commit any auto-fixed nits.
  • Manually fix any remaining violations. (MD040 language hints, MD034 bare URLs, MD038 space-in-code, MD046 mixed code-block style, MD051 broken anchors, MD018 #issue at line start — fixed in source rather than disabling rules)
  • grep -RE '^\s*!!!|^\s*\?\?\?|pymdownx' docs/ returns zero hits.
  • markdownlint-cli2 'docs/**/*.md' exits 0 with the CommonMark profile.
  • All existing docs render in the local Starlight dev server with no visible syntax artifacts.
  • MkDocs still builds locally (mkdocs build --strict) after the syntax conversions.

Phase 4 — CI workflow for Starlight builds

Section titled “Phase 4 — CI workflow for Starlight builds”

Add a new GitHub Actions workflow that runs astro check + astro build on PRs and pushes to main that touch the docs surface. Start non-blocking so it doesn’t gate work while we shake out parity issues, then flip to required after two clean PRs.

  • Create .github/workflows/docs.yml.
  • Triggers: push to main and pull_request to main with paths filter on docs/**, site/**, astro.config.mjs, package.json, package-lock.json, .github/workflows/docs.yml. (paths cover docs/, site/, .markdownlint.yaml, justfile, mise.toml, the workflow itself; astro config + package.json live under site/)
  • Steps: checkout, actions/setup-node@v6 reading version from mise.toml (or package.json engines), npm ci, npm run check, npm run build. (uses jdx/mise-action@v4 to pin Node from mise.toml — single source of truth — rather than a separate setup-node step)
  • Cache ~/.npm and site/node_modules to keep CI fast. (actions/cache@v4 keyed on package-lock.json)
  • Upload site/dist/ as a workflow artifact for inspection on failure. (actions/upload-artifact@v4, 7-day retention)
  • Add a markdown-lint step (markdownlint-cli2 on docs/**/*.md). (separate lint-md job running just lint-md)
  • Also: extend .github/labeler.yml so PRs touching site/** get the documentation label.
  • First-pass: do not add to branch protection. Observe two PRs running clean. (superseded — Donald promoted the checks directly after PR #40’s clean run rather than waiting for a second PR)
  • After two clean runs: add Docs / build (or chosen job name) as a required status check. (done — Docs / Lint Markdown + Docs / Build Starlight added to main branch protection via the dashboard, per PR #40’s task list)
  • Update CLAUDE.md “Git / PR conventions” section to mention the docs check. (added in 6fdacb2 — Docs CI bullet in CLAUDE.md L126)
  • A PR that adds or edits any file under docs/ runs the new workflow and surfaces build success/failure.
  • A PR that touches only Go code skips the workflow (paths filter working).
  • npm run check and npm run build both pass for the current main state.
  • After two clean runs, the workflow is a required check in branch protection.

Phase 5 — Cloudflare Pages deploy + PR previews

Section titled “Phase 5 — Cloudflare Pages deploy + PR previews”

Stand up the Cloudflare Pages project, wire it to the repo, configure build settings, and verify production + preview deploys work end-to-end. At this point the site is live at the Pages-assigned subdomain (e.g. claudelint.pages.dev), not yet at claudelint.dev.

Mostly done (2026-06-08). Donald created the Pages project via the dashboard while PR #40 was open; preview deploys are live (verified at docs-site.claudelint.pages.dev) and the Cloudflare Pages check passes on the PR. Remaining: confirm the Node pin + production-branch settings in the dashboard, the first production deploy (fires when PR #40 merges to main), and optionally saving the API secrets.

  • In Cloudflare dashboard: create a Pages project named claudelint bound to the donaldgifford/claudelint GitHub repo. (exists — the Cloudflare Pages check on PR #40 links to the project dashboard)
  • Configure build settings: framework preset = Astro, build command = cd site && npm install && npm run build, build output directory = site/dist, root directory = repo root. (working — preview builds succeed and serve the full site)
  • Set Node version env var in Cloudflare Pages to match mise.toml Node pin (currently 22.20.0). (set via dashboard, per PR #40’s task list)
  • Set production branch to main. (confirmed — the post-merge production deploy fired off PR #40’s merge commit)
  • Enable preview deployments for all branches with open PRs. (working — per-commit preview URLs like 115f2e1f.claudelint.pages.dev plus the docs-site.claudelint.pages.dev branch alias)
  • Trigger first deploy by pushing a no-op to main (or by manually deploying); verify the Pages-assigned subdomain works. (fired off PR #40’s merge — claudelint.pages.dev and claudelint.dev both serve 200, verified 2026-07-09)
  • Open a test PR that touches a doc; verify Cloudflare Pages auto-comments with the preview URL. (PR #40 itself — the Cloudflare Pages status check carries the deploy link)
  • Verify Pagefind search works on the deployed preview (Cloudflare may need an extra build step to run pagefind; Starlight handles this by default but double-check). (verified — /pagefind/pagefind.js serves 200 on the branch alias; search UI returns results in the preview)
  • Save the Cloudflare Pages project ID and account ID as repo secrets (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN) in case we want a custom deploy workflow later. (not saved as of 2026-06-08 — gh secret list shows neither; optional until we need a custom deploy workflow)
  • https://claudelint.pages.dev (or assigned Pages subdomain) serves the rendered docs site.
  • A PR touching docs/ produces an automatic preview deploy with a unique URL.
  • Pagefind search returns results for queries like “MCP” or “marketplace” against the deployed site.
  • No build errors in the Cloudflare Pages build log for the latest production deploy.

Phase 6 — Cutover to claudelint.dev + polish

Section titled “Phase 6 — Cutover to claudelint.dev + polish”

Point claudelint.dev at the Cloudflare Pages project, verify HTTPS, and do final documentation/onboarding work so future contributors know about the dual-pipeline setup.

  • In Cloudflare DNS for claudelint.dev: add the Pages project as a custom domain (Cloudflare handles cert provisioning automatically). (done via dashboard, per PR #40’s task list)
  • Verify https://claudelint.dev resolves to the Starlight site with a valid cert. (verified 2026-07-09 after PR #40 merged — site, Pagefind, rules reference, and in-site changelog all serve 200 over HTTPS)
  • Test redirect behavior: HTTP → HTTPS, www.claudelint.devclaudelint.dev (or whichever direction you prefer). (HTTP→HTTPS works; www.claudelint.dev has no DNS record as of 2026-07-09 — Donald to decide whether to add a www → apex redirect or leave www unresolvable)
  • Confirm mkdocs build --strict still passes against current docs/. (passes via new just docs-mkdocs-check recipe — runs mkdocs build --strict -d build/mkdocs via uvx; fixed one pre-existing broken cross-link in impl/0001 (../../RELEASE.md → absolute GitHub URL) along the way; documented site_dir collision gotcha in CLAUDE.md)
  • Mermaid parity check: pick (or create) a doc with a Mermaid block, render it via both MkDocs and Starlight, compare visually. (deferred — no Mermaid blocks in docs/ today, and neither pipeline has the plugin wired yet (rehype-mermaid / pymdownx.superfences); do both halves together when the first diagram lands)
  • Update README.md with a “Documentation” section linking to https://claudelint.dev.
  • Update CONTRIBUTING.md (or create one) explaining: docs source = docs/; MkDocs and Starlight both read from there; just docs-dev for local Starlight preview; mkdocs serve for local MkDocs preview. (created)
  • Update CLAUDE.md with: the site/ directory, the new just docs-dev recipe, the dual-output design pointer, and a note that the markdown lint config enforces CommonMark.
  • Update IMPL-0003 phase checkboxes as work completes; flip status to “Implemented” when all phases are done. (status flipped to Completed 2026-07-09 after the production deploy verified. Two items remain open by design: the Mermaid parity check (deferred until the first Mermaid block lands — neither pipeline has the plugin wired) and the www-redirect decision + optional Cloudflare API secrets (Donald’s call). Landing-page polish continues in DESIGN-0004.)
  • Move DESIGN-0003 status from “Draft” → “Implemented”. (flipped 2026-07-09 alongside this doc’s status)
  • https://claudelint.dev serves the docs site with a valid TLS cert and acceptable load time.
  • MkDocs mkdocs build --strict still passes; TechDocs feed is unaffected.
  • Mermaid parity check passes (same fenced syntax renders in both pipelines).
  • README links to https://claudelint.dev.
  • CONTRIBUTING and CLAUDE.md document the dual-pipeline workflow.
  • DESIGN-0003 + IMPL-0003 status moved to Implemented.

FileActionDescription
site/astro.config.mjsCreateAstro + Starlight integration config; points content collection at ../docs/; configures editLink, sidebar auto-discovery, Mermaid plugin.
site/package.jsonCreateAstro + @astrojs/starlight + Mermaid plugin pinned with caret ranges; npm scripts (dev, build, preview, check).
site/package-lock.jsonCreateCommitted lockfile for reproducible installs.
site/src/content.config.tsCreateAstro content collections config (if needed alongside srcDir approach).
site/src/pages/CreateAstro shell pages: index landing page (if not using docs/README.md).
.gitignoreModifyAdd site/node_modules/, site/dist/, site/.astro/.
mise.tomlModifyPin Node LTS with Renovate annotation.
.github/workflows/docs.ymlCreatePR + push CI workflow for Astro build + markdownlint.
.markdownlint.json (or .markdownlint-cli2.yaml)ModifyEnforce CommonMark + GFM; disallow MkDocs-specific syntax.
justfileModifyAdd docs-dev, docs-build, docs-check, lint-md recipes.
docs/index.md (or wired docs/README.md)Create/ModifyPublic-site landing page content.
docs/**/*.mdModifyConvert any MkDocs-only Markdown to CommonMark.
README.mdModifyAdd “Documentation” section pointing to claudelint.dev.
CONTRIBUTING.mdCreate/ModifyDocument dual-pipeline doc workflow.
CLAUDE.mdModifyDocument site/, new just recipes, lint config, and dual-output setup.
  • Local dev (Phase 1): cd site && npm run dev serves Starlight at http://localhost:4321 with no errors.
  • Build smoke (Phase 1, 4): cd site && npm run build produces a non-empty site/dist/.
  • Content wiring (Phase 2): Sidebar enumerates all docs; each doc renders correctly; cross-doc relative links resolve.
  • Markdown lint (Phase 3): markdownlint-cli2 'docs/**/*.md' exits 0 with the CommonMark profile.
  • MkDocs parity (Phase 3, 6): mkdocs build --strict succeeds against the modified docs/ tree.
  • CI (Phase 4): A PR touching docs/ triggers the new workflow; a Go-only PR does not.
  • Cloudflare preview (Phase 5): PR comment lists a preview URL; URL serves the modified docs.
  • Production deploy (Phase 5, 6): Pages subdomain → claudelint.dev after DNS cutover; valid TLS; Pagefind search returns results.
  • Mermaid parity (Phase 6): A test doc with a Mermaid block renders identically (visually equivalent) in MkDocs Material and Starlight.
  • Rollback drill (Phase 6, optional): Verify that disabling the docs workflow or revoking the Cloudflare custom domain doesn’t impact MkDocs / TechDocs build behavior.
  • External services: Cloudflare account access with claudelint.dev registered (Donald has this).
  • Tooling: Node LTS (added to mise.toml in Phase 1).
  • Existing tooling: mkdocs, markdownlint-cli2, just, mise (all already present).
  • GitHub repo secrets: CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN (added in Phase 5, used only if we ever move off the auto-integration).
  • Blocking work: none — IMPL-0003 can proceed on the docs/site branch independently of any Go work.

All Open Questions resolved 2026-06-01.

  • Q1 — Node toolchain location: Isolated site/ subdirectory with its own package.json + lockfile. Keeps Node out of the Go-centric repo root; CI path filters stay unambiguous.
  • Q2 — Node version pinning: mise.toml with # renovate: annotation. Matches the rest of the repo’s tool management; Renovate keeps it current.
  • Q3 — Mermaid renderer: rehype-mermaid via @astrojs/markdown-remark. Server-side rendering, no client JS payload, same ```mermaid fenced syntax MkDocs uses.
  • Q4 — CI blocking behavior: Non-required for the first two PRs after merge; flip to required check in branch protection once both come back clean. Avoids the “first run failed, can’t merge anything” trap.
  • Q5 — Sidebar grouping: Filesystem auto-discovery, grouped by doc type (RFC, ADR, DESIGN, IMPL, PLAN, Investigation), docs ordered numerically within each group. Mirrors docs/<type>/README.md.
  • Q6 — Landing page: Promote docs/README.md as the Starlight homepage via root-page config. Single Markdown source feeds both MkDocs and Starlight.
  • Q7 — Edit-link target: editLink points at https://github.com/donaldgifford/claudelint/edit/main/{path} so readers can propose edits via GitHub’s web editor.
  • Q8 — Renovate: Default npm manager handles Astro/Starlight bumps; no custom group config.