Converting Markdown to PDF for Printing
Findings from evaluating ways to convert this PKB's markdown notes into nicely printed PDFs on Manjaro. The chosen route (Pandoc + XeLaTeX) has a reusable defaults file and a thin md-to-pdf wrapper script so any single note can be turned into an A4 PDF with one command.
Tech Stack: Pandoc + XeLaTeX¶
Pandoc converts markdown to LaTeX, and XeLaTeX renders it to PDF. XeLaTeX was preferred over the lighter Tectonic engine specifically because it can select fonts already installed on the system (via fontspec), giving control over body, sans and monospace typefaces without shipping LaTeX font packages.
Installed Packages¶
sudo pacman -S --needed pandoc-cli texlive-latex \
texlive-latexrecommended texlive-latexextra \
texlive-fontsrecommended texlive-xetex
Notes from the install:
- The pandoc package is
pandoc-cli, notpandoc. texlive-xetexis required and easy to miss — without it, XeLaTeX errors withI can't find the format file 'xelatex.fmt'!even though thexelatexbinary exists. Installing the package triggers thetexlivepost-transaction hook that builds the format file.
Configuration¶
Pandoc supports defaults files — reusable YAML that bundles engine, layout and typography settings so they need not be repeated on the command line. The active file lives at:
~/.local/share/pandoc/defaults/md_to_pdf.yaml
Current md_to_pdf.yaml:
pdf-engine: xelatex
from: markdown
standalone: true
toc: true
toc-depth: 3
number-sections: false
highlight-style: tango
variables:
documentclass: article
papersize: a4
geometry:
- margin=2.2cm
fontsize: 11pt
linestretch: 1.15
colorlinks: true
linkcolor: RoyalBlue
urlcolor: RoyalBlue
mainfont: "Noto Serif"
sansfont: "Noto Sans"
monofont: "DejaVu Sans Mono"
monofontoptions:
- Scale=0.85
wrap: auto
Everything here is tunable:
- Fonts:
mainfont/sansfont/monofontaccept any family listed byfc-list. The fonts above were verified present on this install.monofontoptions: Scale=0.85shrinks code so long lines and URLs fit the text width. - Page:
papersize,geometry(margins),fontsize,linestretch. - Structure:
toc,toc-depth,number-sections. - Code highlighting:
highlight-style(e.g.tango,kate,pygments,zenburn). - Links:
colorlinkswithlinkcolor/urlcolor.
Wrapper Script¶
A small wrapper at ~/.local/bin/md-to-pdf (on $PATH) is the day-to-day interface. It takes the input note and an optional output name, validates the toolchain and defaults file up front, runs pandoc, and prints only Wrote <file>.pdf on success.
#!/usr/bin/env bash
set -euo pipefail
DEFAULTS_FILE="${HOME}/.local/share/pandoc/defaults/md_to_pdf.yaml"
die() {
printf 'md-to-pdf: %s\n' "$1" >&2
exit 1
}
usage() {
printf 'Usage: md-to-pdf INPUT.md [OUTPUT.pdf] [-- PANDOC_ARGS...]\n' >&2
exit 2
}
positional=()
passthrough=()
while [ "$#" -gt 0 ]; do
if [ "$1" = "--" ]; then
shift
passthrough=("$@")
break
fi
positional+=("$1")
shift
done
[ "${#positional[@]}" -ge 1 ] && [ "${#positional[@]}" -le 2 ] || usage
input="${positional[0]}"
[ -f "$input" ] || die "input file not found: ${input}"
if [ "${#positional[@]}" -eq 2 ]; then
output="${positional[1]}"
else
output="${input%.*}.pdf"
fi
command -v pandoc >/dev/null 2>&1 || die "pandoc not found on PATH (install: pacman -S pandoc-cli)"
command -v xelatex >/dev/null 2>&1 || die "xelatex not found on PATH (install: pacman -S texlive-xetex)"
[ -f "$DEFAULTS_FILE" ] || die "pandoc defaults file missing: ${DEFAULTS_FILE}"
pandoc "$input" -d "$DEFAULTS_FILE" -o "$output" ${passthrough[@]+"${passthrough[@]}"}
printf 'Wrote %s\n' "$output"
Behaviour:
- Output name is optional.
md-to-pdf note.mdwritesnote.pdfalongside the input (derived by swapping the extension);md-to-pdf note.md /tmp/out.pdfwrites to the given path. - Passthrough after
--. Anything following a--separator is forwarded verbatim to pandoc, appended after the wrapper's own flags (so a user flag wins on conflict). This exposes pandoc's full option set without the wrapper needing to know about each flag. - Preflight checks fail fast with a clear message if
pandoc,xelatexor the defaults file is missing, each pointing at the fix. - Quiet on success — a single
Wrote <path>line, nothing else. - Exit codes:
0success,1a missing file/tool,2wrong argument count (usage).
The ${passthrough[@]+"${passthrough[@]}"} expansion is the safe way to expand a possibly-empty array under set -u; a plain "${passthrough[@]}" errors on an empty array in older bash.
Usage¶
# Deduced output name (writes note.pdf next to the input)
md-to-pdf docs/done/20260711-ssh-key-unlocking-plasma.md
# Explicit output path
md-to-pdf note.md /tmp/note.pdf
# Override a defaults setting for one run, via passthrough
md-to-pdf note.md -- --toc=false
# Explicit output plus passthrough
md-to-pdf note.md /tmp/note.pdf -- -V mainfont="TeX Gyre Termes"
Note the override syntax: toc is a top-level setting in the defaults file, so it is switched off with the --toc=false flag, not -V toc=false. The -V key=value form sets template variables (e.g. mainfont, fontsize, geometry), which is a different namespace — using -V on a top-level setting silently does nothing.
To bypass the wrapper entirely and drive pandoc directly:
pandoc note.md -d md_to_pdf -o note.pdf --toc=false
Per-document overrides are also possible via YAML frontmatter in the note itself.
Trade-offs and Gotchas¶
- Long unbreakable strings (very long URLs inside code blocks) can still overflow the right margin — a LaTeX limitation, not a config fault. The
Scale=0.85monospace setting mitigates it; manual line breaks fix the rest. - First run is slow. The initial XeLaTeX invocation builds format files and caches; subsequent conversions are fast.
Other Options Considered¶
All packages below are in the official Arch/Manjaro repositories unless noted.
| Option | Install | Print quality | Configurability | Notes |
|---|---|---|---|---|
| Pandoc + LaTeX | pandoc-cli + texlive-* (~1-2 GB) |
Excellent | Excellent | Gold standard typography; heaviest install |
| Pandoc + Tectonic | pandoc-cli + tectonic (~200 MB) |
Excellent | Excellent | Same output, self-contained LaTeX engine that fetches packages on demand; no system-font selection |
| Pandoc + XeLaTeX | pandoc-cli + texlive-latex* + texlive-xetex |
Excellent | Excellent | Chosen — LaTeX quality plus system fonts via fontspec |
| Typst | typst (~30 MB) |
Very good | Good | Modern, fast, tiny; younger ecosystem; pair with pandoc via --pdf-engine=typst |
| WeasyPrint | python-weasyprint (~100 MB) |
Very good | Good (CSS @page) |
MD→HTML→PDF; style with plain CSS; two-step pipeline |
| md-to-pdf / Chromium | npm i -g md-to-pdf (Chromium already present) |
Good | Moderate | Zero-effort; Chromium's print engine less precise on page breaks |
References¶
Backlinks¶
No other pages link to this page.