mdtopdf is a small, configurable Markdown-to-PDF CLI built on top of
Pandoc. It is intended for repeatable document generation
from plain Markdown, with optional fonts, logos, LaTeX header includes, and
Pandoc defaults files.
The tool does not hardcode any company branding. Letterheads, logos, fonts, and document styling are supplied through CLI flags or JSON config files.
- Node.js 20 or newer
pandoconPATH- A Pandoc PDF engine, usually
xelatex
Check your environment:
node src/cli.js --checkExpected output looks like:
ok pandoc: pandoc 3.x
ok xelatex: XeTeX 3.x
From this repository:
npm install
node src/cli.js --helpUse as a local binary from another project:
npm install --save-dev /path/to/mdtopdf
npx mdtopdf docs/contract.md -o out/contract.pdfBuild a standalone binary with Bun:
bun run build
./dist/mdtopdf --helpConvert one Markdown file. If -o is omitted for a single input file, the
output path is inferred from the input filename.
node src/cli.js docs/contract.md
# writes docs/contract.pdfConvert multiple Markdown files into one PDF:
node src/cli.js docs/01-intro.md docs/02-terms.md -o out/document.pdfUse a logo on every page:
node src/cli.js docs/contract.md \
-o out/contract.pdf \
--preset letterhead \
--logo assets/logo.png \
--logo-all-pages \
--logo-height 1.2cm \
--headheight 44pt \
--headsep 20ptUse local font files without installing fonts system-wide:
node src/cli.js docs/contract.md \
-o out/contract.pdf \
--font-family DocumentFont \
--font-dir fonts \
--font-regular NotoSerif-Regular.ttf \
--font-bold NotoSerif-Bold.ttf \
--font-italic NotoSerif-Italic.ttf \
--font-bold-italic NotoSerif-BoldItalic.ttfPreview the Pandoc command without generating a PDF:
node src/cli.js docs/contract.md --dry-runConfiguration can come from three places. Later sources override earlier ones:
- Preset defaults
- JSON config file passed with
--config - CLI flags
Example:
{
"preset": "letterhead",
"out": "out/contract.pdf",
"logo": "assets/logo.png",
"logoAllPages": true,
"logoHeight": "1.2cm",
"headheight": "44pt",
"headsep": "20pt",
"fontFamily": "DocumentFont",
"fontDir": "fonts",
"fontRegular": "NotoSerif-Regular.ttf",
"fontBold": "NotoSerif-Bold.ttf"
}Run it:
node src/cli.js --config contract.config.json docs/contract.mdPaths inside config files are resolved relative to the config file. For font
files, use fontDir plus file names when the files live together.
See:
examples/letterhead.config.jsonexamples/font-files.config.jsondocs/configuration.md
Presets are intentionally generic:
| Preset | Purpose |
|---|---|
default |
Generic A4 PDF, 2.5cm margins, page numbers on, no branding |
plain |
Similar to default with slightly looser line height |
letterhead |
Generic letterhead-ready layout; no logo unless --logo is provided |
company |
Backward-compatible alias-style preset for older workflows; still no hardcoded branding |
Presets live in src/presets.js and can be edited or extended for a local
project. Keep project-specific logos and fonts in config files rather than in
the presets.
Core output:
-o, --out <file>: output PDF path--config <file>: JSON config file--preset <name>:default,plain,letterhead, orcompany--dry-run: print the Pandoc command and exit--verbose: print the Pandoc command before running--check: check external dependencies and exit
Pandoc and page layout:
--pdf-engine <name>: Pandoc PDF engine, e.g.xelatex--papersize <size>:a4,letter, etc.--margin <value>: shorthand page margin, e.g.2.5cm--geometry <value>: raw LaTeX geometry string; overrides--margin--fontsize <size>: base font size, e.g.11pt--lineheight <value>: line spacing multiplier--page-numbers/--no-page-numbers: toggle page numbers--defaults <file>: Pandoc defaults file--metadata <file...>: metadata files passed to Pandoc--include <file...>: LaTeX header includes passed to Pandoc
Installed fonts:
--font <name>: installed main font name--mono <name>: installed monospace font name
Font files:
--font-family <name>: family label used byfontspec--font-dir <dir>: directory containing font files--font-regular <file>: regular font file--font-bold <file>: bold font file--font-italic <file>: italic font file--font-bold-italic <file>: bold italic font file
Logo header:
--logo <path>: logo image path--logo-height <value>: logo height, e.g.1.2cm--logo-all-pages: apply logo to every page--headheight <value>: LaTeX header height--headsep <value>: space between header and body
CJK helpers:
--cjk/--no-cjk: define CJK helper font families in LaTeX--cjk-cn <name>: Chinese CJK font--cjk-jp <name>: Japanese CJK font--cjk-kr <name>: Korean CJK font
You can use normal Pandoc defaults files through --defaults:
node src/cli.js docs/contract.md --defaults templates/default.defaults.yamlTemplates included here:
templates/default.defaults.yamltemplates/letterhead.defaults.yamltemplates/company.defaults.yamlfor older workflows
Pandoc supports raw LaTeX in Markdown when targeting PDF. This is useful for page breaks:
\newpage
## Schedule AAvoid standalone lines made entirely of underscores when you want signature fields. Pandoc may interpret them as horizontal rules. Prefer labelled fields:
Signature: _____________________
Date: _________________________pandoc not found on PATH
: Install Pandoc and verify with pandoc --version.
xelatex not found
: Install a TeX distribution such as MacTeX, TeX Live, or MiKTeX. Or choose a
different engine with --pdf-engine.
Logo overlaps body text
: Increase --headheight and --headsep.
Font not found
: Use --font only for fonts installed system-wide. For portable output, use
--font-dir and font file options.
Title or words hyphenate awkwardly
: Add a small LaTeX include file with project-specific typography rules and pass
it with --include. Keep that file in the consuming project rather than baking
the rule into this tool.
Run tests:
npm testRun the CLI locally:
node src/cli.js --help
node src/cli.js --checkBuild standalone binary:
bun run buildThe implementation is dependency-light by design: Commander for CLI parsing, Node.js built-ins for everything else, and Pandoc for document conversion.