ADR-0022 — Diagrams show their Archify view on the page and their Mermaid in Markdown

Every diagram is one component holding a Mermaid fence: pages show its Archify view, and every Markdown surface carries the Mermaid.

Context

ADR-0011 put each Archify diagram on its page in a sandboxed frame, under its Mermaid version. The owner asked (2026-11-01):

  • Inside the frame, only the diagram, its labels and its controls. Today the frame also shows Archify's title and subtitle, its notes and index rail and its theme and style buttons, and because all of that is taller than the frame, the frame scrolls.
  • On the page, only the Archify view; in Markdown, only the Mermaid. The copy-Markdown action, the Markdown routes and llms-full.txt give Mermaid, never the Archify tag. Enforced for every package and project.
  • The diagram's background matches the site's theme, while boxes and arrows stay vibrant and meaningful, with live animation.

The owner then chose, from options: wireframes get Archify versions too, drawn from their Mermaid, one per screen; enforcement is strict, so every Mermaid diagram outside the templates has an Archify view, and the four that lack one today get it in this change; and the implementation is checked in the owner's Chrome through the Claude in Chrome extension, on localhost only.

Facts this decision rests on, read from Archify 3.2.0's output and the installed packages:

  • What an Archify page holds. A toolbar (theme, style, Live/Still, Present, Export); a header (the title and subtitle); the diagram, with its own controls (Route, Map, Lens, find, guide, zoom); and a notes rail (cards, a node index, "Notes & index").
  • ?embed=1 hides the toolbar, header, cards and diagram controls, and turns motion and exploration off. Too bare for what the owner asked.
  • ?theme=light or ?theme=dark sets the theme before first paint.
  • Colours are CSS variables. The page's: --bg, --grid, --canvas-dot, --panel, --panel-border, --text and its muted, dim and faint variants. The nodes' come from their kind: --frontend-stroke cyan, --backend-stroke green, --database-stroke violet, --cloud-stroke amber, --security-stroke rose, --messagebus-stroke orange, --external-stroke slate, each with a light-theme value.
  • Motion starts Still when no preference is saved. Inside the sandbox, storage throws, so every embed starts Still today.
  • Size. The diagram's svg carries a viewBox and data-reader-fit: width-first (it fills the width) or intrinsic-height (a wide diagram, such as the 1277 × 236 version lifecycle). Archify has no wheel handler; the scrolling comes from its page being taller than the 420 or 560px frame.
  • Markdown. fumadocs-mdx's includeProcessedMarkdown takes a filterElement that can keep an element's children and drop the element ('children-only', fumadocs-core remark-llms). The copy-Markdown action, /<page>.md, Markdown negotiation and llms-full.txt all read that processed Markdown (docsLlms in src/lib/source.ts). Search records exclude code blocks, so a Mermaid fence never reaches them.
  • Wireframes. Archify has no wireframe type. Its architecture type places components by pos and size and wraps them in labelled region boundaries, enough to draw a screen as boxes inside a phone frame.
  • Counts. 37 wireframes in 8 app flows, 4 Mermaid diagrams without an Archify view (maintain/index.mdx, architecture/system.mdx, dfd/system.mdx, sequence/build-and-search-sync.mdx), and 9 diagrams with one. An Archify file is about 820 KB, most of it the viewer built into every file.

