- Aug 23, 2026
- 5 min read
Validate Mermaid Diagrams Before You Push Them
My architecture notes live in Markdown, and the diagrams in them are Mermaid blocks that GitHub renders. That’s convenient until a diagram has a syntax error: GitHub shows the error box only when someone opens the file. Nothing fails at commit or push time.
When I wrote an overview page with five diagrams (system context, a request sequence, a sign-up sequence, a state machine and a loop), I wanted two checks before pushing: does every diagram parse, and is it actually readable? The second check mattered more than I expected.
Step 1: extract the blocks
Mermaid’s CLI renders one diagram per file, so the first job is pulling each
fenced mermaid block out of the Markdown into its own .mmd file. A few
lines of Python do it:
import re
from pathlib import Path
FENCE = "`" * 3
text = Path("architecture/overview.md").read_text()
blocks = re.findall(FENCE + r"mermaid\n(.*?)" + FENCE, text, re.S)
for i, block in enumerate(blocks):
Path(f"d{i}.mmd").write_text(block)
print(len(blocks), "diagrams")
The non-greedy (.*?) with re.S stops at the first closing fence, so
neighbouring blocks don’t merge into one.
Step 2: render each one
for f in d*.mmd; do
npx -y @mermaid-js/mermaid-cli@11 -i "$f" -o "${f%.mmd}.svg" -q \
&& echo "ok $f" || echo "FAIL $f"
done
mmdc exits non-zero on a parse error and prints the line it choked on. The
first run downloads a headless browser, so expect a minute; after that it’s
quick. I pin the major version (@11) so a CLI update doesn’t change what
“valid” means between runs.
All five parsed on the first try. That turned out to be the least interesting result.
Step 3: look at it
Parsing proves the syntax. It says nothing about whether the picture helps. So I render PNGs at a fixed width on a white background and open them:
npx -y @mermaid-js/mermaid-cli@11 -i d0.mmd -o d0.png -w 1200 -b white -q
The system-context diagram was valid and useless. I had written it as
flowchart LR with one node per site and one edge per site to the forms
Worker, plus edges for alerts, emails, the homelab pull and a laptop tool.
Left to right, the layout engine produced long crossing lines, three
identical edge labels stacked on top of each other, and one label floating
far from its arrow.
What made it readable
Top to bottom, with rows inside groups
I switched the outer direction to flowchart TB and laid out each group as a
row with direction LR inside the subgraph:
flowchart TB
V([Visitor's browser])
subgraph Pages["Static sites"]
direction LR
A["site-a.example<br/>enquiry, ebook"]
B["site-b.example<br/>questions, newsletter"]
end
subgraph Edge["Workers + KV"]
direction LR
W["forms Worker"]
KV[("KV store")]
end
V --> Pages
Pages -->|"form POST, same-origin"| W
W -->|"1 · store"| KV
Layers now read top to bottom: visitor, sites, edge, storage, homelab.
One edge from the group, not one per node
Every site posts to the same Worker in the same way, so the diagram only
needs to say it once. Mermaid lets an edge start at a subgraph:
Pages --> W. Three parallel edges with identical labels became one.
Merge labels that collide
In the homelab loop diagram, one step had two outgoing edges labelled
SENDER=smtp and SENDER=mailtrap. They rendered on top of each other. The
fix was to make the choice a node instead of two labels:
E --> SND["sender adapter<br/>SENDER=smtp → Mailpit (dev)<br/>SENDER=mailtrap → bulk API"]
If two edges always leave the same node toward similar targets, a single node describing the choice is usually clearer anyway.
Number the order
On the edges out of the Worker I labelled 1 · store and 2 · alert. The
order (store first, alert second) is the point of the design, and a digit in
the label says it without a separate sequence diagram.
Syntax notes that saved me errors
- Quote labels with special characters. Anything with parentheses,
slashes,
<br/>or punctuation goes in double quotes:W["leads-forms Worker<br/>site/api/forms/*"]. Unquoted parentheses are read as shape syntax. - Edge labels too:
A -->|"form POST, same-origin"| B. - Shapes:
[(...)]is a cylinder (storage),([...])a stadium (people, devices),{...}a decision. - Dotted edges (
-.->) for things that happen in the background or only sometimes: alerts, dev-only paths, cron triggers. - sequenceDiagram:
autonumbernumbers the arrows for free;--)is an async message (fire and forget), which fits a background alert;alt … else … endshows the success and failure paths in one diagram. - stateDiagram-v2: transition labels after a colon accept free text,
including parentheses:
pending --> confirmed: POST confirm (within 7 days).[*]is both the start and the end state. - Say what you mean in labels. One of my transitions read “link expires (no email is ever sent)”, which sounded like the confirmation email is never sent. It now reads “link expires, never confirmed (gets no newsletters or drips)”.
Where this fits
Diagrams are part of how I document decisions for my future self, alongside Architecture Decision Records. A diagram that doesn’t render, or renders as a tangle, fails at the one moment someone needs it. One loop (extract, render, look) is cheap insurance.
For an example of what these diagrams describe, see the pull-only homelab mirror, whose loop is one of the five.
Takeaways
- GitHub won’t tell you a Mermaid block is broken until someone views it. Check before pushing.
- Extract blocks with a short regex, run
mermaid-clion each, and pin its major version. - Valid isn’t readable: render PNGs and look at them.
- For system context:
flowchart TB, subgraphs withdirection LR, edges from groups instead of from every node. - Turn colliding edge labels into a node, and quote any label with special characters.
Previous: shell traps while watching logs.