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:
- CSP constraint —
script-src cdn.jsdelivr.netandworker-src cdn.jsdelivr.netbecome mandatory (Mermaid v11 uses Web Workers for its parsers). - Render flash — the diagram appears after JS execution, creating a visible delay.
- 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 defaultThe 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: in a sequenceDiagram
The postmortem-cf-bot-blocking-mcp post contained:
Note over CF: Custom Rule : host + path + IP rangemmdc fails on HTML entities inside labels:
Error: Parse error on line 8:
...Note over CF: Custom Rule : host + p
got 'TXT', expected [NEWLINE, 'end', 'COLON' ...]Fix: pre-process before generation:
sed -i 's/ / /g' index.fr.mdLesson: 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.netandscript-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