Why I Render Mermaid Before Uploading Markdown Images
Why this matters
Diagrams are easy to treat as decoration until an automated publisher has to move an article between three different representations: authoring Markdown, publishable local assets, and delivery URLs.
In this project, Mermaid is intentionally part of the authoring source. The publishing pipeline, however, only processes Markdown image nodes. That creates a boundary worth making explicit: a Mermaid code fence must become a local image before image optimization and upload begin.
The focused question is: how can a Markdown publisher make diagram rendering reproducible without mixing generated URLs back into the authoring source?
What I built or tested
The repository uses three artifacts for one article:
| Artifact | Purpose | Safe rerun input? |
|---|---|---|
source.md |
Human-authored Markdown with Mermaid blocks | Yes, for rendering |
index.md |
Rendered Markdown with local PNG references | Yes, for publishing |
published.md |
Archival Markdown with delivery URLs | No |
The renderer scans fenced mermaid blocks, assigns each one a deterministic filename from its order and content hash, writes its source beneath the article's image directory, and asks Mermaid CLI for a transparent PNG. It checks that the PNG exists, then replaces the fence with a relative Markdown image link in index.md.
That sequence leaves the authoring document readable while handing the publisher exactly the input it understands.
Setup
The example uses a Node.js TypeScript publisher with Mermaid CLI available locally. An article needs ordinary publisher-compatible front matter and one or more Mermaid fences. In normal authoring, the fence begins with the literal mermaid ` marker and ends with `; the pipeline diagram below is one such fence.
The render command writes a different file rather than editing the source in place:
npm run article:render -- articles/my-article/source.md \
--output-markdown articles/my-article/index.md
That output file is the input for asset preparation or publication.
Step-by-step walkthrough
1. Keep the diagram as source until rendering
The renderer finds fenced Mermaid blocks and computes a short hash from normalized diagram text. The output name also includes the diagram's position, so two distinct blocks do not collide merely because they have similar labels.
The original source stays in source.md. The generated Mermaid text is kept privately beside the generated images, which is useful when a diagram needs to be regenerated with the same toolchain.
2. Replace the fence with a local, relative asset
After Mermaid CLI creates the PNG, the renderer verifies that the expected file exists. Only then does it write a Markdown image link such as ./images/diagram-01-<hash>.png into index.md.
The important contract is the arrow from rendered Markdown to image preparation. The uploader never needs to parse Mermaid syntax or know how a diagram was created; it receives an ordinary local image reference.
3. Prepare images only after the boundary is crossed
The publisher parses Markdown, visits image nodes, and rejects paths that are absolute, external, data URLs, query-bearing, or outside the article directory. For a permitted image it optimizes the file and caches the upload by provider and object key.
It also deduplicates repeated references to the same source image during one publish operation. This makes index.md a small, deterministic contract: all publishable visual inputs are local files below the article directory.
4. Publish the rendered input, archive the URL form
During a dry run, the publisher uses preview storage and in-memory publication state. A real run uses the configured storage provider and persists upload records. In both cases, the image replacement happens before the platform publish input is constructed.
The final Markdown with delivery URLs is useful for archival and inspection, but it is not the rerun input. Re-rendering starts from source.md; re-publishing starts from the local-asset index.md.
What went wrong
The tempting shortcut is to send source.md directly to image preparation. It looks harmless because Mermaid is valid Markdown code fencing.
But a fenced diagram is not a Markdown image node. The uploader walks image nodes only, so it has no local file to optimize or upload. The platform would receive source text instead of a managed diagram asset. Conversely, putting external delivery URLs back in the authoring source would make later renders and local verification depend on an already-published artifact.
Rendering is also a real dependency boundary. Mermaid CLI needs a working browser runtime. This implementation treats a missing expected PNG as an error rather than emitting index.md with a broken link.
Fix or mitigation
Use a two-phase handoff:
- Author Mermaid in
source.md. - Render it into
index.mdand inspect the local PNG. - Run a publisher dry run against the rendered file.
- Prepare assets and publish only from
index.md. - Retain
published.mdas a record of the externally addressable result.
This separation also gives an AI coding agent a safe review boundary. It can draft or revise the Mermaid source, while the renderer and publisher enforce the deterministic parts of the transformation.
Trade-offs
This design creates more files and requires Mermaid CLI in every environment that renders an article. Diagram changes must be rendered again, and generated PNGs need to travel with the article workspace.
Those costs buy useful properties:
- authoring Markdown remains independent of a storage provider;
- the publisher sees only local paths it can validate;
- a preview can exercise image replacement without a platform write;
- the final URL artifact cannot accidentally become the next publishing input.
It is not a universal rule. A site that natively renders Mermaid could publish the fence directly. The extra render phase is justified when the target platform needs ordinary image URLs and the publishing tool owns image processing.
How I verified it
I verified the boundary at four levels:
- Source trace: checked that rendering writes the generated image, verifies it exists, and changes the fence into a local relative image reference.
- Automated test: checked the Mermaid tests that assert a stable local PNG link and preserved front matter.
- Article run: rendered this article and inspected the generated diagram before publishing.
- Publisher preview: ran the generator validator and publisher dry run, which exercise the rendered Markdown without calling a platform API.
The limitation is deliberate: a dry run proves the local transformation and payload construction, not that a remote platform will display every image as expected. That final step still needs the one authorized publication result.
Conclusion
Mermaid rendering is not merely a visual build step in this pipeline. It is the conversion from an authoring language to a local-asset contract that the publisher can validate, optimize, upload, and reproduce.
Keep the source fence, generate the local image, publish the rendered Markdown, and archive the delivery-URL result. With those boundaries in place, a diagram stays both editable for authors and concrete for the platform.
AI assistance disclosure
AI assisted with outlining and drafting. Technical claims were checked against the repository, and the article's render and dry-run path were verified locally before publication.

Top comments (0)