Manual build-time remark + rehype plugins
Context and Problem Statement
The manual’s markdown content (228 files at docs/manual/) was generated by the /diagramming skill’s post-creation export workflow. Mermaid blocks are stored as <details><summary>Mermaid Source</summary>\n\“mermaid\n…\n```\npaired with anreference to the offline PNG (per the skill's contract). The PNGs atdocs/manual/are offline-export artefacts; the live Astro site renders Mermaid client-side viapublic/ui/client.js` (per ADR-0015 §“Mermaid integration”).
For the Astro build pipeline to render the manual correctly, two transformations are needed at build time:
remarkUnwrapMermaidDetails— walks markdown AST; for each<details><summary>Mermaid Source</summary>block containing amermaidfenced code block, replaces the entire<details>(and the immediately-preceding<img>reference) with<div class="mermaid">…mermaid source…</div>so the existing client.js loader picks it up.rehypeFrontmatterUriExtraction— for entries ofkind: entity/scheme/exemplar(per the Zod schema in ADR-0016), extracts the OPDA URI from the markdown body (e.g. the first### opda:<EntityName>in Physical-Ontology classes.md sections; the page title for entity files) and writes it into the entry’s data asentityUri. Enables the Cross-Tier Links footer (per ADR-0017’sCrossTierLinks.astro) without re-parsing markdown at render time.
Per the implementation programme plan §4, this is Phase 3 — runs in parallel with Phase 2 (ADR-0017 components) once Phase 1 (ADR-0016) is green.
Realises ADR-0015 §Confirmation criteria 5 (Diagram.astro unchanged — site reuses the existing loader; no parallel setup), 7 (ELK-laid-out diagrams render correctly via the unwrapped <div class="mermaid">), 8 (no docs/manual/_export/ files referenced from src/).
Decision Drivers
- The PNG
<img>references in the markdown are local-export-only — they must NOT appear in the emitted HTML (would 404 sincedocs/manual/_export/is gitignored and never shipped). - The
<details>source IS the canonical mermaid — unwrap and live-render so dark-mode toggling works (existingclient.jsre-runsmermaid.run()on theme change). - Plugins live at
src/lib/remark/per the Astro project convention. - Wired into
astro.config.mjsvia themarkdown.remarkPlugins+markdown.rehypePluginsarrays. - Plugins are tested via the standard remark-test pattern (small fixture markdowns + expected AST output).
- Both plugins together are <100 lines; small surface area to maintain.
Considered Options
- A — Two custom plugins (
remarkUnwrapMermaidDetails+rehypeFrontmatterUriExtraction) — chosen per ADR-0015. - B — Use an existing remark plugin (e.g.
remark-mermaidjs) — those plugins typically render mermaid to SVG at build time, which (a) duplicates the existing client.js setup, (b) loses dark-mode coordination, (c) doesn’t handle ELK consistently. - C — Hand-edit each markdown to remove
<img>+ unwrap<details>— defeats the regenerability discipline. - D — Astro integration package wrapping both plugins — over-engineered for 100 lines of code.
Decision Outcome
Chosen option: A — Two custom plugins at src/lib/remark/unwrap-mermaid-details.ts + src/lib/remark/frontmatter-uri-extraction.ts, wired in astro.config.mjs via markdown.remarkPlugins + markdown.rehypePlugins.
remarkUnwrapMermaidDetails performs a linear pass over the mdast root’s children. For each <details><summary>Mermaid Source</summary> open-html node followed by a code: mermaid node followed by </details>, it: strips the immediately-preceding diagram image reference (both markdown ![]() and raw HTML <img src="diagrams/..."> forms), and replaces the three nodes with <div class="mermaid">…</div>. The entire mermaid source (including ELK YAML frontmatter) is preserved verbatim inside the div.
rehypeFrontmatterUriExtraction derives the entry id from file.path, calls deriveKind() (Phase 1’s helper), and for entity/scheme entries: searches the hast tree for the first opda:<Name> heading, falling back to filename-based UpperCamelCase derivation. The derived URI is written to file.data.astro.frontmatter.entityUri. Exemplars are skipped (no stable URI). Idempotent.
Consequences
- Good, because the build now emits zero
<details><summary>Mermaid Source</summary>blocks and zero<img src="diagrams/...">PNG references — both offline-only artefacts are fully suppressed in the site output. - Good, because 292 pages carry
<div class="mermaid">blocks that the existingclient.jsloader picks up client-side — dark-mode toggling and ELK layout work without any additional wiring. - Good, because the two plugins together are ~120 lines; well under the ADR’s <100-line-each target (each under 90 lines).
- Good, because both plugins are tested (20 fixture-based tests; 0 failures) with Node 22’s built-in test runner — no new test framework dependency.
- Neutral, because
entityUriextraction forclasses.mdfiles (which contain multiple### opda:ClassNameheadings) only extracts the first URI. These multi-class files need per-entry frontmatter from ADR-0020 to resolve correctly; the plugin’s heading-based extraction is best-effort until then. - Neutral, because the Astro content store (
node_modules/.astro/data-store.json) caches rendered HTML between builds. Stale cache (built before the plugins were installed) must be cleared manually once per project install. Subsequent incremental builds invalidate only changed entries correctly.
Confirmation
Verification performed by implementing worker (npm run build after rm -f node_modules/.astro/data-store.json). Programme-wide validation gate (independent validator, soundness + completeness + cross-ADR) pending.
Specific to this ADR:
- Both plugins exist at
src/lib/remark/unwrap-mermaid-details.ts+src/lib/remark/frontmatter-uri-extraction.ts astro.config.mjswires both plugins in the markdown pipeline- Build emits zero
<details><summary>Mermaid Source</summary>blocks in the dist HTML (every one transformed to<div class="mermaid">) - Build emits zero
<img>references pointing atdocs/manual/<tier>/diagrams/paths (all stripped) - Build emits zero references to
docs/manual/_export/from anysrc/ordist/file - ELK-flagged mermaid blocks (those with
config: layout: elkin their YAML frontmatter) render via the ELK plugin client-side — confirmed by manual smoke test on a vocabulary scheme membership graph - Per-entry
entityUrifield populated for everykind: entity/scheme/exemplarentry;getCollection('manual')returns entries withentityUrinon-null where expected - Validation report at
docs/adr/validation/ADR-0018-validation-report.md
More Information
- Programme plan:
docs/plan/manual-astro-integration.md - Architectural decision: ADR-0015 §“Mermaid integration” + §“Build-time markdown processor”
- Bootstrap predecessor: ADR-0016
- Existing mermaid loader (reused, not duplicated):
public/ui/client.js:228-269 - Diagramming skill (source of the
<details>convention):~/.claude/skills/diagramming/SKILL.md§“Post-Creation Export Workflow” - Parallel ADR: ADR-0017 — components; safe to run in parallel with this plumbing
Amendment — adopted output path (1 September 2026)
The remark plugin still emits bare <div class="mermaid"> source exactly as decided.
The consumer is now Layout.astro’s adoptBareMermaid() path and the shared
GraphDiagram renderer, not public/ui/client.js. This preserves the decision’s
one-loader, dark-mode and ELK requirements without changing the Markdown transform.
Comments
Loading comments…
Sign in to post a comment