Skip to content
Guides · Recipes

How to Convert Markdown to PDF, DOCX and HTML with Pandoc

Updated October 27, 2026 .md

Short answer: Install pandoc from its GitHub .deb and the Typst binary (248 MB together, about 2 seconds on a small sandbox), then run pandoc --citeproc --pdf-engine=typst. Typst rendered our report in 0.3 seconds; xelatex took 1.6 seconds after a 467 MB, 47-second TeX Live install. Use --reference-doc for DOCX and --embed-resources for HTML.

Every number here comes from one small sandbox (2 vCPU, 4 GB) on prod on 2026-10-27. The test file was a one-page report with a title block, a table, a code block, one equation, a ::: note block and two citations from a BibTeX file. For DOCX, XLSX or PPTX input, use LibreOffice instead: see batch convert DOCX to PDF with LibreOffice headless.

Which PDF engine to pick#

Pandoc writes PDF through a separate engine. We installed three and rendered the same file with each:

Engine Install Disk added Render time Math Fonts in the PDF
Typst 0.15.1 1.2 s (one static binary) 53 MB 0.31 to 0.33 s Correct Libertinus Serif, New Computer Modern Math
XeLaTeX (TeX Live 2023) 47.3 s (apt, 4 packages) 467 MB 1.60 to 1.76 s Correct Latin Modern
WeasyPrint 70.0 under 1 s (uv tool install) 63 MB 1.32 to 1.60 s Broken Whatever your CSS picks

Pick Typst unless you already have LaTeX templates. It is the smallest, about five times faster than XeLaTeX here, and handles math. Pick XeLaTeX if your team has a LaTeX template or needs a LaTeX package. Pick WeasyPrint only for documents without math whose look you want to control with CSS: it does not render MathML, so our equation came out as garbled text followed by its raw TeX source (x‾=1n∑i=1nxi≈−17.7%\bar{x} = \frac{1}{n}…).

We did not measure texlive-full. The four packages above were enough for pandoc's default LaTeX template, including tables, code highlighting and math.

1. Install pandoc and Typst#

Pandoc is not in the base image, and Ubuntu 24.04's apt offers pandoc 3.1.3. Install the current release from pandoc's GitHub releases, as the pandoc install page describes, and the Typst binary from Typst's releases:

sbx new --type small --name docs-pandoc
id=sb_hjaoeintkz    # the id that sbx new printed
sbx exec $id --timeout 600 "bash install.sh"

install.sh runs these steps inside the sandbox:

sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends poppler-utils
V=$(curl -fsSL https://api.github.com/repos/jgm/pandoc/releases/latest | grep -m1 '"tag_name"' | cut -d'"' -f4)
curl -fsSLo /tmp/pandoc.deb https://github.com/jgm/pandoc/releases/download/$V/pandoc-$V-1-amd64.deb
sudo dpkg -i /tmp/pandoc.deb
curl -fsSL https://github.com/typst/typst/releases/latest/download/typst-x86_64-unknown-linux-musl.tar.xz \
  | sudo tar -xJ -C /usr/local/bin --strip-components=1 typst-x86_64-unknown-linux-musl/typst
pandoc --version | head -1
# pandoc 3.13
typst --version
# typst 0.15.1 (9dfd3a08)

The pandoc .deb took 1.2 seconds and added 195 MB; Typst took 1.2 seconds and added 53 MB. poppler-utils gives you pdfinfo and pdffonts for checking the output.

If you need LaTeX too, this is the set we measured:

sudo apt-get install -y -qq --no-install-recommends \
  texlive-xetex texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended lmodern
xelatex --version | head -1
# XeTeX 3.141592653-2.6-0.999995 (TeX Live 2023/Debian)

Copy your Markdown, .bib file and filters in with sbx scp or a tar over sbx exec -i; see move files in and out of a sandbox.

2. Markdown to PDF with citations#

The report's front matter names the bibliography, and the text cites with [@knuth1984]:

---
title: Quarterly Build Report
author: Platform Team
date: 2026-10-27
bibliography: refs.bib
---

Build times dropped by 18% this quarter. The method follows literate
programming [@knuth1984] and the reproducible-builds checklist [@lamb2021].

--citeproc turns the keys into citations and appends the reference list (Chicago author-date unless you pass --csl):

pandoc report.md --citeproc --lua-filter note.lua --pdf-engine=typst -o out-typst.pdf
pandoc report.md --citeproc --lua-filter note.lua --pdf-engine=xelatex -o out-xelatex.pdf
pdftotext -layout out-typst.pdf - | grep Knuth
# Build times dropped by 18% this quarter. The method follows literate programming (Knuth 1984)
# Knuth, Donald E. 1984. “Literate Programming.” The Computer Journal 27 (2): 97–111.
pdfinfo out-typst.pdf | grep Pages
# Pages:           1

The Typst PDF was 41 KB and the XeLaTeX PDF 29 KB, both one page.

3. A Lua filter that styles one block per format#

Pandoc's fenced divs let you mark a block in Markdown, and a Lua filter decides what it becomes in each format. This one turns ::: note into a gray box in Typst, a named paragraph style in Word, and keeps the CSS class in HTML:

-- note.lua: style ::: note blocks per output format.
function Div(el)
  if not el.classes:includes('note') then return nil end
  if FORMAT == 'docx' then
    el.attributes['custom-style'] = 'Note'
    return el
  elseif FORMAT == 'typst' then
    local out = pandoc.List{ pandoc.RawBlock('typst', '#block(fill: luma(235), inset: 8pt, radius: 4pt)[') }
    out:extend(el.content)
    out:insert(pandoc.RawBlock('typst', ']'))
    return out
  end
  return el -- HTML keeps class="note" for your CSS
end
::: note
Numbers come from the CI dashboard, not from developer laptops.
:::

FORMAT is typst even when the final output is a PDF, so one filter covers both. To see the Typst source pandoc hands to the engine, run pandoc report.md --citeproc --lua-filter note.lua -t typst; our note became #block(fill: luma(235), inset: 8pt, radius: 4pt)[.

4. Markdown to DOCX with a reference document#

Word output takes its fonts, colors and paragraph styles from a reference document. Start from pandoc's own, edit the styles, and pass it with --reference-doc. We edited it with python-docx through uv, which is already in the sandbox:

pandoc -o reference.docx --print-default-data-file reference.docx
uv run --with python-docx python make_ref.py
# saved reference.docx
pandoc report.md --citeproc --lua-filter note.lua --reference-doc reference.docx -o out.docx
# make_ref.py: body font, heading color and a "Note" style.
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.style import WD_STYLE_TYPE
d = Document("reference.docx")
d.styles["Normal"].font.name = "Liberation Serif"
d.styles["Normal"].font.size = Pt(11)
for name in ("Heading 1", "Heading 2"):
    d.styles[name].font.color.rgb = RGBColor(0x1F, 0x4E, 0x79)
note = d.styles.add_style("Note", WD_STYLE_TYPE.PARAGRAPH)
note.base_style = d.styles["Normal"]
note.font.italic = True
note.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
d.save("reference.docx")
print("saved reference.docx")

The conversion took 0.27 to 0.30 seconds. To check which styles the document uses, read its XML:

unzip -p out.docx word/document.xml | grep -o 'w:pStyle w:val="[A-Za-z0-9]*"' | sort | uniq -c
#       2 w:pStyle w:val="Bibliography"
#       3 w:pStyle w:val="Heading1"
#       1 w:pStyle w:val="Note"
# …

The note paragraph uses Note, the style from the reference doc. Pandoc only copies styles from the reference document; the text comes from your Markdown.

5. Markdown to a single HTML file#

pandoc report.md --citeproc --lua-filter note.lua --standalone --embed-resources \
  --css style.css --mathml -o out.html
# [WARNING] Deprecated: --mathml. Use --math-method=mathml instead.

--embed-resources inlines the CSS, so out.html (6.8 KB here) works as one file you can email or attach. The equation is written as MathML, and the note class is kept for your CSS. Pandoc 3.13 still accepts --mathml but warns; in new scripts write --math-method=mathml.

To serve the HTML from the sandbox, run a static server and give its port a private HTTPS URL.

Keep the toolchain for next time#

Packages live on the sandbox disk, so they survive sbx stop and sbx resume. To start every new sandbox with pandoc and Typst already installed, save the set-up sandbox as a template: see turn a sandbox into a reusable template.

FAQ#

Do I need a full TeX Live install for pandoc PDFs?#

No. With Typst you need no TeX at all. If you want LaTeX, texlive-xetex, texlive-latex-recommended, texlive-latex-extra, texlive-fonts-recommended and lmodern with --no-install-recommends added 467 MB and were enough for pandoc's default template in our test.

Why does my equation show up as raw TeX in a WeasyPrint PDF?#

WeasyPrint does not render MathML, so pandoc's math comes out as plain characters followed by the TeX annotation. Use --pdf-engine=typst or --pdf-engine=xelatex for documents with math.

Can I use the pandoc from apt?#

Ubuntu 24.04's apt offers pandoc 3.1.3. We tested only pandoc 3.13 from GitHub with Typst 0.15.1, so we can't say whether the older pandoc works with the current Typst. Installing the current .deb took about a second.

Which sandbox size do I need?#

small was enough: the slowest render, XeLaTeX, took under 2 seconds. Long books or many files at once benefit from more vCPUs; see Machines & sizes.

More guides

Top AI sandbox platforms for code execution in 2026, ranked

For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.

Read the guide
Best sandboxes for running coding agents (Claude Code, Codex, Gemini CLI) in 2026, ranked

For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.

Read the guide
Best Daytona sandbox alternatives in 2026, ranked

The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.

Read the guide
Best E2B alternatives in 2026, ranked

If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.

Read the guide