Appearance
Docs Wiki β Design β
- Date: 2026-07-16
- Status: Approved (brainstorming) β ready for implementation plan
- Branch:
feature/docs-wiki
Goal β
A small, self-hosted wiki over the repo's docs/ markdown, added to the Nx workspace. It must:
- Render every
.mdunderdocs/with a browsable sidebar + full-text search. - Hot reload on add β editing a doc reloads instantly, and dropping in a new
.mdmakes it appear in the sidebar live (no manual restart). - Build to a static site that deploys to Cloudflare Pages, gated behind Cloudflare Access (internal-only; the docs contain internal architecture and authz notes that must not be world-readable).
Confirmed decisions β
| Decision | Choice |
|---|---|
| Tool | VitePress (Vite-based SSG; instant HMR + built-in local search) |
| Where it lives | Nx project docs, rooted at the repo-root docs/ (not apps/docs) β content stays in place, .vitepress/ co-located per VitePress convention, no srcDir-outside-root gymnastics |
| Content scope | docs/**/*.md only |
| New-file behaviour | Appears live on add (custom watch-restart plugin) |
| Deploy | nx build docs + Cloudflare Pages project dispatch-docs, gated by Cloudflare Access |
Architecture β
text
docs/ β Nx project root (project.json lives here); existing .md untouched
.vitepress/
config.mts β nav, search, sidebar wiring, srcExclude, ignoreDeadLinks, vite plugin
sidebar.ts β hand-written tree scanner (buildSidebar) + pure helpers
sidebar.test.ts β bun test coverage for the scanner
live-sidebar.ts β Vite plugin: watch *.md add/unlink β debounced server.restart()
theme/
index.ts β extends default theme, imports custom.css
custom.css β thin Dispatch brand accent (derived from #C1D841 / #10130B)
index.md β minimal hand-authored landing page (VitePress "home" layout)
project.json β nx targets: serve / build / preview (nx:run-commands, cwd: docs)
β¦all existing docsβ¦ β rendered as pages, unchanged- VitePress project root =
docs/. The dev server and build run withcwd: docs. - The Nx project is registered by
docs/project.json; its name isdocs.
Components (each independently understandable) β
config.mtsβ the single source of VitePress configuration. Calls the sidebar scanner, registers thelive-sidebarVite plugin, sets exclusions and search. Depends onsidebar.ts+ the plugin; consumed by the VitePress CLI.sidebar.tsβ a small hand-written scanner (~60 lines, unit-tested) that walks the docs tree at config-load and returns a nested, collapsible sidebar grouped by folder. Each file's label is its first# H1(fallback: prettified filename); date-prefixed files (YYYY-MM-DD-β¦) sort newest-first, others alphabetically; empty folders are omitted. Paths are anchored to this file's own directory viaimport.meta.url, NOTprocess.cwd()β so it is correct regardless of the directory Nx runs the command from. This replaces thevitepress-sidebarpackage (whosedocumentRootPathresolves againstprocess.cwd(), which is fragile under Nxcwd: docs, and whose exact API could not be verified against current docs). Pure helpers (titleFromContent,prettifyName) and the tree builder (buildSidebar) are covered bysidebar.test.tsrun withbun test(no new test dependency).live-sidebar.tsβ a ~20-line Vite plugin (configureServer(server)). Watches via Vite's built-in chokidar watcher; onadd/unlinkof a.md(change events ignored to avoid loops, debounced ~150ms) it bumps the mtime ofconfig.mts(viautimes, no content change β git stays clean), which makes VitePress run its own config-change restart. That path re-runsconfig.mtsβbuildSidebar()re-scans β the new file is in the sidebar. Why not callserver.restart()directly: a plugin-initiatedserver.restart()does not reset the local-search MiniSearch index, so withsearch.provider: 'local'every restart throwsMiniSearch: duplicate ID β¦βserver restart failedand the sidebar never refreshes (vuejs/vitepress#4251). VitePress's own config-restart clears that index; touching the config delegates to it. This is the only bespoke runtime code beyond the scanner.theme/custom.cssβ overrides VitePress--vp-c-brand-*to the Dispatch primary and sets the brand font family. Cosmetic; no logic.
Hot-reload-on-add: how it works β
Two layers, deliberately separated:
- Content edits β VitePress/Vite HMR handles this natively. Free.
- New file appears live β Vite auto-registers a route for a new
.md, but the sidebar is computed once at config-load, so it will not include the new file until config re-runs.live-sidebar.tsforces that re-run by touchingconfig.mts(mtime bump) when a.mdis added or removed, which triggers VitePress's own config-change restart (search-index-safe β see thelive-sidebar.tscomponent note). Net effect:touch docs/foo.mdβ it shows in the sidebar within ~1s, with local search intact.
Content scope & exclusions β
Scope is docs/**/*.md only. Safety is mostly automatic:
- VitePress serves only markdown β the sensitive
docs/google_client_secret*.jsonanddocs/samples/*.pdf(and all.go/.ts/.pysource underdocs/implementation/contract-qa/) are never served. No code needed. srcExclude: ['**/contract-qa/**']β drops the vendored contract-qa prototype'sREADME.mdfiles from the VitePress page set. Thesidebar.tsscanner applies the same exclusion (acontract-qapath check + skipping.vitepress,node_modules,samples) so the sidebar and the route set agree.ignoreDeadLinks: trueβ the existing docs were not authored for VitePress routing and cross-reference each other with plain relative paths; without this the build fails on dead links. Set it now to keepnx build docsgreen; can be tightened to'localhostLinks'or a targeted list later.
Theming β
Thin brand layer only: theme/custom.css overrides the VitePress brand variables (--vp-c-brand-*, --vp-button-brand-*) with shades derived from the Dispatch primary #C1D841 / primary-content #10130B, so the site doesn't read as stock VitePress. For accessibility, links use a darkened lime on the light theme (the raw #C1D841 is a surface colour, not a text colour) and the raw lime on the dark theme; solid accents/buttons use the exact #C1D841 background with #10130B text. No brand fonts are loaded (avoids an external font fetch in the docs theme) β colour accent only. This is intentionally not a full port of the design system; that would be scope creep for a docs tool.
Nx targets & dependencies β
docs/project.json (executor nx:run-commands, cwd: docs):
| Target | Command | Notes |
|---|---|---|
serve | bunx vitepress dev | HMR + live-sidebar watcher |
build | bunx vitepress build | static output β docs/.vitepress/dist; wire outputs for Nx cache |
preview | bunx vitepress preview | serve the built site locally |
- Dev dependencies (via bun):
vitepressonly. Novitepress-sidebar(we hand-write the scanner), no extra watcher dependency (reuse Vite's bundled chokidar), no extra test runner (use built-inbun test). .gitignore: adddocs/.vitepress/distanddocs/.vitepress/cache.- The
docsproject participates in the workspacenx run-many -t lint test buildgate (CI + husky pre-push): Nx infers alinttarget, andproject.jsondeclaresbuild+ atesttarget that runs the sidebar scanner spec viabun test. This is intentional β a broken docs build/lint is caught before merge, consistent with the repo's "never let CI find it" rule. (Trade-off: a future doc with unescaped Vue{{ }}would failnx build docs; wrap such spans in<div v-pre>β see the caveats. CI'sci.ymlskips docs-only PRs viapaths-ignore: docs/**, so pure-docs changes don't spend Actions minutes, but a mixed code+docs PR runs the docs targets too.)
Deploy β Cloudflare Pages behind Cloudflare Access β
Mirrors the existing web deploy (cloudflare/wrangler-action@v3, CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID, manual workflow_dispatch).
Pages project:
dispatch-docs. Addinfra/cloudflare/docs.wrangler.tomlanalogous to the existingwrangler.toml(pages_build_output_dir = "../../docs/.vitepress/dist").Deploy step β a dedicated
.github/workflows/deploy-docs.yml, manualworkflow_dispatch(kept separate from the productdeploy.ymlso docs and app releases stay decoupled):bashbun install --frozen-lockfile bunx nx build docs wrangler pages deploy docs/.vitepress/dist --project-name=dispatch-docsCloudflare Access gate (mandatory β do not deploy ungated): create a Zero Trust self-hosted Access application covering the docs hostname:
Application domains: the production Pages hostname and the preview wildcard (
*.dispatch-docs.pages.dev) so preview deployments are gated too.Policy: Allow, include = an explicit allow-list of email addresses (not a whole-domain rule). Baseline entries:
damien@seeg.devdamien@seeg.iodamien@seeg.codamiens@gmail.comdamien@damien.in
Additional addresses added as needed. Everything else is blocked at Cloudflare's edge before the site loads.
Documented as a Zero Trust dashboard/API setup step in this work; may be codified (Terraform/API) as a follow-up, matching the repo's current minimal-infra style.
The Access gate is what makes it safe to publish internal architecture/authz docs; the local nx serve docs tool needs no gating.
Caveats / risks β
- Never deploy without the Access gate.
authz-matrix.md,auth-setup.md, anddispatch-production-design.mdare internal; ungated publication would leak them. The deploy job must not be promoted topush: masteruntil the Access application is confirmed in place. - Dead links:
ignoreDeadLinks: truehides pre-existing broken cross-links rather than fixing them β acceptable for a first cut; revisit later. - Vue-interpolation in existing docs: VitePress compiles each page as a Vue template, so any literal
{{ β¦ }}outside a code fence is evaluated and can break the build. Existing docs weren't written with this in mind. Remediation ifnx build docsfails on a page: wrap the offending block in<div v-pre>β¦</div>(or add that page tosrcExclude). Handle case-by-case during the build task; do not pre-emptively edit docs. .vitepress/insidedocs/: tooling now lives alongside content. This is standard VitePress and the tradeoff we accepted to root the project atdocs/.
Out of scope (YAGNI) β
Mermaid/graphviz diagram rendering, in-browser editing, auth beyond the Access gate, doc versioning, and a full design-system theme port. Revisit only if explicitly wanted.
Implementation outline (not the plan) β
bun add -d vitepress; scaffolddocs/.vitepress/with a minimalconfig.mts+index.md+project.json+.gitignoreentries; verifynx serve docsandnx build docswork.- Write
sidebar.ts(+sidebar.test.ts, TDD withbun test) and wirebuildSidebar()intoconfig.mtswithsrcExclude+ignoreDeadLinks+ local search; verify the sidebar lists all docs and excludes contract-qa. - Add
live-sidebar.tsand register it; verifynx serve docsHMRs edits and shows a newly-dropped.mdlive. - Add
theme/index.ts+theme/custom.css(brand accent); verify the accent renders. nx build docsβ static output; addinfra/cloudflare/docs.wrangler.tomland.github/workflows/deploy-docs.yml.- Stand up the
dispatch-docsPages project + the Cloudflare Access application (explicit email allow-list); verify the gate blocks unauthenticated access on both production and preview URLs.