ADR-013: Documentation Platform — Astro Starlight
Status: Implemented
Date: 2026-09-21
Deciders: Project team
Supersedes: the MkDocs + Material + mkdocstrings documentation stack
Context
The documentation site was built with MkDocs, the Material theme, and mkdocstrings, and
validated by the build-docs job in .github/workflows/ci.yml. The build worked, but three
problems had accumulated:
- Authoring ceiling. MkDocs is a Python build tool with a theming layer, not a component framework. Material is already the best MkDocs theme available, so “stay on MkDocs and upgrade extensions” could not close the gap on component-driven pages, offline search quality, or theming control.
- A build that depended on Django. The three
::: moduleautodoc pages (micboard.models,micboard.management,micboard.websockets.consumers) tied the documentation build to the ability to load Django settings. - Autodoc that did not document anything. Because model classes are intentionally not
re-exported from
micboard/models/__init__.py(ADR-001, ADR-002),::: micboard.modelsrendered only the package docstring and a list of submodule names. The API reference pages carried almost no symbols.
A pre-migration inventory of the 76 Markdown pages found that all 76 were reachable from the
MkDocs nav: block, so no page was orphaned before the migration. The markup inventory was
also reassuringly small: one !!! admonition, no pymdownx.tabbed blocks, no Mermaid
diagrams, no snippets, no emoji shortcodes, and 138 file-relative .md links.
Decision
Migrate the documentation site to Astro Starlight, and replace build-time Python autodoc with a committed, pre-generated API reference.
Candidate evaluation
| Candidate | Markdown reuse | Offline search | Versioning | Python autodoc | Verdict |
|---|---|---|---|---|---|
| Astro Starlight | Plain .md with light frontmatter |
Built in (Pagefind) | None built in | Pre-build extraction | Selected |
| VitePress | Simplest migration | Built in (MiniSearch) | None | Pre-build extraction | Runner-up; weakest i18n |
| Docusaurus | MDX-leaning migration | Requires Algolia or a plugin | Best in class | Pre-build extraction | Fallback if versioning becomes required |
| Fumadocs | MDX-first | Built in | Via Next.js patterns | fumadocs-python (experimental) |
Rejected: Next.js static-export weight |
| Nextra | MDX-first | Built in | Limited | None | Rejected |
Starlight wins on the axes that matter here: the existing Markdown is reusable almost as-is, full-text search is offline and account-free, the output is plain static HTML/CSS/JS, and i18n and RTL are first-class if they are ever needed.
Implementation decisions
- Pages stay in
docs/. Rather than moving 76 files intosrc/content/docs/, thedocscontent collection uses Astro’sglobloader withbase: "./docs"(seesrc/content.config.ts). The tree keeps rendering on GitHub,git log --followhistory is preserved, and no path inREADME.md,AGENTS.md, or the issue tracker had to change. - Relative Markdown links are rewritten at build time.
src/plugins/satteri-docs-links.mjsconverts links such as../adr/011-introduce-eventbus.mdinto the page URLs Starlight serves, mirroring Astro’s slug generation. Authors keep writing file-relative links that also work when the file is read on GitHub, and no page content needed editing. - Frontmatter is the only per-page change.
scripts/check_docs_frontmatter.pylifted each page’s leading# Headinginto atitlefield (Starlight renders the title itself, so the duplicate heading is removed) and derived shortsidebar.labelvalues for the verbose SRED page titles. The script stays in the repository as a prek hook so new pages cannot be added without a title. - API reference is generated ahead of the build.
scripts/generate_api_docs.pywalks a curated module list with Griffe’s static analysis — no imports, no Django settings — and writes Markdown intodocs/api/reference/. The output is committed, sojust docsremains a single step, andjust docs-apiregenerates it.prekand CI run the generator in--checkmode to catch drift. - Last-updated dates come from a route middleware. Starlight’s built-in
lastUpdatedsupport only inspects git history undersrc/content/docs/, sosrc/routeData.tsresolves the dates from a singlegit logpass overdocs/. The CI job checks out full history for this reason. - Hosting is GitHub Pages, published from CI. The repository’s Pages source is the
GitHub Actions workflow:
build-docsuploadssite/as a Pages artifact onmainand thedeploy-docsjob publishes it to https://justprosound.github.io/django-micboard/. The deploy job opts out of the workflow-widecancel-in-progressconcurrency group, because Pages refuses concurrent deployments.base: "/django-micboard"inastro.config.mjsmatches that project-pages sub-path, andsite/remains the build output directory the CI job validates. - Brand tokens live in the theme.
src/styles/micboard.cssmirrors the application palette frommicboard/static/micboard/css/theme.css, and the header logo is imported directly frommicboard/static/micboard/logo.pngso the docs site and the application cannot drift on artwork.scripts/sync-docs-assets.mjscopies the canonical favicon into the generatedpublic/directory at build time rather than committing a second copy.
Consequences
Positive
- The build no longer imports Django or reads
DJANGO_SETTINGS_MODULE; it is hermetic and completes in a few seconds. - The API reference now documents the model domains, the service layer, management commands,
the WebSocket consumers, and the exception hierarchy — with
Args/Returns/Raisessections and deep links to source lines — where the previous autodoc pages listed only module names. - Search is offline, built into the site, and requires no external account.
- Page coverage, link integrity, search, brand rendering, and WCAG 2.1 AA compliance are all
enforced in CI (
scripts/check_docs_coverage.py,scripts/check_docs_links.py, and the Playwright suite intests/docs-e2e/).
Negative and accepted trade-offs
- Inline autodoc is gone. A
::: moduledirective can no longer be dropped into a page; a module must be added toPAGESinscripts/generate_api_docs.pyand the reference regenerated. This is the load-bearing cost of the migration and is accepted in exchange for a hermetic build. If the extractor ever falls short for a specific surface, the fallback is hand-authored Markdown for that surface plus links to source. - The toolchain now spans two ecosystems. Node and npm become prerequisites alongside
uv; once they are installed,just installinstalls both dependency trees.just docsfails with a clear message when npm is unavailable, and reinstalls the Node tree when the lockfile is newer than it. - No per-release versioning. Starlight has no built-in versioning and the community
starlight-versionsplugin is experimental. We do not need per-release snapshots today. If that changes, the options arestarlight-versionsor a move to Docusaurus, whosedocusaurus docs:versionCLI is mature. Reopening the platform question for versioning alone is out of scope. - URLs changed. The site moved off Read the Docs (
.readthedocs.yamland the exporteddocs/requirements.txtare deleted), and slugs are now generated by Astro, sochangelog/breaking-changes-v26.01/is served aschangelog/breaking-changes-v2601/. - i18n is available but unused. Starlight supports localisation and RTL; no locale work is in scope, and none is configured.
Compliance
just docsbuilds the site;just serve-docsserves it on port 9000 with hot reload.just docs-verifyruns the frontmatter, reference-freshness, page-coverage, and link checks.just docs-e2eruns the search, brand, and accessibility suite against the built site.- The
build-docsCI job runs all of the above and uploadssite/as a Pages artifact onmain;deploy-docsthen publishes it to GitHub Pages. - A merge performed with
GITHUB_TOKEN, such as a bot-merged release pull request, does not raise thepushevent, so that commit publishes nothing.deploy-docstherefore also runs onworkflow_dispatch; republish withgh workflow run ci.yml --ref main.
