Files
apf_portal/docs/decisions/0022-docs-site-vitepress.md
julien aa8ad97feb
CI / check (push) Successful in 1m47s
CI / commits (push) Has been skipped
CI / scan (push) Successful in 1m48s
CI / a11y (push) Successful in 2m1s
CI / perf (push) Successful in 3m33s
fix(portal-admin): adr refs point at gitea, not the madr template repo (#153)
## Summary

Records the decision to render `docs/**/*.md` as a separately-deployed static site using **VitePress + `vitepress-plugin-mermaid`**.

This is **ADR-only** — the implementation (install + `.vitepress/config.ts` + `docs/index.md` + Gitea Actions workflow) lands as the next chantier. Splitting them keeps the decision review focused on the *why* before the *how*.

## What lands

### [`docs/decisions/0022-docs-site-vitepress.md`](docs/decisions/0022-docs-site-vitepress.md)

Full MADR 4.0.0 ADR. Decision drivers, 4 considered options (VitePress, MkDocs Material, Docusaurus 3, Astro Starlight), site-structure mapping, Mermaid integration, deployment + CI plan, consequences, revisit triggers. Tags: `process`, `infrastructure`.

**Key choices captured:**

- VitePress wins on toolchain alignment (Vite already in the workspace via `@nx/vite`, `vite-plugin-angular`, Vitest). MkDocs Material was the strong runner-up; the Python runtime tax in Gitea Actions tipped the balance.
- `vitepress-plugin-mermaid` for ```` ```mermaid ```` blocks ([ADR-0009](docs/decisions/0009-auth-flow-oidc-pkce-msal-node.md) sequence + [architecture.md](docs/architecture.md) C4 are hard requirements).
- Site sources entirely from `docs/`. Mapping:
  - `docs/index.md` (new, Hero layout) → `/`
  - `docs/development.md` → `/development`
  - `docs/architecture.md` → `/architecture`
  - `docs/decisions/README.md` → `/decisions/` (curated index, kept as section landing)
  - `docs/decisions/00NN-….md` → auto-listed in the sidebar by numeric prefix
  - `docs/setup/0N-….md` → `/setup/0N-…`
- **Excluded** via `srcExclude`: `docs/README.md` (stays as the git-side / IDE-preview index — option A from the prior discussion) and `docs/decisions/template.md` (authoring scaffold).
- Empty placeholder sections in `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.
- Deployment: dedicated hostname (provisional `docs.portal.apf.fr`) behind the same Caddy reverse-proxy as the apps, fed by a new `.gitea/workflows/docs-site.yml` triggered on `docs/**` push. Exact hostname follows the future infrastructure ADR; not locked here.

### [`docs/decisions/README.md`](docs/decisions/README.md)

ADR-0022 added to the index table.

### [`CLAUDE.md`](CLAUDE.md)

Bumps the ADR range from `0001 → 0021` to `0001 → 0022`. New bullet in the "Architecture (recorded in ADRs)" section describing the docs-site choice in one paragraph. Implementation tracked in "Still on the roadmap" until the next PR lands it.

## Notes for the reviewer

- **Why ADR before implementation?** The choice between VitePress / MkDocs Material / Docusaurus / Astro Starlight is exactly the "stable + recognized + innovative" trade-off [CLAUDE.md](CLAUDE.md) asks to document. Reviewing the rationale on its own (without dragging through the install diff) keeps the discussion focused.
- **Why not surface ADRs inside `portal-admin`?** Audience mismatch — [ADR-0020](docs/decisions/0020-portal-admin-app.md) §"Audience is disjoint" frames `portal-admin` around APF internal operators (CMS, audit, user directory), not architects. The full reasoning is in ADR-0022 §"Context and Problem Statement".
- **Why two index artefacts (`docs/README.md` + future `docs/index.md`)?** Option A from the structure discussion. Each serves a distinct audience: `README.md` is the flat link list that renders well in IDEs / Gitea source view; `index.md` will be the VitePress Hero landing for the web audience. Light duplication, no maintenance pressure (the IDE one only needs updating when sections appear/disappear).
- **Why `vitepress-plugin-mermaid` rather than Docusaurus's built-in Mermaid?** The community plugin is a sub-1.0 dependency on the wrapper (Mermaid itself is mature), but Mermaid is so mainstream that switching it out is a half-day rewrite if the plugin stalls. Trading that risk against Docusaurus's MDX-by-default footprint + React runtime is a net win.
- **Why `process` + `infrastructure` tags?** Mirrors [ADR-0015](docs/decisions/0015-cicd-gitea-actions.md) (also a CI / deploy decision with content authoring implications) and is consistent with the [tag vocabulary](docs/decisions/README.md#tag-vocabulary). No new tag invented.

## Test plan

- [x] `docs/decisions/0022-docs-site-vitepress.md` validates as MADR 4.0.0 (frontmatter, section order). Index in [`docs/decisions/README.md`](docs/decisions/README.md) updated in the same PR per [ADR conventions](docs/decisions/README.md#conventions).
- [x] No code touched — `lint / test / build` matrix unaffected.
- [ ] Review for trade-off accuracy: did I get MkDocs Material's strengths right? Is Astro Starlight's maturity argument fair?
- [ ] Implementation chantier (next PR): `pnpm add -D vitepress vitepress-plugin-mermaid mermaid`, `docs/.vitepress/config.ts`, `docs/index.md`, `.gitea/workflows/docs-site.yml`, `package.json` scripts. Will land within the same week assuming this ADR holds.

## What's next

If accepted as-is, the immediate follow-up is:

1. Install VitePress + the Mermaid plugin.
2. Author `docs/.vitepress/config.ts` with the sidebar shape spelled out in this ADR (auto-generated sub-sidebar for `/decisions`, hand-curated top-level).
3. Author `docs/index.md` (Hero layout).
4. Add the `docs-site` Gitea Actions workflow.
5. Wire the dev script (`pnpm docs:dev`) into `package.json` so contributors can preview locally.

If reviewers want to push back on the toolchain choice (MkDocs Material in particular has a strong case for the theme polish), this is the right PR to surface that — implementation hasn't started.

---------

Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #153
2026-05-15 18:48:07 +02:00

160 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 1218 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.