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)WeasyPrintwkhtmltopdfmd-to-pdf / PuppeteerBrowser tool
Install size3–5 GB~150 MB~100 MB~50 MB~400 MBNone
Setup time20–40 min5 min5 min2 min5 minNone
EngineLaTeXCSS (varies)CSS (own)CSS (old WebKit)CSS (Chromium)CSS (Chromium)
Print CSSN/ADependsExcellentVery poorGoodGood
@page margin boxesN/ADependsYesNoNoYes (Paged.js)
Running headersYes (LaTeX)DependsYesNoTemplates onlyYes
counter(pages)YesDependsYesNoTemplates onlyYes
Per-page footnotesYesNoYesNoNoYes
Citations / BibTeXYesYesNoNoNoNo
Cross-referencesYesYesNoNoNoNo
Index generationYesNoNoNoNoNo
Float placementExcellentPoorPoorPoorPoorPoor
Line breakingBestGoodGoodFairGoodGood
JavaScript contentNoNoNoNoYesYes
Mermaid diagramsVia filterVia filterNoNoYesYes
Batch / scriptingYesYesYesYesYesNo
Live previewNoNoNoNoNoYes
Other output formats30+30+NoNoNoHTML

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 @page margin 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