--- status: accepted date: 2026-05-15 decision-makers: R&D Lead tags: [process, infrastructure] --- # 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-admin` reference 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 for `fix(portal-admin): adr refs point at gitea`](../../apps/portal-admin/src/app/pages/audit/audit.html) — 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](0009-auth-flow-oidc-pkce-msal-node.md) and [architecture.md](../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](0020-portal-admin-app.md) §"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](../../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](0009-auth-flow-oidc-pkce-msal-node.md) and [architecture.md](../architecture.md) carry diagrams as ` ```mermaid ` fenced 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.x` and `v2.x` side by side. - **No i18n needed for the docs themselves.** Per [CLAUDE.md](../../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 `pymdownx` extension. - **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*-*.md` so adding a new ADR is a single-file change (no `config.ts` edit). 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](../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 docs` produces `docs/.vitepress/dist/`. - Hosting: served as static files behind the same Caddy reverse-proxy as `portal-shell` / `portal-admin`, on a dedicated hostname (provisional `docs.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.yml` triggers on `push` to `main` when `docs/**` or `package.json` / `pnpm-lock.yaml` changes. Runs `pnpm vitepress build docs` and rsyncs to the static host. Per [ADR-0015](0015-cicd-gitea-actions.md) the YAML stays thin — orchestration in `package.json` scripts. - 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](0009-auth-flow-oidc-pkce-msal-node.md) and the C4 diagrams in [architecture.md](../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 install` boots 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.ts` edit — keeps the "write ADR, push, browse it" loop frictionless. - Bad, because we add a sub-1.0 dependency to the toolchain: `vitepress-plugin-mermaid` is community-maintained, not first-party. Mitigation: the plugin is a thin wrapper around Mermaid's official renderer; if it ever stalls, replacement with a direct `markdown-it-mermaid` integration 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-admin` bundles, no impact on `feature-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](0009-auth-flow-oidc-pkce-msal-node.md) and [architecture.md](../architecture.md) contains rendered Mermaid `` (or at minimum the plugin's `
` 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.json` scripts (`docs:dev`, `docs:build`). - The host (`docs.portal.apf.fr` or 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](0001-use-adrs-to-record-architectural-decisions.md) — choice of MADR + folder layout. This ADR builds on it by giving the rendered surface. - [ADR-0015](0015-cicd-gitea-actions.md) — Gitea Actions as the CI platform; the docs-site workflow follows the same "thin YAML over portable scripts" convention. - [ADR-0018](0018-environment-configuration-strategy.md) — 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.