---
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 `<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.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.
