• 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: autonumber numbers the arrows for free; --) is an async message (fire and forget), which fits a background alert; alt … else … end shows 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-cli on each, and pin its major version.
  • Valid isn’t readable: render PNGs and look at them.
  • For system context: flowchart TB, subgraphs with direction 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.