I built md-mermaid-pdf: Markdown → PDF with Mermaid that actually renders
When I’m learning a system or writing it up for someone else, I like one Markdown file: prose, headings, and diagrams, all in git so diffs stay honest.
I often work in Cursor with Claude in the loop—describe a boundary or a flow, ask for a diagram, tighten the Mermaid until it matches what I mean. Viewers like GitHub already render mermaid fences in the browser.
The gap showed up when I needed a PDF for a review or handoff. The stack I’d been using—md-to-pdf (Marked for HTML, Puppeteer for print—produced a fine PDF except for one thing: Mermaid blocks were still plain code, not graphics. I didn’t want a parallel diagram pipeline or manual exports every time the doc changed.
So I built and published md-mermaid-pdf: same general config surface as md-to-pdf, with a Mermaid pass in the browser before page.pdf().
md-to-pdf vs what I shipped
| md-to-pdf | md-mermaid-pdf | |
|---|---|---|
| Mermaid in PDF | Shown as code | Rendered as diagrams |
| Config / options | Baseline | Drop-in style + Mermaid-specific options (upstream docs) |
How it works (short)
- Markdown is parsed to HTML (same family of pipeline as
md-to-pdf). - Fenced
mermaidblocks become<div class="mermaid">so Mermaid can own them. - In headless Chromium, the library loads Mermaid (default: CDN, configurable via
mermaidCdnUrl; or bundled /mermaidSource: 'bundled'/'auto'for offline or locked-down CI—see README). - It awaits
mermaid.run()so SVGs exist beforepage.pdf(). - Smart skip: if the file has no
mermaidfence, the Mermaid script path is skipped (faster, no network hit).
Default CDN use means network at generate time unless you inject a local bundle via config.script or use bundled mode—documented in the repo.
What’s in the package
Install & engines: Node ≥ 20.16, npm ≥ 10.8 (see engines on npm). The package is CommonJS (require('md-mermaid-pdf')); ESM interop is via bundlers or createRequire.
Core APIs (full table in the README) include mdToPdf, zero-config mdToPdfAuto, mdToPdfFromFiles (compose doc), and mdToPdfBatch (determinism / batching).
Features I use and ship:
- Presets:
preset: 'github' | 'minimal' | 'slides'— slides = landscape,---as slide breaks. - TOC:
toc: trueprepends a heading-based table of contents. - Diagram export:
mermaidExportImages: 'out/diagrams'or{ dir: 'out', format: 'svg' }(PNG default). - Strict CI:
failOnMermaidError: trueoronMermaidError: (err, { diagramCount }) => 'skip'for softer behavior. - Mermaid tuning:
mermaidConfig(theme, flowchart curves, etc.), CLI--theme dark. - Hooks:
beforeRender(page)for extra CSS or DOM tweaks before PDF. - Debug:
debug: truewrites intermediate HTML (.md-mermaid-pdf-debug.html) and logs Mermaid errors to stderr. - Flaky renders:
mermaidWaitUntil,mermaidRenderTimeoutMsto tune waits.
YAML front matter in the .md merges into config (preset, toc, mermaidConfig, pdf_options, …). You can nest under md_mermaid_pdf: to avoid colliding with other YAML (e.g. CMS metadata)—details in the README.
CLI
Bin: md-mermaid-pdf, alias mmdpdf.
npx md-mermaid-pdf examples/sample.md
npx md-mermaid-pdf input.md output.pdf
npx md-mermaid-pdf input.md --watch
npx md-mermaid-pdf slides.md --slides
npx md-mermaid-pdf a.md b.md c.md # batch: one PDF per file
npx md-mermaid-pdf --concat a.md b.md -o book.pdf
npx md-mermaid-pdf "docs/**/*.md"
npx md-mermaid-pdf input.md -o - # PDF to stdout
npx mmdpdf input.md
Programmatic (minimal example)
const { mdToPdf, mdToPdfAuto } = require('md-mermaid-pdf');
await mdToPdf({ path: 'doc.md' }, { dest: 'doc.pdf', basedir: __dirname });
await mdToPdfAuto('doc.md'); // dest beside input, sensible defaults + mermaidSource auto
More options (CDN override, bundled Mermaid, mermaidExportImages, toc, presets, beforeRender) are in the programmatic section of the README.
Docker
docker build -t md-mermaid-pdf .
docker run --rm -v "$(pwd):/work" -w /work md-mermaid-pdf input.md output.pdf
Mount /work so the container can read your Markdown and write the PDF—same pattern as the README.
GitHub Action
Composite action in the repo’s action/ directory:
- uses: actions/checkout@v4
- uses: Ali-Karaki/md-mermaid-pdf/action@main
with:
input: docs/readme.md
output: readme.pdf
Integration recipes (Express, Next.js route, more Action patterns): docs/recipes.md.
Demo site, VS Code, CI
- Live demo / marketing UI: Railway deployment — source md-mermaid-pdf-site (Vite + React; PDF download in production uses the Docker path; locally,
dev:api+devwith Vite proxy per that repo’s README). - VS Code: extension under
packages/vscode-md-mermaid-pdf— command “Export Markdown to PDF (Mermaid)” for the active editor. - CI: workflow badge and runs live on GitHub Actions.
Troubleshooting (where to look)
The README’s Troubleshooting section covers offline/air-gapped Mermaid, Puppeteer on Linux/CI (deps, --no-sandbox when root—see Puppeteer troubleshooting), and tuning waits. If something’s wrong with edge-case diagrams, fonts, or CI, open an issue—I maintain the library in public and iterate from real reports.
Why Cursor + Claude still fit
The model side speeds up drafting and refactoring Mermaid from rough explanations; it doesn’t replace architectural judgment. md-mermaid-pdf is the export layer I wanted: same .md I iterate on in the editor, PDFs that include real diagrams, and enough CLI/API/Docker/Action surface to use it in real pipelines—not just on my laptop.