Records the decision to render `docs/**/*.md` as a separately-deployed static site using VitePress (Vite-based, Node-only toolchain) with `vitepress-plugin-mermaid` for the existing C4 + sequence diagrams. Site mapping: docs/index.md (new, Hero layout) → / docs/development.md → /development docs/architecture.md → /architecture docs/decisions/README.md → /decisions/ docs/decisions/00NN-…md → /decisions/00NN-… (auto sidebar) docs/setup/0N-…md → /setup/0N-… `docs/README.md` stays as the git-side / IDE-preview index and is excluded from the published site. `docs/decisions/template.md` is likewise excluded — authoring scaffold, not reader content. Audience is internal (RSSI, devs, ops). Surfacing inside portal-admin was considered and rejected — ADR-0020 §"Audience is disjoint" frames portal-admin around operational workloads, not architecture review. A decoupled site keeps the boundary clean. Toolchain alignment is the deciding factor over MkDocs Material: adding a Python runtime to Gitea Actions is a permanent maintenance tax we can avoid by reusing the Vite stack already in the workspace. Docusaurus + Astro Starlight are documented in "Pros and cons" as runner-up + revisit-later options. Implementation lands as a separate chantier (`.vitepress/config.ts`, `docs/index.md`, Gitea Actions workflow, `package.json` scripts).
15 KiB
status, date, decision-makers, tags
| status | date | decision-makers | tags | ||
|---|---|---|---|---|---|
| accepted | 2026-05-15 | R&D Lead |
|
Documentation site — VitePress + Mermaid plugin, separate static deployment
Context and Problem Statement
The project ships its documentation as Markdown under docs/ (architecture diagrams, daily-dev guide, onboarding setup, 21 ADRs and growing). Today the only way to consult those files is git clone + IDE preview, or browse them directly on Gitea. Two recurring frictions surface from this:
- Some pages in
portal-adminreference ADRs in their intro copy; without a public URL for the ADR, the anchors had to either point at Gitea's source-view (raw markdown, ergonomic but no rendering of Mermaid, no search) or — as observed in PR forfix(portal-admin): adr refs point at gitea— at the MADR template repo by mistake. - A future RSSI / auditor / new contributor handed a link to an ADR would land on raw markdown on Gitea, without inter-ADR navigation, without a search box, without rendered Mermaid sequence diagrams (which carry a non-trivial part of the architecture rationale in ADR-0009 and architecture.md).
Earlier discussion considered surfacing the docs/ content inside portal-admin as a "Documentation" section. That option was rejected: portal-admin's audience per ADR-0020 §"Audience is disjoint" is APF internal operators (CMS, audit, user directory) — not architects. Embedding dev-internal ADRs there blurs the surface and adds Markdown-rendering + BFF↔filesystem coupling for no audience win.
The need is therefore: a separately-deployed static site that renders docs/**/*.md cleanly, auto-rebuilds on push, and stays decoupled from the runtime apps.
Decision Drivers
- Toolchain alignment. The workspace is 100 % Node / pnpm / Vite / TypeScript. Adding a second runtime (Python, Go, Ruby) for the docs site is a permanent maintenance tax in CI.
- Stability bar. Per CLAUDE.md §"Project rules": stable, recognized, battle-tested choices. No pre-1.0 dependencies, no one-maintainer projects.
- Markdown-first content. All current and foreseeable content is pure Markdown. MDX / framework-component embedding is not required.
- Mermaid rendering is a hard requirement. ADR-0009 and architecture.md carry diagrams as
```mermaidfenced blocks; both must render. - Audience is internal. RSSI, future devs, ops. Not a marketing surface; theme polish is secondary, content density is primary.
- No versioning needed. ADRs are immutable once accepted; supersession is by inter-ADR reference, not by version snapshot. The site never needs to host
v1.xandv2.xside by side. - No i18n needed for the docs themselves. Per CLAUDE.md: all documentation is English-only. The site is single-locale.
- Build speed matters. A CI hook on every
docs/change should publish in seconds, not minutes — otherwise contributors will lose the "edit a sentence, see it live in 30 s" feedback loop. - Cost of failure. If the docs site is down, no app traffic is affected (it's strictly read-only reference content). Reliability bar is therefore relaxed compared to
portal-shell/portal-bff.
Considered Options
- VitePress +
vitepress-plugin-mermaid(chosen). - MkDocs Material with the Mermaid
pymdownxextension. - Docusaurus 3 (Mermaid built-in since v2).
- Astro Starlight (Mermaid via plugin).
Decision Outcome
Chosen: VitePress as the static-site generator, vitepress-plugin-mermaid for the diagram support.
Why VitePress wins this matchup.
- Zero new toolchain. Vite is already in the workspace (
@nx/vite,vite-plugin-angular, Vitest). Adding VitePress is a single dev-dep + a config file; no Python runtime to pin in CI, no Ruby gems, no separate cache layer. - Vue ergonomics stay invisible for pure-Markdown content. We never have to write a Vue component — but if we ever need a small interactive widget (an ADR-status badge, a "supersession chain" navigator), the escape valve exists and is mature.
- Build is sub-second incremental, ~2 s full for the current
docs/size — Vite's HMR carries over to the doc-site dev loop. - Battle-tested by the tools we already use: Vite, Vitest, Pinia, Vue Router all run their docs on VitePress. The project's own tech bar treats "used by the build-tool family we depend on" as a strong signal — it's the same logic that drove the Angular / NestJS / Prisma choices.
- Markdown-first config. No MDX ceremony for content that is and will remain pure Markdown.
Site structure & source layout
The site builds from docs/ (configured as VitePress's srcDir). Mapping:
docs/index.md → / (Hero layout, landing)
docs/development.md → /development
docs/architecture.md → /architecture
docs/decisions/README.md → /decisions/ (curated index — by tag)
docs/decisions/0001-…md → 0021-…md → /decisions/00NN-…
docs/setup/01-…md → 03-…md → /setup/0N-…
docs/index.md is added in this chantier with VitePress's layout: home frontmatter — Hero + features cards linking the three top-level sections (Architecture, Decisions, Onboarding). It lives alongside the existing docs/README.md, which stays as the git-side / IDE-preview index that contributors browse via Gitea's source view. The two artefacts serve different audiences and the duplication is light (the IDE index is a flat link list, the web home is a polished landing); docs/README.md is excluded from the site via srcExclude.
Excluded from the published site:
docs/README.md— git-side index, redundant with the VitePress home.docs/decisions/template.md— authoring scaffold, not reader content.
Sidebar generation:
- Top-level groups (Architecture, Decisions, Onboarding) are hand-curated in
.vitepress/config.ts— three entries, low churn. - The Decisions sub-list is auto-generated by walking
docs/decisions/00*-*.mdso adding a new ADR is a single-file change (noconfig.tsedit). Numeric filename prefix gives natural ordering.
The curated docs/decisions/README.md index (tag-grouped, supersession-aware) stays as the section's landing page — sidebar lists ADRs by number, README lists them by theme. Both are useful and don't duplicate effort.
Empty placeholder sections of the current docs/README.md (Operations & runbooks, Security/perf/a11y rationales) are NOT pre-created as empty pages. They appear in the sidebar when real content lands.
Mermaid integration
vitepress-plugin-mermaid (community plugin, maintainer emersonbottero, ~500★, 2.x release stream, used by mainstream VitePress doc sites). Two lines in .vitepress/config.ts + ```mermaid fenced blocks render. Client-side rendering — acceptable for the document sizes in scope; if diagram count grows enough to warrant build-time SVG pre-rendering, the plugin offers an opt-in for that without changing source documents.
Mermaid's theme: 'default' + plugin config follows the site's light/dark switcher — diagrams adapt to the reader's chosen theme without manual classes.
Deployment & CI
- Build:
pnpm vitepress build docsproducesdocs/.vitepress/dist/. - Hosting: served as static files behind the same Caddy reverse-proxy as
portal-shell/portal-admin, on a dedicated hostname (provisionaldocs.portal.apf.fr— the exact host follows the future infrastructure ADR's domain plan; not locked here). - CI: a new Gitea Actions workflow
docs-site.ymltriggers onpushtomainwhendocs/**orpackage.json/pnpm-lock.yamlchanges. Runspnpm vitepress build docsand rsyncs to the static host. Per ADR-0015 the YAML stays thin — orchestration inpackage.jsonscripts. - No PR-preview deployments in v1 — contributors render locally via
pnpm vitepress dev docs. Preview deploys can land later if the cycle becomes painful.
Consequences
- Good, because the broken-ADR-link class of bugs (PR #N pointing at
github.com/adr/madr) is eliminated at source: anchors become stable site URLs that survive ADR renames. - Good, because Mermaid sequence diagrams in ADR-0009 and the C4 diagrams in architecture.md become readable without dev tooling — RSSI / new contributors can absorb them from a browser.
- Good, because the toolchain stays Node-only. Single
pnpm installboots both the apps and the docs site. - Good, because the site is decoupled: a docs deploy never threatens app uptime, and an app outage never blocks doc consultation.
- Good, because new ADRs auto-surface in the sidebar with no
config.tsedit — keeps the "write ADR, push, browse it" loop frictionless. - Bad, because we add a sub-1.0 dependency to the toolchain:
vitepress-plugin-mermaidis community-maintained, not first-party. Mitigation: the plugin is a thin wrapper around Mermaid's official renderer; if it ever stalls, replacement with a directmarkdown-it-mermaidintegration is a half-day rewrite, not a regression. - Bad, because client-side Mermaid rendering produces a brief flash on diagram-heavy pages. Mitigation: in scope today (5-6 diagrams total), revisit with build-time SVG pre-render if it becomes user-visible.
- Neutral, because we ship a tiny dependency on Vue (VitePress's runtime) even though the rest of the workspace is Angular. The Vue runtime ships inside the docs site bundle only; no impact on
portal-shell/portal-adminbundles, no impact onfeature-auth/shared-*libs.
Confirmation
- The docs-site Gitea Actions workflow becomes a required check on PRs that touch
docs/**— a syntactically broken Markdown or sidebar config fails the build before merge. - The build produces
docs/.vitepress/dist/deterministically; size is monitored manually for now (no Lighthouse-style budget — site is internal, content density beats bundle size). - A smoke check in the workflow asserts that the generated HTML for ADR-0009 and architecture.md contains rendered Mermaid
<svg>(or at minimum the plugin's<div class="mermaid">host) — guards against a future Mermaid plugin upgrade silently breaking the integration.
Pros and Cons of the Options
VitePress + vitepress-plugin-mermaid (chosen)
- Good, because zero new toolchain (Vite already in the workspace).
- Good, because Markdown-first config, no MDX ceremony.
- Good, because used by the build-tool family we already trust (Vite, Vitest, Pinia, Vue Router).
- Good, because sub-second incremental dev builds via Vite HMR.
- Good, because the sidebar can mix hand-curated top-level groups with filesystem-walking sub-sections, scaling cleanly as ADRs grow.
- Bad, because Mermaid support is a community plugin (sub-1.0 dependency on the wrapper, though Mermaid itself is mature and the plugin is thin).
- Neutral, because we ship a Vue runtime in the doc site bundle. Internal site, no perceptible impact.
MkDocs Material
- Good, because the gold-standard tech-docs theme — best polish out of the box, deepest plugin ecosystem (mermaid2, swagger, social cards, instant prev/next, etc.).
- Good, because Mermaid integration is documented in the Material handbook itself, zero ambiguity on setup.
- Good, because i18n + versioning are first-class if we ever need them.
- Bad, because adds Python runtime to CI — pinning version + installing the right
mkdocs-material+ extension stack on every PR. Permanent friction in an otherwise Node-only workspace. - Bad, because the build is slower than VitePress on incremental changes (rebuilds the whole site on most edits).
- Neutral, because YAML config is fine but is a third config syntax to maintain alongside TypeScript + JSON.
Docusaurus 3
- Good, because Mermaid is built-in since v2 (
docusaurus.config.js: one boolean flag). - Good, because versioning + i18n + blog + MDX + search are all first-class.
- Bad, because the full feature set is overkill for the scope. Internal ADR site = pure Markdown, single-locale, no versioning, no blog — paying complexity tax for unused capabilities.
- Bad, because MDX-by-default makes simple Markdown edits subtly fragile (a stray
<in prose becomes an error). Can be disabled, but at that point we're paying Docusaurus's footprint for VitePress's feature set. - Bad, because the bundle/runtime ships React, adding a stack the workspace doesn't otherwise use. Unlike Vue-via-VitePress (a sub-dep that only fires during the docs build), React-via-Docusaurus is the docs surface itself — every site visit loads it.
Astro Starlight
- Good, because Markdown-first like VitePress, plus first-class MDX if we ever want it.
- Good, because Astro's "islands" model produces fast, low-JS pages — better than client-rendered VitePress on pure-content sites.
- Good, because the theme is sober and doc-centric.
- Bad, because young: Astro 1.0 in 2022, Starlight 1.0 in 2024. The plugin ecosystem is smaller than VitePress/Docusaurus/MkDocs; for less-mainstream integrations we'd be more likely to hit "write your own".
- Bad, because Astro is a fourth framework in the workspace (Angular + React via Storybook later + Vue via VitePress + Astro). The tech-bar's "stable, recognized" lens flags this as worth deferring 12–18 months.
- Neutral, on Mermaid: plugin exists, comparable maturity to VitePress's.
More Information
- Implementation lands in a single chantier directly after this ADR:
docs/.vitepress/config.ts,docs/index.md(Hero layout),.gitea/workflows/docs-site.yml,package.jsonscripts (docs:dev,docs:build). - The host (
docs.portal.apf.fror a sub-path) is provisional — the production hosting decision (Caddy config, TLS termination, IP-allowlist gating) follows the future infrastructure ADR. The doc site is internal-only by default; opening it to the public web is a separate decision that can be made later by flipping one Caddy rule. - Related ADRs:
- ADR-0001 — choice of MADR + folder layout. This ADR builds on it by giving the rendered surface.
- ADR-0015 — Gitea Actions as the CI platform; the docs-site workflow follows the same "thin YAML over portable scripts" convention.
- ADR-0018 — for the hostname / per-environment config when the future infra ADR lands.
- Revisit triggers:
- If
docs/content stops being pure Markdown (gain interactive widgets, embedded diagrams beyond Mermaid, …), reconsider whether VitePress's escape valves are enough or whether Astro Starlight's MDX-first model is a better fit. - If Vue is genuinely a "fourth framework" maintenance cost (we hit a CVE chain that ripples through the doc site), the migration target is most likely MkDocs Material — at which point the Python cost is paid once, accepted as the price of removing Vue.
- If