Mermaid diagrams & conditional widget loading
Mermaid renders ```mermaid fenced code blocks as diagrams, like Obsidian.
Implementing it introduced a general mechanism — per-note, backend-decided
widget script loading — that any future per-language client widget reuses.
Two parts
- Conditional widget loading (backend) — the server knows, at render time,
which client widgets a page needs, and emits exactly those<script>tags. - The mermaid bundle (frontend) — a tiny glue that lazily loads the heavy
mermaid library, mirroring the datachart/echarts split.
No mermaid HTML is produced server-side: goldmark already renders the fenced
block as <pre><code class="language-mermaid">…</code></pre>. The glue takes it
from there in the browser.
Conditional widget loading
Why backend-decided, not a client loader
A client-side loader (one loader.js that inspects the DOM and injects the
widgets it finds) adds a request waterfall: HTML → run loader → inject glue →
download glue → … The browser's preload scanner can't see dynamically injected
scripts. Since the server already parses every note at render time, it knows
which widgets are needed and can put the <script defer> tags straight into the
initial HTML — the preload scanner fetches them immediately, in parallel, with
no JS-execution gate.
How it works
model.NoteView.CodeLanguagesis amap[string]boolset of the languages of
every fenced code block in the note, lowercased. It is built by
extractCodeLanguages()during the AST walk (internal/model/note_codelang.go),
called alongsideextractCharts()fromnote.go.HasCodeLanguage(lang)is
the nil-safe lookup.templateviews.Noteexposes narrow accessorsHasCharts()and
HasCodeLanguage(lang)that delegate to the model. Use these — do not
Unwrap()the raw*model.NoteViewinto template-facing code (the wrapper
exists to keep model system fields out of the templater).rendernotepage.buildDefaultTemplateCtx(endpoint.go) appends widget script
URLs toJSURLsbased on the note:chart.jswhennote.HasCharts()mermaid.jswhennote.HasCodeLanguage("mermaid")
app.UserJSURLs()now returns only the core bootstrap scripts
(defaulttemplate.js, the $mol user app); widgets are appended per note.app.AssetURL(path)exposes the cache-busting hashing (assetURL) so each
widget URL carries its own content hash — granular cache busting, no shared
hash.
Adding a new per-language widget
- Build a glue bundle under
assets/<name>/(see below) →assets/<name>.js. - Add it to the
//go:embedlist inassets/embed.go. - In
buildDefaultTemplateCtx, append it when the feature is present, e.g.
if note.HasCodeLanguage("plantuml") { jsURLs = append(jsURLs, env.AssetURL("/assets/plantuml.js")) }.
That's the whole wiring — no client loader, no template changes.
The mermaid bundle
assets/mermaid/ mirrors assets/chart/:
src/index.ts→assets/mermaid.js— tiny glue. Finds
pre > code.language-mermaid, converts each to<div class="mermaid">with the
decoded text, lazily loads/assets/mermaid.min.js(only if a block exists),
thenmermaid.initialize({ startOnLoad: false, theme: dark ? 'dark' : 'default', securityLevel: 'strict' })+mermaid.run({ nodes }).src/lib.ts→assets/mermaid.min.js— wraps the ESM mermaid package in an
IIFE that setswindow.mermaid, mirroring the prebuiltecharts.min.jsglobal.
~3 MB; loaded lazily and only on pages with a diagram.esbuild.browser.mjsbuilds both outputs.
Theme follows document.documentElement.classList.contains('dark'), set by the
inline theme script in views.html.
Build
npm run mermaid # builds assets/mermaid.js + assets/mermaid.min.js
Both outputs are committed artifacts (like chart.js / echarts.min.js).
npm run build (tsc + vite) does not build the widget bundles — run the script
after editing assets/mermaid/src/*.
Files
| File | Role |
|---|---|
internal/model/note_codelang.go |
CodeLanguages set + HasCodeLanguage |
internal/model/note.go |
CodeLanguages field; calls extractCodeLanguages() |
internal/templateviews/note.go |
HasCharts() / HasCodeLanguage() accessors |
internal/case/rendernotepage/endpoint.go |
appends widget scripts per note |
cmd/server/main.go |
core-only UserJSURLs(), AssetURL() |
assets/mermaid/ |
glue + lib bundle sources |
assets/mermaid.js, assets/mermaid.min.js |
built artifacts |
assets/embed.go |
embeds the artifacts |
docs/demo/mermaid.md |
demo note + e2e fixture (/mermaid) |
e2e/vault.spec.js |
"Mermaid Diagrams" tests |
Tests
internal/model/note_codelang_test.go— language set extraction.e2e/vault.spec.js"Mermaid Diagrams" — diagrams render to SVG;mermaid.js
loads on/mermaidbut not on a diagram-free page.