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

> 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.

Source: https://sandboxer.com/guides/convert-documents-with-pandoc-in-a-sandbox. Updated 2026-10-27.

**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](/guides/convert-documents-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](/guides/whats-preinstalled-in-a-sandbox), and Ubuntu 24.04's apt offers pandoc 3.1.3. Install the current release from [pandoc's GitHub releases](https://github.com/jgm/pandoc/releases), as the [pandoc install page](https://pandoc.org/installing.html) describes, and the Typst binary from [Typst's releases](https://github.com/typst/typst/releases):

```bash
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:

```bash
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:

```bash
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](/guides/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]`:

```markdown
---
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`):

```bash
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](https://pandoc.org/MANUAL.html#divs-and-spans) let you mark a block in Markdown, and a [Lua filter](https://pandoc.org/lua-filters.html) 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:

```lua
-- 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
```

```markdown
::: 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:

```bash
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
```

```python
# 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:

```bash
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

```bash
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](/guides/give-a-sandbox-port-an-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](/guides/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](/docs/machines).

## Related

- [Batch convert DOCX to PDF with LibreOffice headless](/guides/convert-documents-with-libreoffice-headless)
- [Move files in and out of a sandbox](/guides/move-files-in-and-out-of-a-sandbox)
- [Turn a sandbox into a reusable template](/guides/turn-a-sandbox-into-a-reusable-template)
- [What's preinstalled in a sandbox](/guides/whats-preinstalled-in-a-sandbox)
