Contents

Hugo: freezing Mermaid diagrams to static dark/light SVGs

Problem

Mermaid diagrams on arleo.eu were rendering client-side via cdn.jsdelivr.net. Three concrete consequences:

  1. CSP constraintscript-src cdn.jsdelivr.net and worker-src cdn.jsdelivr.net become mandatory (Mermaid v11 uses Web Workers for its parsers).
  2. Render flash — the diagram appears after JS execution, creating a visible delay.
  3. Dark theme ignored — Mermaid initialized the SVG in light mode even when the site theme was dark.

The solution: generate SVGs at build time with mmdc, outside any browser context.


Architecture: dark/light pairs

mmdc (@mermaid-js/mermaid-cli v11.15.0) accepts a --theme flag. We systematically generate two SVGs per diagram:

mmdc -i source.mmd -o diagram-{hash}-dark.svg  --theme dark
mmdc -i source.mmd -o diagram-{hash}-light.svg --theme default

The Hugo mermaid-svg shortcode selects the right file based on the theme attribute on <html>:

{{- $dark  := printf "diagram-%s-dark.svg"  .Get "hash" -}}
{{- $light := printf "diagram-%s-light.svg" .Get "hash" -}}
<img class="mermaid-svg mermaid-dark"  src="{{ $dark  | relURL }}" loading="lazy" />
<img class="mermaid-svg mermaid-light" src="{{ $light | relURL }}" loading="lazy" />

CSS in _custom.scss:

html[theme=dark]  .mermaid-light { display: none; }
html[theme=light] .mermaid-dark  { display: none; }
html:not([theme]) .mermaid-dark  { display: none; }

Implementation in hugo-mcp

The _freeze_mermaid_blocks() function in main.py is called automatically on every create_page or update_page.

Detection: regex on ```mermaid blocks.

Hash: SHA256[:8] of the raw source. Same source = same hash across FR and EN of the same post — SVGs are shared, not duplicated.

Preservation marker: the block is replaced by:

<!-- mermaid-source:BASE64_SOURCE -->
{{< mermaid-svg hash="3007aa7f" >}}

The mermaid-source comment encodes the source in base64. On a subsequent update, if the computed hash matches the existing hash, SVGs are not regenerated. If the source changed, old SVGs are replaced.

Orphan cleanup: before deleting an SVG that became unused, the code scans all .md files in the bundle (not just the file being processed). This prevents deleting an SVG still referenced by the sibling language version when diagrams are identical across FR and EN.


Bug: &nbsp; in a sequenceDiagram

The postmortem-cf-bot-blocking-mcp post contained:

Note over CF: Custom Rule&nbsp;: host + path + IP range

mmdc fails on HTML entities inside labels:

Error: Parse error on line 8:
...Note over CF: Custom Rule&nbsp;: host + p
got 'TXT', expected [NEWLINE, 'end', 'COLON' ...]

Fix: pre-process before generation:

sed -i 's/&nbsp;/ /g' index.fr.md

Lesson: always use plain Unicode spaces in Mermaid sources, never HTML entities.


Results

  • 12 posts with Mermaid diagrams, FR + EN versions
  • 44 SVG pairs generated (dark + light)
  • Some FR/EN posts share the same SVG files (identical hash)
  • Removed worker-src cdn.jsdelivr.net and script-src cdn.jsdelivr.net (Mermaid) from CSP
  • Immediate render, no flash, theme respected from first paint
  • Puppeteer verification: 0 image 404, 0 CSP violation, dark theme correctly applied
Diagram Diagram