Comparison
Pandoc vs Browser-Based Markdown to PDF: Every CLI Tool Compared
Published July 30, 2026
The choice between Pandoc and a browser-based converter is usually framed as power versus convenience, which is roughly right and hides the more useful distinction: these tools use fundamentally different typesetting engines, and that determines what they are each good at more than any feature list does.
Pandoc’s PDF path goes through LaTeX, which is a typesetting system. Browser tools and most CLI alternatives go through a CSS engine. LaTeX gives better line breaking, real float placement and mature cross-referencing. CSS gives you a styling language you already know, live iteration, and web-native content like Mermaid diagrams. Neither is strictly better.
This guide compares every option properly.
The comparison
| Pandoc (LaTeX) | Pandoc (HTML) | WeasyPrint | wkhtmltopdf | md-to-pdf / Puppeteer | Browser tool | |
|---|---|---|---|---|---|---|
| Install size | 3–5 GB | ~150 MB | ~100 MB | ~50 MB | ~400 MB | None |
| Setup time | 20–40 min | 5 min | 5 min | 2 min | 5 min | None |
| Engine | LaTeX | CSS (varies) | CSS (own) | CSS (old WebKit) | CSS (Chromium) | CSS (Chromium) |
| Print CSS | N/A | Depends | Excellent | Very poor | Good | Good |
@page margin boxes | N/A | Depends | Yes | No | No | Yes (Paged.js) |
| Running headers | Yes (LaTeX) | Depends | Yes | No | Templates only | Yes |
counter(pages) | Yes | Depends | Yes | No | Templates only | Yes |
| Per-page footnotes | Yes | No | Yes | No | No | Yes |
| Citations / BibTeX | Yes | Yes | No | No | No | No |
| Cross-references | Yes | Yes | No | No | No | No |
| Index generation | Yes | No | No | No | No | No |
| Float placement | Excellent | Poor | Poor | Poor | Poor | Poor |
| Line breaking | Best | Good | Good | Fair | Good | Good |
| JavaScript content | No | No | No | No | Yes | Yes |
| Mermaid diagrams | Via filter | Via filter | No | No | Yes | Yes |
| Batch / scripting | Yes | Yes | Yes | Yes | Yes | No |
| Live preview | No | No | No | No | No | Yes |
| Other output formats | 30+ | 30+ | No | No | No | HTML |
Three rows in there decide most cases. Citations is a hard yes/no — only Pandoc has it. @page margin boxes determines whether you can have real page numbers and running headers. Live preview determines how fast you can iterate on layout.
Pandoc in depth
Pandoc is a document converter, not a PDF tool. It reads about forty formats and writes about sixty. PDF is one output among many, and it produces it by converting to an intermediate format and invoking an external engine.
Choosing an engine
# LaTeX engines — best typography
pandoc doc.md -o doc.pdf --pdf-engine=xelatex # Unicode + system fonts
pandoc doc.md -o doc.pdf --pdf-engine=lualatex # Unicode + Lua scripting
pandoc doc.md -o doc.pdf --pdf-engine=pdflatex # Fastest, poor Unicode
# CSS engines — use your own stylesheet
pandoc doc.md -o doc.pdf --pdf-engine=weasyprint --css=print.css
pandoc doc.md -o doc.pdf --pdf-engine=wkhtmltopdf --css=print.css
# Commercial
pandoc doc.md -o doc.pdf --pdf-engine=prince --css=print.css
Use xelatex by default. pdflatex cannot use system fonts and handles non-ASCII text badly. lualatex is a fine alternative and slightly slower. If you specify mainfont, you must use xelatex or lualatex.
What Pandoc uniquely does
Citations. The reason many people use it at all:
pandoc paper.md \
--citeproc \
--bibliography=refs.bib \
--csl=apa-7th-edition.csl \
--pdf-engine=xelatex \
-o paper.pdf
Ten thousand CSL styles, and switching between them is one flag. Nothing else in this comparison can do this — see the academic writing guide.
Cross-references via pandoc-crossref, so “Figure 3” and “see section 2.1” stay correct when you reorder.
Multi-format output from one source. A PDF for submission, HTML for a preprint server, DOCX for a collaborator who wants tracked changes, EPUB for an ebook — all from the same Markdown:
for fmt in pdf html docx epub; do
pandoc book.md -o "build/book.$fmt" --toc
done
This is genuinely hard to replicate and often the deciding factor.
Filters. Lua or JSON filters transform the document AST between parsing and writing, letting you implement arbitrary behaviour:
-- shortcode.lua: turn {{version}} into a value from metadata
function Str(el)
if el.text == "{{version}}" then
return pandoc.Str(PANDOC_DOCUMENT.meta.version[1].text)
end
end
pandoc doc.md --lua-filter=shortcode.lua -M version=4.2.0 -o doc.pdf
Templates. Full control over the LaTeX preamble:
pandoc --print-default-template=latex > custom.tex
# edit it
pandoc doc.md --template=custom.tex --pdf-engine=xelatex -o doc.pdf
Useful variables
Most formatting requirements map to -V flags without touching a template:
pandoc doc.md \
--pdf-engine=xelatex \
--toc --toc-depth=2 \
-V documentclass=report \
-V fontsize=11pt \
-V geometry:margin=25mm \
-V mainfont="Source Serif 4" \
-V sansfont="Inter" \
-V monofont="JetBrains Mono" \
-V linestretch=1.5 \
-V colorlinks=true \
-V linkcolor=blue \
-V urlcolor=blue \
-o doc.pdf
Put them in a defaults.yaml to avoid retyping:
# defaults.yaml
pdf-engine: xelatex
toc: true
toc-depth: 2
variables:
documentclass: report
fontsize: 11pt
geometry: margin=25mm
mainfont: "Source Serif 4"
monofont: "JetBrains Mono"
colorlinks: true
pandoc doc.md -d defaults.yaml -o doc.pdf
The install problem
This is the real barrier. A full TeX Live is 4–5 GB and takes twenty minutes. Slimmer options:
# macOS: BasicTeX is ~100 MB instead of MacTeX's ~4 GB
brew install pandoc
brew install --cask basictex
sudo tlmgr update --self
sudo tlmgr install collection-fontsrecommended
# Debian/Ubuntu: the minimal set that actually works
sudo apt install pandoc texlive-xetex texlive-fonts-recommended
# Or skip LaTeX entirely
pip install weasyprint
pandoc doc.md --pdf-engine=weasyprint --css=print.css -o doc.pdf
The last option is worth emphasising. You can use Pandoc without LaTeX by pairing it with WeasyPrint. You keep citations, filters and multi-format output; you give up LaTeX’s typography and gain CSS control. For many people this is the best of both.
Or avoid local install with Docker:
docker run --rm -v "$PWD:/data" pandoc/latex:latest \
doc.md --pdf-engine=xelatex -o doc.pdf
The pandoc/latex image is ~1 GB but needs no host setup and is reproducible — a good fit for CI.
WeasyPrint
The most under-appreciated tool here. A Python library implementing CSS Paged Media properly, and the best CSS-based option for document work.
pip install weasyprint
# Markdown → HTML → PDF
pandoc doc.md -t html5 --standalone --css=print.css | weasyprint - doc.pdf
# Or directly from HTML
weasyprint input.html output.pdf -s print.css
Python API:
from weasyprint import HTML, CSS
import markdown
md = markdown.Markdown(extensions=["tables", "fenced_code", "footnotes", "toc"])
html = md.convert(open("doc.md", encoding="utf-8").read())
HTML(string=f"<!doctype html><html lang='en'><body>{html}</body></html>").write_pdf(
"doc.pdf",
stylesheets=[CSS(filename="print.css")],
)
Why it matters
It implements the paged media spec that Chromium does not. @page margin boxes, string-set running headers, counter(pages), target-counter(), leader(), and float: footnote for real per-page footnotes. If you want a properly typeset document driven by CSS rather than LaTeX, this is the tool.
It is small and installs cleanly — ~100 MB with no browser and no LaTeX.
Deterministic output. No headless browser timing races. Same input, same PDF.
Its one real limitation
No JavaScript. WeasyPrint parses HTML and CSS; it does not run a script engine. So:
- Mermaid diagrams do not render — pre-render them to SVG first
- MathJax does not run — use KaTeX server-side rendering, or pre-rendered HTML
- Anything requiring client-side rendering must be rendered before WeasyPrint sees it
For a document pipeline that is often fine, and it is the trade-off for determinism.
wkhtmltopdf
Widely recommended in older material and worth being direct about: avoid it for new work.
It is built on a Qt fork of WebKit from around 2012. The project is archived and no longer maintained. Practical consequences:
- No CSS Grid, patchy Flexbox
- No
@pagemargin boxes, so no page numbers via CSS - No modern CSS features — custom properties,
clamp(), logical properties - Known security issues in the bundled engine, including SSRF via local file access
- Rendering that diverges from every current browser
Its virtues are a small install and being fast. If you have a working wkhtmltopdf pipeline producing simple documents, there is no urgency to change it. Do not start one. WeasyPrint installs almost as easily and is better in every way that matters.
Node and headless Chrome tools
md-to-pdf
The simplest Node option:
npm install -g md-to-pdf
md-to-pdf document.md
md-to-pdf --stylesheet print.css --pdf-options '{"format":"A4","printBackground":true}' document.md
Config file for repeat use:
// .md-to-pdf.js
module.exports = {
stylesheet: ["./print.css"],
pdf_options: {
format: "A4",
margin: { top: "22mm", bottom: "22mm", left: "18mm", right: "18mm" },
printBackground: true,
displayHeaderFooter: true,
footerTemplate: `<div style="font-size:8pt;width:100%;text-align:center;color:#888;">
<span class="pageNumber"></span> / <span class="totalPages"></span></div>`,
},
launch_options: { args: ["--no-sandbox"] },
};
printBackground: true is essential and not the default — without it every background colour is stripped.
Direct Puppeteer
Full control when you need it:
import puppeteer from "puppeteer";
import { readFile } from "node:fs/promises";
import MarkdownIt from "markdown-it";
const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
const source = await readFile("document.md", "utf8");
const css = await readFile("print.css", "utf8");
const html = `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><style>${css}</style></head>
<body>${md.render(source)}</body></html>`;
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
await page.evaluateHandle("document.fonts.ready"); // critical
await page.pdf({
path: "document.pdf",
format: "A4",
printBackground: true,
margin: { top: "22mm", bottom: "22mm", left: "18mm", right: "18mm" },
});
await browser.close();
The document.fonts.ready await is the line people omit and then wonder why fonts are wrong — see the typography guide.
The Chromium ceiling
Everything Chromium-based shares one limitation worth restating: no @page margin boxes. No page numbers via CSS, no string-set running headers, no counter(pages), no target-counter() for a contents list with page numbers, no per-page footnotes.
Puppeteer’s headerTemplate and footerTemplate substitute for basic page numbers, with inlined styles and no access to document content. For anything more — a running header showing the current chapter — you need Paged.js, WeasyPrint or Prince.
Paged.js
Worth naming separately: a JavaScript polyfill implementing CSS Paged Media in any browser. It reads your @page rules and generates a paginated DOM with real page boxes, margin boxes and running headers.
That means you can have full paged-media features and JavaScript content, in a browser, with a live preview. It is what this site’s editor uses, and it is the only combination that gives you all three.
Choosing by what you need
Rather than by tool, work backwards from the requirement:
“I need citations and a bibliography.” Pandoc with --citeproc. Nothing else does this. Pair with WeasyPrint if you want to skip the LaTeX install.
“I need page numbers and running headers.” WeasyPrint, Prince, or a Paged.js-based tool. Not plain Puppeteer, not wkhtmltopdf, not VS Code extensions.
“I need PDF and DOCX and HTML from one source.” Pandoc. This is its strongest suit after citations.
“I need to convert 500 files in CI.” Pandoc or WeasyPrint in Docker. Both are deterministic and headless. See the automation guide.
“I need Mermaid diagrams or client-side rendered content.” Something Chromium-based, or Paged.js. WeasyPrint cannot run JavaScript.
“I am fitting a resume to one page.” A browser tool with a live paginated preview. The iteration loop dominates — every other option means export, open, look, adjust, repeat.
“I need the best possible typography for a book.” Pandoc with xelatex, or LaTeX directly. Line breaking, hyphenation and float placement are genuinely better.
“I want to convert this one document right now.” A browser tool. Installing 4 GB of LaTeX to convert a README once is not a reasonable trade.
“My document is confidential.” Any local tool, or a verifiably client-side browser tool — see the privacy guide for how to verify.
The honest summary
Pandoc is the most capable document converter that exists, and for citations, multi-format output and book typography nothing comes close. Its cost is a substantial install and a command-line workflow with no visual feedback.
WeasyPrint is the best CSS-based engine and deserves more attention than it gets — proper paged media, small install, deterministic, no JavaScript.
A browser tool with Paged.js wins on iteration speed and needs no setup, which matters more than feature lists suggest for layout-sensitive documents. It cannot do citations, cross-references or batch processing.
These are not competitors so much as different tools. A realistic setup uses more than one: a browser tool for quick and layout-sensitive work, Pandoc for the paper with eighty references.
Try it yourself
The editor here runs on Paged.js, so you get real @page margin boxes, running headers and counter(pages) alongside a live paginated preview and Mermaid/KaTeX rendering — the combination none of the CLI tools offer. Everything runs client-side with nothing uploaded.
For citations, batch conversion or DOCX output, use Pandoc. Both have their place.
About this guide
Written and maintained by the developer of MarkdownToFile — the browser-based converter this site runs on. Techniques described here are tested against the engines named in the text (Chromium's print pipeline, Paged.js, WeasyPrint), and engine limitations are stated rather than glossed over. Corrections are welcome via the contact page; more about the project on the about page.
Try it yourself — free, no signup
Convert your Markdown to a polished PDF right in your browser.
Open the editor