personal-presence-os

Mermaid diagrams

What it is

Any fenced code block tagged mermaid in a Markdown file is rendered as a diagram in the browser. The source is written in Mermaid's syntax — flowcharts, sequence diagrams, state diagrams, Gantt charts, and the rest. No preprocessing, no image generation, no separate asset pipeline.

Writing a diagram

Use a fenced code block with the language tag mermaid:

```mermaid
graph LR
  A[Start] --> B{Decision}
  B -->|yes| C[Act]
  B -->|no| D[Wait]
```

Renders as a flowchart in the browser. Same source, same indentation rules as a regular code block — the only difference is the mermaid language tag instead of bash, js, or similar.

How it renders — three layers in one container

The marked renderer intercepts mermaid code blocks and emits a single container with three nested layers. Each layer exists for a different consumer:

<div class="mermaid-diagram">
  <pre class="mermaid">graph LR\n  A --> B</pre>
  <noscript><pre><code class="language-mermaid">graph LR\n  A --> B</code></pre></noscript>
  <div class="mermaid-source">graph LR\n  A --> B</div>
</div>
Layer Consumer Role
<pre class="mermaid"> Browsers with JS Mermaid.js replaces its text content with an inline SVG on page load.
<noscript> Browsers with JS disabled Shows the raw source as a plain code block — the diagram source is still readable, just not visual.
<div class="mermaid-source"> LLM crawlers, screen readers, search bots Visually hidden (clip: rect(0,0,0,0)), but the raw source stays in the DOM after Mermaid replaces the <pre> with an SVG. Without this, automated consumers of the rendered page would only see opaque SVG path data.

All three layers carry the same escaped source. HTML special characters in the diagram source (<, >, &, ") are escaped before insertion — never trust diagram source as safe HTML.

How it loads — lazy, local, opt-in per page

Mermaid.js is not on the global critical path. The layout only injects the loader script when the rendered page body contains class="mermaid":

const hasMermaid = body.includes('class="mermaid"');
// ...
${hasMermaid ? mermaidScript() : ''}

The loader itself is a small inline module script that dynamically imports the full Mermaid bundle only if diagrams are actually present in the DOM:

const diagrams = document.querySelectorAll('pre.mermaid');
if (diagrams.length) {
  const m = await import('/vendor/mermaid.esm.min.js');
  m.default.initialize({ startOnLoad: false, theme: 'neutral' });
  await m.default.run({ querySelector: 'pre.mermaid' });
}

Pages without diagrams pay zero bytes for Mermaid — no script tag, no network request, no parse cost. Pages with diagrams fetch /vendor/mermaid.esm.min.js once (cached thereafter) and render every diagram on the page in a single run() call.

Why vendored, not CDN

Mermaid.js lives at engine/vendor/mermaid.esm.min.js and is served from the engine's /vendor/:filename route. Earlier iterations pulled it from jsDelivr, but the CDN turned out to be blocked from some build environments. Vendoring the bundle:

  • Removes the third-party runtime dependency.
  • Keeps builds deterministic — the version that ships is the version committed.
  • Simplifies the CSP — no need to allowlist an external CDN.

mermaid is declared as an npm dependency in package.json purely so the vendored bundle can be regenerated from a known version. Runtime never imports from node_modules.

What gets copied at build time

The build step copies engine/vendor/ into each site's dist/<domain>/vendor/ directory, alongside the site's own assets/. Every domain that prerenders content gets its own copy of mermaid.esm.min.js — there is no shared CDN origin.

In dev mode, the Hono server exposes the same files on the fly via the /vendor/:filename route, so bun run dev behaves identically to production without needing a dist/ on disk.

Theme

Mermaid is initialised with theme: 'neutral' — muted greys that sit comfortably in a prose page without fighting the surrounding type. The theme is set once, globally, in the inline loader; there is no per-page override.

Styling

Two CSS rules live in the base layout's style block:

.mermaid-diagram { margin: 1.5rem 0; }
.mermaid-diagram svg { max-width: 100%; height: auto; }

Diagrams scale down on narrow viewports instead of forcing horizontal scroll. The hidden .mermaid-source layer uses the standard visually-hidden pattern (position: absolute; clip: rect(0,0,0,0);) so it stays in the accessibility tree and DOM without affecting layout.

When to use a diagram

Mermaid is good for:

  • Flowcharts of loops, state machines, or decision trees where the shape of the relationships matters as much as the labels.
  • Sequence diagrams showing message order between a small number of actors.
  • Short dependency graphs or pipelines that would take a paragraph to describe in prose.

Mermaid is the wrong tool for:

  • Precise architecture diagrams where layout fidelity matters — use an SVG asset.
  • Anything with more than ~15 nodes — the auto-layout breaks down and the diagram becomes harder to read than the text would be.
  • Visuals that need to survive in a newsletter or social preview — Mermaid renders client-side, so cross-posted copies will show the raw source unless the target platform also runs Mermaid.

Where this is used

See the Agency log entry on negative loops for five Mermaid flowcharts embedded in prose — a good worked example of the "shape of the relationship, not the details" use case.