Decision

  1. One shape for every diagram. A diagram in content/ is written as a <Diagram> holding exactly one Mermaid fence and nothing else:

    <Diagram src="/diagrams/example-cli/request-flow.html" title="Request flow">
    
    ```mermaid
    flowchart LR
      Parser --> Router --> Handler
    ```
    
    </Diagram>

    src follows ADR-0011's rule (/diagrams/<project-slug>/<slug>.html); title is required. <Diagram> is registered in src/components/mdx.tsx; <HtmlEmbed> is no longer available to pages and lives on inside <Diagram>.

  2. The page shows only the Archify view. <Diagram> renders the frame; the Mermaid is not rendered on the page. Under the frame stay the title as its caption and Open full screen.

  3. Markdown shows only the Mermaid. The processed Markdown keeps <Diagram>'s children and drops the tag (filterElement, 'children-only'), so every Markdown surface (the copy-Markdown action, /<page>.md, Markdown negotiation, llms-full.txt) carries the Mermaid fence and never an Archify URL.

  4. Every Mermaid diagram is a <Diagram>, wireframes included. The build fails on a Mermaid fence outside a <Diagram>, anywhere in content/ except content/docs/maintain/_templates/ (templates show the shape; a copy must be wrapped before it builds). A wireframe's Archify version is an architecture diagram drawn from its Mermaid: one per screen, its boxes placed as the Mermaid rows, inside a labelled phone-screen region, at phone width. The four diagrams without one get theirs in this change.

  5. Where the files live. Both files sit in diagrams/<project-slug>/: the candidate, <slug>.json, and Archify's HTML, <slug>.html, byte for byte as deliver wrote it, so check --require-provenance can run on the committed file. public/diagrams/ goes. A route, src/app/diagrams/[project]/[file]/route.ts, prerenders each at /diagrams/<project-slug>/<slug>.html, with the site shell of decision 6, served with ADR-0011's CSP sandbox header. The build fails on: an HTML without its JSON; a src with no file; a diagram file no page uses; an Archify receipt (*.delivery*.json) in diagrams/ or public/.

  6. The site shell, src/lib/diagram-shell.ts, is added to every diagram as it is served, so a new package's diagrams need nothing of their own:

    • In a frame (?frame=1, set by <Diagram>): Archify's title and subtitle, notes rail and its reveal button, theme and style buttons and the guide's credit line are hidden. The diagram, its labels, Live/Still, Present, Export and the diagram controls stay. A direct visit, through Open full screen, keeps Archify's title and notes.
    • Colours. The page variables take the site's tokens for each theme (--home-bg, --home-panel, --home-line, --home-ink, --home-muted, --home-faint), so the diagram sits on the page's own ground. Nodes and arrows keep Archify's kind palette, vibrant and meaningful. Diagrams are delivered with Archify's classic style (no visual_preset), and the existing ones are delivered again that way.
    • Theme. <Diagram> passes ?theme= from the site's theme, and changes it when the reader switches theme (the frame reloads from the browser cache).
    • Motion starts Live: the shell turns Archify's motion on once after load, unless the reader prefers reduced motion. The reader can switch to Still.
    • No inner scrolling. The frame's height is set on the server from the diagram's viewBox, plus room for the controls, as a CSS aspect ratio, so the page doesn't jump. The shell then reports the exact content height to the page with postMessage. The page takes it only from that frame (event.source), and only as a number within bounds. Page scrolling passes over the frame. A wide diagram (intrinsic-height) scrolls sideways inside the frame on phones, Archify's own containment.
  7. The same for every repository. AGENTS.md §8, PROJECT_AGENTS.md §4 and the templates state the shape. The build checks run on every folder in content/.

Options considered

ConcernOptionProsConsVerdict
Trimming ArchifyA — a shell added as the file is servedOne place for every diagram; the committed file stays Archify's ownDepends on Archify's class names; re-checked on each Archify upgradeChosen
Trimming ArchifyB — Archify's ?embed=1No site codeNo controls and no motion: not what the owner askedRejected
Trimming ArchifyC — edit each delivered file in the repositoryNo routeBreaks check --require-provenance on the committed file; repeated per fileRejected
Mermaid on the pageA — not shownWhat the owner askedThe text version is one action away (Markdown), not on the pageChosen (owner)
Mermaid on the pageB — a closed "Text version" disclosureText on the page for those who want itNot what the owner askedRejected
MarkdownA — filterElement, 'children-only'Every Markdown surface at once, at buildTies to fumadocs-mdx's optionChosen
MarkdownB — rewrite the text in renderPageNo build optionA regular expression over MDXRejected
Frame heightA — from the viewBox on the server, refined by the shell's reportNo jump; no inner scrollA postMessage channel to validateChosen
Frame heightB — fixed, as todaySimpleScrolls or leaves empty spaceRejected
WireframesA — one Archify file per screenEach screen's picture where its Mermaid is37 files, about 30 MB; a page with 7 screens loads about 5.6 MB of frames as it scrollsChosen (owner)
WireframesB — one per app flow8 files, about 6.5 MBScreens apart from their statesRejected (owner)
WireframesC — stay as codeNo new filesCode on the pageRejected (owner)
EnforcementA — strict: every Mermaid diagram outside the templatesNo page can skip itThe 4 diagrams without a view are drawn nowChosen (owner)
EnforcementB — only the <Diagram> shapeLess work nowBare Mermaid keeps showing as codeRejected (owner)
ColoursA — site ground, Archify's kind palette, classic styleMatches the site; colour carries meaningEvery existing diagram is delivered againChosen
ColoursB — keep Archify's backgroundsNo shell coloursDoesn't match the siteRejected

Consequences

Positive

  • A reader sees the diagram and its controls, at its own height, in the site's colours, moving.
  • Tools and LLMs get Mermaid text, never an iframe URL.
  • A consuming repository writes one <Diagram>; the rest is the site's.

