21/03/269 min read
#markdown#mermaid#pdf#documentation#ai#cursor

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-pdfmd-mermaid-pdf
Mermaid in PDFShown as codeRendered as diagrams
Config / optionsBaselineDrop-in style + Mermaid-specific options (upstream docs)

How it works (short)

  1. Markdown is parsed to HTML (same family of pipeline as md-to-pdf).
  2. Fenced mermaid blocks become <div class="mermaid"> so Mermaid can own them.
  3. 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).
  4. It awaits mermaid.run() so SVGs exist before page.pdf().
  5. Smart skip: if the file has no mermaid fence, 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: true prepends a heading-based table of contents.
  • Diagram export: mermaidExportImages: 'out/diagrams' or { dir: 'out', format: 'svg' } (PNG default).
  • Strict CI: failOnMermaidError: true or onMermaidError: (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: true writes intermediate HTML (.md-mermaid-pdf-debug.html) and logs Mermaid errors to stderr.
  • Flaky renders: mermaidWaitUntil, mermaidRenderTimeoutMs to 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 + dev with 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.

Related Articles