---
title: "Hugo: freezing Mermaid diagrams to static dark/light SVGs"
description: "How and why to replace client-side Mermaid rendering with static SVGs generated at build time: dark/light pairs via mmdc, content-based hashing, multilingual deduplication, and the \u0026nbsp; trap in sequenceDiagrams."
url: "https://www.arleo.eu/en/posts/debug-mermaid-svg-freeze/"
language: "en"
datePublished: "2026-05-29T20:00:00+02:00"
dateModified: "2026-05-29T20:00:00+02:00"
tags: ["hugo","mermaid","svg","build","csp","infrastructure"]
categories: ["infrastructure","hardening"]
contentSignal: "ai-train=no, search=yes, ai-input=yes"
---

# Hugo: freezing Mermaid diagrams to static dark/light SVGs
How and why to replace client-side Mermaid rendering with static SVGs generated at build time: dark/light pairs via mmdc, content-based hashing, multilingual deduplication, and the &nbsp; trap in sequenceDiagrams.



## Problem

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

1. **CSP constraint** — `script-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:

```bash
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>`:

```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`:

```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:

```markdown
<!-- 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:

```bash
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

<!-- mermaid-source:Zmxvd2NoYXJ0IFRECiAgICBBWyJjcmVhdGVfcGFnZSAvIHVwZGF0ZV9wYWdlIl0gLS0+IEJbIl9mcmVlemVfbWVybWFpZF9ibG9ja3MoKSJdCiAgICBCIC0tPiBDeyJNZXJtYWlkIGJsb2NrXG5kZXRlY3RlZD8ifQogICAgQyAtLSBubyAtLT4gRFsiQ29udGVudCB1bmNoYW5nZWQiXQogICAgQyAtLSB5ZXMgLS0+IEVbIlNIQTI1Nls6OF1cbm9mIHNvdXJjZSJdCiAgICBFIC0tPiBGeyJFeGlzdGluZyBoYXNoXG5pbiBTVkcgZGlyPyJ9CiAgICBGIC0tIHNhbWUgaGFzaCAtLT4gR1siU1ZHcyByZXVzZWQiXQogICAgRiAtLSBuZXcvY2hhbmdlZCAtLT4gSFsibW1kYyBkYXJrXG5tbWRjIGxpZ2h0Il0KICAgIEggLS0+IElbImRpYWdyYW0te2hhc2h9LWRhcmsuc3ZnXG5kaWFncmFtLXtoYXNofS1saWdodC5zdmciXQogICAgSSAtLT4gSlsiU2NhbiBhbGwgLm1kXG5pbiBidW5kbGUiXQogICAgSiAtLT4gS1siRGVsZXRlIHNhZmVcbm9ycGhhbiBTVkdzIl0KICAgIEcgLS0+IExbIlJlcGxhY2UgYmxvY2tcbndpdGggc2hvcnRjb2RlIl0KICAgIEsgLS0+IEw= -->
{{< mermaid-svg hash="cee573c3" >}}


## Tags

- hugo
- mermaid
- svg
- build
- csp
- infrastructure

## Categories

- infrastructure
- hardening
