Mermaid Implementation Analysis #
Date: 2025-10-07 #
Problem #
Mermaid diagrams in content/archive/JavaBasic/_index.md were not rendering correctly after migrating from Hextra theme to hugo-tufte theme.
Root Cause Analysis #
Why CDN Approach Failed #
Attempts Made:
CDN v10.9.4 (ES Module)
<script type="module"> import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'; mermaid.initialize({ startOnLoad: true, theme: 'neutral' }); </script>- Result: Syntax errors reported
- Error: “Syntax error in text, mermaid version 10.9.4”
CDN v11 (ES Module)
<script type="module"> import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs'; mermaid.initialize({ startOnLoad: true, theme: 'neutral' }); </script>- Result: Still had errors
- Note: Content worked fine with Hextra theme
CDN v11 (Regular Script)
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script> <script> mermaid.initialize({ startOnLoad: true, theme: 'neutral' }); </script>- Result: Continued errors despite switching from ES module to UMD
Why Bundled Approach Works #
Final Working Implementation:
Copied Mermaid bundle from Hextra theme and used Hugo’s resource pipeline:
<!-- layouts/_default/baseof.html -->
{{ if .Page.Store.Get "hasMermaid" }}
{{- $mermaidJS := resources.Get "lib/mermaid/mermaid.min.js" | fingerprint -}}
<script defer src="{{ $mermaidJS.RelPermalink }}" integrity="{{ $mermaidJS.Data.Integrity }}"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
});
</script>
{{ end }}
Bundle Details:
- Version: 11.3.0
- Size: 2.4MB
- Date: Nov 2, 2024
- Location:
assets/lib/mermaid/mermaid.min.js - Source: Copied from
themes/hextra/assets/lib/mermaid/mermaid.min.js
Key Differences: CDN vs Bundled #
| Aspect | CDN Approach | Bundled Approach |
|---|---|---|
| Version Control | Fixed to CDN version (10.x or 11.x) | Explicit version (11.3.0) |
| Load Timing | Network-dependent, external request | Local file, faster load |
| Module System | ES Module or UMD from CDN | UMD bundle |
| Build Process | Direct CDN URL | Hugo resource pipeline with fingerprinting |
| Integrity | CDN-provided (if any) | Hugo-generated SRI hash |
| Compatibility | May have version mismatches | Tested and verified with theme |
| Offline Development | Requires internet | Works offline |
| Result | ❌ Syntax errors | ✅ Works correctly |
Critical Success Factors #
1. Exact Version Match #
The bundled file uses Mermaid 11.3.0, which may have specific compatibility or bug fixes not present in generic v11 CDN builds.
2. Hugo Resource Pipeline #
{{- $mermaidJS := resources.Get "lib/mermaid/mermaid.min.js" | fingerprint -}}
- Processes file through Hugo’s asset pipeline
- Generates subresource integrity (SRI) hash
- Uses
RelPermalinkfor proper URL generation - Enables browser caching with fingerprinted URLs
3. Deferred Loading #
<script defer src="{{ $mermaidJS.RelPermalink }}" integrity="{{ $mermaidJS.Data.Integrity }}"></script>
deferattribute ensures script loads after HTML parsing- Doesn’t block page rendering
- Executes in order after DOM is ready
4. DOMContentLoaded Wrapper #
document.addEventListener("DOMContentLoaded", function () {
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
});
- Ensures DOM is fully loaded before initialization
- Prevents race conditions
- Safe initialization timing
5. Conditional Loading #
{{ if .Page.Store.Get "hasMermaid" }}
- Only loads Mermaid when needed
- Set by render hook:
{{- .Page.Store.Set "hasMermaid" true -}} - Reduces bundle size for pages without diagrams
Complete Implementation #
Required Files #
1. Render Hook (layouts/_default/_markup/render-codeblock-mermaid.html)
<pre class="mermaid">
{{- .Inner | safeHTML -}}
</pre>
{{- .Page.Store.Set "hasMermaid" true -}}
2. Base Template (layouts/_default/baseof.html)
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
{{- partial "header.html" . -}}
<body>
{{ block "main" . }}{{ end }}
<!-- Load Katex, if necessary. -->
{{ if or .Params.math .IsHome }}
{{ partial "math.html" . }}
{{ end }}
<!-- Load Mermaid, if necessary. -->
{{ if .Page.Store.Get "hasMermaid" }}
{{- $mermaidJS := resources.Get "lib/mermaid/mermaid.min.js" | fingerprint -}}
<script defer src="{{ $mermaidJS.RelPermalink }}" integrity="{{ $mermaidJS.Data.Integrity }}"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
});
</script>
{{ end }}
</body>
</html>
3. Mermaid Bundle (assets/lib/mermaid/mermaid.min.js)
- 2.4MB bundled file
- Version 11.3.0
- Copied from Hextra theme
Hextra Theme Implementation Reference #
Hextra theme uses the same pattern:
Location: themes/hextra/layouts/partials/scripts.html (lines 34-45)
{{/* Mermaid */}}
{{/* FIXME: need to investigate .Page.Store hasMermaid is set for homepage */}}
{{- if and (.Page.Store.Get "hasMermaid") (not .Page.IsHome) -}}
{{- $mermaidJS := resources.Get "lib/mermaid/mermaid.min.js" | fingerprint -}}
<script defer src="{{ $mermaidJS.RelPermalink }}" integrity="{{ $mermaidJS.Data.Integrity }}"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
const theme = document.documentElement.classList.contains("dark") ? "dark" : "default";
mermaid.initialize({ startOnLoad: true, theme: theme });
});
</script>
{{- end -}}
Differences from our implementation:
- Hextra has additional check
(not .Page.IsHome)- they note this is a FIXME - Hextra supports dark theme detection - we use fixed
'neutral'theme - Both use identical resource pipeline approach
- Both use identical
deferandDOMContentLoadedpatterns
Why This Matters #
Version-Specific Behavior #
Mermaid syntax evolves between versions. The content in JavaBasic/_index.md uses modern syntax:
flowchart LR
intro@{ shape: text, label: "..." }This @{ shape: text } syntax may not be supported or may have bugs in older/generic builds.
Build Process Integration #
Hugo’s resource pipeline:
- Validates file exists in
assets/ - Generates content hash for cache busting
- Creates SRI hash for security
- Optimizes delivery with proper MIME types
CDN approach bypasses all this and relies on external infrastructure.
Debugging Lessons #
- “It works in theme X” → Check theme’s exact implementation
- CDN version mismatches → Use bundled files for consistency
- ES modules vs UMD → Some tools expect specific module systems
- Hugo best practices → Use resource pipeline, not external URLs
Maintenance Notes #
Updating Mermaid Version #
To update Mermaid in the future:
# Download specific version
curl -o assets/lib/mermaid/mermaid.min.js \
https://cdn.jsdelivr.net/npm/mermaid@11.3.0/dist/mermaid.min.js
# Or copy from updated Hextra theme
cp themes/hextra/assets/lib/mermaid/mermaid.min.js \
assets/lib/mermaid/mermaid.min.js
Testing Checklist #
- Verify version number:
grep -aoE '"[0-9]+\.[0-9]+\.[0-9]+"' assets/lib/mermaid/mermaid.min.js | head -1 - Test with existing diagrams in
content/archive/JavaBasic/_index.md - Check browser console for errors
- Verify SRI hash generation in HTML output
- Test both light and neutral themes (if implementing dark mode)
Known Issues #
- Hextra theme notes: “need to investigate .Page.Store hasMermaid is set for homepage”
- This suggests potential false positives in detection
- Our implementation doesn’t exclude homepage - may want to add this check
Conclusion #
Working Solution: Bundled Mermaid 11.3.0 with Hugo resource pipeline
Key Insight: Theme-bundled assets are tested and verified for compatibility. When CDN fails, copy the working bundle rather than troubleshooting version mismatches.
Success Factors:
- Exact version control (11.3.0)
- Hugo resource pipeline with fingerprinting
- Proper load timing (defer + DOMContentLoaded)
- Conditional loading via Page.Store
- Render hook for code block transformation