Negative / accepted trade-offs

  • About 41 new Archify files, roughly 34 MB more in the repository and the deployed output.
  • A page with many screens loads several frames as the reader scrolls.
  • Each diagram exists twice, Mermaid and Archify; both change in the same change.
  • The shell depends on Archify's markup. Its tests run against a committed Archify file, so an upgrade that renames what the shell hides fails the tests.
  • The Mermaid text is not on the page. Archify's diagram is an accessible svg (role="img", title and description), and the frame has a title.

Content invariants

  • 1 (Private stays private). Unchanged: a diagram of a private project never goes into diagrams/.
  • 5 (Readable as Markdown). Strengthened: every diagram's Markdown is its Mermaid.

Security considerations

  • Isolation unchanged. The shell's script runs inside the same sandboxed, opaque-origin frame (no allow-same-origin), and direct visits keep the CSP sandbox header.
  • postMessage. The page accepts a message only when event.source is that frame's window and the data is { type: 'archify-height', height } with a finite height between 120 and 4000; anything else is ignored. The frame sends nothing but its height, and a Present request ({ type: 'archify-present' }, no data) when it cannot go fullscreen; the page answers it by opening that diagram's own URL, which it builds itself (implementation notes).
  • The route serves only files matching <project-slug>/<slug>.html from diagrams/, listed at build (generateStaticParams, no other paths). No request data is read.
  • Nothing new leaves the system. The shell adds no network call; the checks of ADR-0011 (no external resources, no network APIs, no local paths) run on each committed file.

Implementation notes

  • src/components/diagram.tsx (<Diagram>, server: checks the src and title, reads the size) with its client part diagram-frame.tsx for the theme and the height. <HtmlEmbed> was folded into the frame rather than kept as a separate component; it is removed from src/components/mdx.tsx and from the repository, and a page that still uses it fails the build with the way forward.
  • src/lib/source.ts: includeProcessedMarkdown with filterElement keeping <Diagram>'s children (markdownFilter in src/lib/remark-diagrams.ts).
  • The build check of decision 4 (remarkDiagrams, first among the remark plugins through applyMdxPreset) and the file checks of decision 5 (checkDiagrams, run by the loader plugin; a JSON without its HTML fails too).
  • src/app/diagrams/[project]/[file]/route.ts (with src/lib/diagram-files.ts) and src/lib/diagram-shell.ts. A diagram is wide, for the frame's minimum width, at Archify's own ratio (1.55); the frame's first height is the viewBox ratio plus 10rem for the controls, then the reported height.
  • Every diagram moved to diagrams/docs/ and delivered again in the classic style; Archify views made for the 4 diagrams and the 37 wireframes; every page converted to <Diagram>; the templates updated. Wireframe text is shortened with "…" where Archify would draw it below about 11px (it draws a label on one line); the full text is in the Mermaid. 49 diagrams in all, about 40 MB of HTML. Every file passes check --require-provenance, holds no local path and calls no network API.
  • Tests: the shell (what it hides, against a committed Archify file; the colours against global.css; Live start and reduced motion; the height report, an open menu, a height that answers itself, the toolbar's stacking, Present to fullscreen and its fallback), the height bounds and the Present request, the <Diagram> shape check (no fence, two fences, other content, a bare fence outside one, a bare fence nested in another element, templates exempt, <HtmlEmbed>), the Markdown output (the fence kept, the tag gone), <Diagram>'s frame, and the route's paths.
  • Checked in Chrome on localhost (the built site, desktop and phone width, both themes). Three faults found there and fixed in the shell and the frame:
    • Flicker. Archify lifts its bottom dock and reserves room for it from the viewport height, which in a frame is the frame: the frame answered its own height, about 16 times a second. In a frame the dock holds still and has fixed room, and past 8 changes in 2 seconds the shell reports only growth until the frame's width changes.
    • Export menu under the diagram. The frame's toolbar kept its stacking (z-index), spans the frame so the menu opens inside it on phones, and the menu takes its own height; the shell reports the open menu's bottom, so the frame grows to show it.
    • Present. Archify's Present stage is the viewport's height, the frame's own. The frame now has allow="fullscreen", and the shell takes the stage to fullscreen; leaving fullscreen leaves the stage. Where fullscreen is refused, the frame asks the page (archify-present), which opens the diagram's own page with ?present=1.
    • Theme. A lazy frame did not always follow its changed src after hydration, and stayed dark on a light page; the frame is now a new element per theme. Real fullscreen could not be seen: the automated Chrome refuses fullscreen to any page ("not granted", even at the top level). The fallback was seen working; the fullscreen path is covered by tests.
  • bun run test, bun run types:check and bunx next build pass.

Docs to update on implementation

On this page

13 min read