Convert Markdown files into beautifully styled PDFs using marked, KaTeX and puppeteer.
GitHub-flavored Markdown, tables, code blocks, blockquotes, images and LaTeX math equations ($inline$ and $$display$$) are all rendered with sane, print-friendly defaults — page numbers in the footer, A4 by default, justified body text, and zebra-striped tables.
npm install -g @brahim.ariani/md2pdf-cliOr use it directly in a project:
npm install brahim.ariani/md2pdf-climd2pdf <input.md> [output.pdf] [options]If output.pdf is omitted, the output filename is derived from the input (e.g. research.md → research.pdf).
| Flag | Description |
|---|---|
--title <text> |
Document title (defaults to the input filename) |
--css <file> |
Path to a custom CSS file (replaces the default styles) |
--theme <name> |
Built-in theme: default, academic, latex |
--format <size> |
Page format: A4, Letter, Legal, ... Default: A4 |
--toc |
Prepend an auto-generated table of contents |
--toc-depth <n> |
Deepest heading level included in the TOC. Default: 3 |
--toc-title <text> |
TOC heading text. Default: Contents |
--highlight |
Syntax-highlight fenced code blocks with Shiki |
--code-theme <name> |
Shiki theme for code blocks. Default: github-light |
--mermaid |
Render ```mermaid code blocks as diagrams |
--mermaid-theme <t> |
Mermaid theme: base, default, neutral, dark, forest. Default: base |
--cover |
Render a title page from YAML front matter |
--no-cover |
Never render a title page (overrides front matter) |
--header-logos |
Repeat the cover logos in every page header (skips the cover) |
--no-page-numbers |
Disable the page-number footer |
--no-math |
Disable KaTeX equation rendering |
--no-sanitize |
Disable HTML sanitization (unsafe, see below) |
--keep-html |
Keep the intermediate .tmp.html file for debugging |
-h, --help |
Show usage |
md2pdf research.md
md2pdf research.md out/research.pdf
md2pdf report.md report.pdf --title "Quarterly Report" --css theme.css
md2pdf notes.md notes.pdf --format Letter --no-page-numbers
md2pdf book.md book.pdf --toc --toc-depth 2 --toc-title "Table of Contents"
md2pdf code.md code.pdf --highlight --code-theme github-dark
md2pdf paper.md paper.pdf --cover --toc
md2pdf thesis.md thesis.pdf --theme academic --toc
md2pdf paper.md paper.pdf # equations rendered by default
md2pdf draft.md draft.pdf --no-math # treat $...$ as literal textInline math uses single dollars, display math uses double dollars:
The famous identity is $e^{i\pi} + 1 = 0$.
$$
\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}
$$Equations are rendered server-side with KaTeX, so the PDF is self-contained and prints identically on any machine.
Pass --toc to prepend an auto-generated, clickable table of contents built
from the document headings. Each heading also receives a stable id slug, so
the TOC links resolve as in-document bookmarks.
md2pdf report.md report.pdf --toc # depth 3 (default)
md2pdf report.md report.pdf --toc --toc-depth 2 # only h1 + h2
md2pdf report.md report.pdf --toc --toc-title "Sommaire"The TOC is placed on its own page (it ends with a page break). You can fully
restyle it via --css by targeting nav.toc, nav.toc .toc-title, etc.
Pick a built-in look with --theme:
| Theme | Description |
|---|---|
default |
Clean sans-serif, navy headings, zebra tables (the original look) |
academic |
Georgia serif, justified & indented paragraphs, centered title |
latex |
Classic LaTeX article look: Computer Modern serif, booktabs tables |
md2pdf report.md report.pdf --theme academic
md2pdf thesis.md thesis.pdf --theme latex --toc
md2pdf design.md design.pdf --mermaid --highlight --coverEvery theme keeps the same structural rules (math, TOC, cover page, tables,
page-break safety) and only swaps typography and colors. An unknown theme name
falls back to default with a warning. For full control, --css still
replaces all styles entirely.
Markdown files may start with a YAML front-matter block. It is parsed, stripped from the body, and used to enrich the document:
---
title: Quarterly Report
subtitle: Q2 2026 Financial Overview
author:
- Brahim Ariani
- Finance Team
date: 2026-05-31
version: 2.1.0
cover: true
---
# Introduction
...titlebecomes the HTML document title (a--titleflag still wins).- With
--cover(orcover: truein the front matter), a dedicated title page is rendered fromtitle,subtitle,version,author/authorsanddate, followed by a page break.--no-coverdisables it even if the front matter requests one.
md2pdf paper.md paper.pdf --cover
md2pdf paper.md paper.pdf --cover --toc # title page, then a TOC pageFor full control over the cover layout, design and image placement, use the
object form of cover instead of cover: true:
---
title: Quarterly Report
subtitle: Q2 2026 Financial Overview
version: 2.1.0
author: Brahim Ariani
date: 2026-05-31
cover:
logos: # up to 3 logos, spread across the top
- assets/logo-left.svg
- assets/logo-right.svg
image: assets/hero.png # illustration shown above the title
align: center # vertical placement: top | center | bottom | between
# background: true # use `image` as a full-bleed background instead
---Cover-specific keys (all optional):
| Key | Type | Effect |
|---|---|---|
enabled |
boolean |
Set false to keep the config but skip the cover |
logo |
string |
A single logo (centered above everything else) |
logos |
string[] |
Up to 3 logos laid out as a top row with even spacing |
image |
string |
Path or URL to a cover illustration shown between the logos and title |
background |
boolean |
Render image as a full-bleed background layer behind the text |
align |
string |
Vertical placement: top, center (default), bottom or between |
version |
string |
Version line (also accepted as a top-level version: key) |
Image paths are resolved relative to the Markdown file; remote (https://) and
data: URLs are used as-is. A single logo (or one entry in logos) is
centered; two or three logos are spread across the top with space-between.
Pass --header-logos to repeat the cover logos at the top of every page.
The logos are inlined as data URIs (so they reliably render inside the header)
and aligned with the body margins.
md2pdf paper.md paper.pdf --cover --header-logosWhen a cover page is present alongside a header/footer, the cover is rendered on
its own and merged ahead of the body, so its header and footer are fully
suppressed: the cover carries no page number and no header logos. Page
numbering and the header logos start on the first content page at 1.
Restyle the cover via --css by targeting section.cover (and its
.cover-align-* / .cover-bg modifiers), .cover-content, .cover-logos,
.cover-logo, .cover-image, .cover-bg-image, .cover-title,
.cover-subtitle, .cover-version, .cover-author, .cover-date.
Pass --highlight to colorize fenced code blocks with
Shiki (the same engine that powers VS Code). Colors are
inlined into the HTML, so the PDF stays self-contained and prints identically
everywhere — no client-side JavaScript or web fonts required.
md2pdf code.md code.pdf --highlight
md2pdf code.md code.pdf --highlight --code-theme github-darkOnly the languages actually used in the document are loaded, keeping conversion
fast. Use any Shiki theme name (e.g. github-light, github-dark, nord,
dracula, min-light). Unknown languages fall back to a plain, escaped code
block, and an unknown theme falls back to github-light.
With --mermaid, fenced code blocks tagged mermaid are rendered into vector
diagrams with Mermaid (flowcharts, sequence diagrams,
Gantt charts, etc.):
```mermaid
flowchart LR
A[Start] --> B{OK?}
B -- Yes --> C[Ship]
B -- No --> A
```md2pdf design.md design.pdf --mermaid
md2pdf design.md design.pdf --mermaid --mermaid-theme neutralDiagrams are rendered inside the same headless Chromium used for printing, so
the resulting SVG is embedded directly in the PDF — no network access or extra
tooling required. Mermaid runs with securityLevel: 'strict', and a diagram
with invalid syntax is skipped rather than aborting the whole conversion.
Without --mermaid, ```mermaid blocks are left as plain code.
Markdown allows raw HTML, which means an untrusted .md file can embed
<script>, <iframe>, or event-handler attributes (onerror, onclick, ...).
Because the document is rendered through a real browser (Chromium) before being
printed, such payloads would otherwise execute.
To prevent this, the HTML produced from your Markdown is sanitized by default
with DOMPurify before it ever reaches the
browser. Scripts, event handlers, and dangerous URIs (javascript:, ...) are
stripped, while legitimate content — headings, tables, code blocks, images,
links and KaTeX/MathML/SVG math — is preserved.
If you fully trust the input and need to keep raw HTML (custom <script>,
embeds, etc.), you can opt out:
md2pdf trusted.md trusted.pdf --no-sanitizeawait convert({ input: 'trusted.md', output: 'trusted.pdf', sanitize: false });Only disable sanitization for content you control. Never run
--no-sanitizeon files from untrusted sources.
const { convert } = require('md2pdf-cli');
await convert({
input: 'research.md',
output: 'research.pdf',
title: 'My Research Report',
// cssFile: 'theme.css',
// css: '/* inline CSS string */',
format: 'A4',
pageNumbers: true,
sanitize: true,
toc: true,
tocDepth: 3,
highlight: true,
codeTheme: 'github-light',
});| Option | Type | Default | Description |
|---|---|---|---|
input |
string |
— | Path to a Markdown file (required) |
output |
string |
— | Path to the output PDF (required) |
title |
string |
input basename | <title> of the generated HTML |
theme |
string |
'default' |
Built-in theme: default/academic/latex |
css |
string |
bundled default | Inline CSS string (overrides theme) |
cssFile |
string |
— | Path to a CSS file (overrides css and theme) |
format |
string |
'A4' |
Puppeteer page format |
margin |
object |
22mm / 18mm | { top, bottom, left, right } |
pageNumbers |
boolean |
true |
Render n / total in the footer |
math |
boolean |
true |
Render $...$ and $$...$$ as KaTeX |
sanitize |
boolean |
true |
Sanitize generated HTML (strip scripts/handlers) |
toc |
boolean |
false |
Prepend an auto-generated table of contents |
tocDepth |
number |
3 |
Deepest heading level included in the TOC |
tocTitle |
string |
'Contents' |
TOC heading text |
highlight |
boolean |
false |
Syntax-highlight code blocks with Shiki |
codeTheme |
string |
'github-light' |
Shiki theme name for code blocks |
mermaid |
boolean |
false |
Render mermaid code blocks as diagrams |
mermaidTheme |
string |
'base' |
Mermaid theme name (light by default) |
cover |
boolean |
front matter | Render a title page (true/false overrides YAML) |
headerLogos |
boolean |
false |
Repeat the cover logos in every page header |
headerTemplate |
string |
empty | Puppeteer header HTML |
footerTemplate |
string |
page numbers | Puppeteer footer HTML |
puppeteerOptions |
object |
{} |
Extra options passed to puppeteer.launch |
keepHtml |
boolean |
false |
Keep the intermediate .tmp.html file |
Returns { output, brokenImages } where brokenImages lists any <img> URLs that failed to load.
- Node.js >= 18
- Puppeteer will download a compatible Chromium on install (≈ 170 MB). To skip this and reuse an existing Chrome, set
PUPPETEER_SKIP_DOWNLOAD=truebefore installing and passpuppeteerOptions: { executablePath: '...' }toconvert().
MIT