Client-Side Mermaid Render Pipeline
meridian beginner 4 min read
ELI5
Astro hands the browser a fenced code block tagged “mermaid”. The browser then dynamically loads the mermaid library, scoops out the lines from each block, makes a fresh <div class="mermaid">, and tells mermaid to redraw — once on first load, and again every time you click a sidebar link without a full reload.
Technical Deep Dive
The render hook is registered in astro.config.mjs as a Starlight head script.
Render Sequence
sequenceDiagram autonumber participant U as User participant SL as Starlight page participant SCR as head script participant CDN as cdn.jsdelivr.net mermaid@11 U->>SL: navigate (hard load) SL->>SCR: script tag executes SCR->>CDN: import mermaid.esm.min.mjs CDN-->>SCR: mermaid module SCR->>SCR: mermaid.initialize startOnLoad:false theme:dark SCR->>SCR: renderMermaid() loop each pre[data-language="mermaid"] SCR->>SCR: read .ec-line spans (or fallback textContent) SCR->>SCR: replace .expressive-code wrapper with div.mermaid end SCR->>SCR: mermaid.run({ querySelector: '.mermaid' }) U->>SL: click internal link SL-->>SCR: astro:after-swap SCR->>SCR: renderMermaid() againWhy .ec-line Reconstruction
Starlight wraps fenced code blocks in expressive-code, which splits the source into per-line <span class="ec-line"> elements for syntax highlighting. Reading pre.textContent would still work but include zero-width whitespace and copy-button affordances; the per-line join produces a clean newline-separated string that mermaid’s parser tolerates.
const lines = pre.querySelectorAll('.ec-line');const code = lines.length > 0 ? Array.from(lines).map(l => l.textContent).join('\n') : pre.textContent;The lines.length > 0 guard preserves a fallback for blocks that arrive un-expressivecoded (e.g. dynamically injected at runtime).
View-Transition Survivability
Starlight (Astro) uses view transitions for soft navigation. The script’s module-scoped state (mermaid library + initialise call) survives, but the DOM is swapped — every navigation calls renderMermaid again via the astro:after-swap listener so newly-arrived <pre data-language="mermaid"> blocks get rewritten before they ever paint as plain code.
Initialisation
mermaid.initialize({ startOnLoad: false, theme: 'dark' }) disables mermaid’s own DOM scan; the script drives every render explicitly. Theme is hard-dark to match the Mars surface (see meridian-001).
Authoring Path
Markdown authors still write fenced ```mermaid blocks. remark-mermaidjs is in the dependency list as a build-time fallback rendering route, but the runtime script is what visibly renders the diagrams a reader sees on a Starlight page.
Key Terms
expressive-code→ Starlight’s syntax-highlighting wrapper; emits<pre data-language="...">with.ec-linechildren.astro:after-swap→ DOM event Astro dispatches after a soft view-transition completes.startOnLoad→ mermaid option that, when true, auto-renders onDOMContentLoaded. Forced false here to keep render under the script’s control.
Q&A
Q: What if the CDN is blocked by a CSP?
A: The dynamic import() rejects, the script throws once, and every mermaid block stays as a plain code block. There is no inline fallback bundled with meridian today.
Q: Why replace pre.closest('.expressive-code') rather than pre itself?
A: The expressive-code wrapper carries the copy button, frame, and title chrome — replacing only pre leaves orphaned chrome around the rendered SVG. closest('.expressive-code') || pre falls back when the wrapper is absent.
Q: Does the rerender cost grow with page size?
A: Yes — mermaid.run({ querySelector: '.mermaid' }) walks every .mermaid div on the page each astro:after-swap. For pages with many diagrams the cost is real but bounded by current page contents, not history.
Examples
Author writes:
```mermaidflowchart LR A --> B```The build emits <pre data-language="mermaid"><span class="ec-line">flowchart LR</span><span class="ec-line"> A --> B</span></pre>. On first paint the script reads the two .ec-line spans, joins them with \n, replaces the .expressive-code wrapper with <div class="mermaid">flowchart LR\n A --> B</div>, and mermaid.run paints SVG into it.
neighbors on the map
- Applying the Mars Theme to Starlight Docs creating a new docs site in the devarno ecosystem
- Packet Structure & Envelope Format debugging packet corruption