Pandoc is one of those tools that looks simple at first:
pandoc input.md -o output.pdf
But when Markdown becomes a real document, things become more interesting.
Images may disappear, long shell commands can overflow the page, Mermaid diagrams need additional processing, and the generated table of contents may contain sections that we do not want.
This note documents the practical techniques for building a clean PDF from Markdown using Pandoc.
1. Example Project Structure
Let's use a fictional project called Internal API Deployment Guide.
The directory structure is:
docs/
├── assets/
│ ├── architecture.png
│ └── logo.png
├── chapters/
│ ├── introduction.md
│ ├── installation.md
│ └── deployment.md
└── main.md
The important point is that image paths are relative to the Markdown file or the working directory used by Pandoc.
For example:

If the image is located elsewhere:
docs/
├── assets/
│ └── architecture.png
└── chapters/
└── deployment.md
then the path from deployment.md would be:

2. Cover Image
A cover image can simply be inserted using Markdown:

However, if the intention is to create a proper PDF title page, it is often cleaner to use YAML metadata and a dedicated LaTeX template.
For example:
---
title: "Internal API Deployment Guide"
author: "Example Engineering Team"
date: "2026"
---
Then:
pandoc main.md \
--pdf-engine=xelatex \
-o deployment-guide.pdf
The important lesson is:
Pandoc does not treat a Markdown image as a special "cover page". It is simply an image in the document flow.
If you need a dedicated cover page, control the layout using LaTeX/template mechanisms.
3. Image Paths
One common problem is:
Image not found
or the image simply does not appear in the PDF.
Consider:
project/
├── main.md
└── assets/
└── architecture.png
The Markdown should use:

Then run Pandoc from the project directory:
pandoc main.md -o output.pdf
A useful debugging technique is to verify the file first:
ls -lh assets/architecture.png
Then test Pandoc:
pandoc main.md -o test.pdf
If the image still does not appear, check the actual working directory:
pwd
Relative paths are one of the easiest things to overlook when working with Pandoc.
4. Table of Contents
Pandoc can automatically generate a table of contents:
pandoc main.md \
--toc \
-o output.pdf
The heading levels included in the TOC can be controlled:
pandoc main.md \
--toc \
--toc-depth=2 \
-o output.pdf
With:
# Deployment Guide
## Installing Dependencies
### Installing PHP
### Installing Composer
## Configuring Nginx
Using:
--toc-depth=2
the TOC will contain:
Deployment Guide
Installing Dependencies
Configuring Nginx
but not:
Installing PHP
Installing Composer
This is useful for keeping a technical PDF readable.
5. Removing Unwanted Sections from the TOC
Sometimes the document contains front matter:
# Front Matter
## Title Page
## Copyright
## Disclaimer
# Chapter 1
## Installation
You may not want every front-matter section appearing in the TOC.
One approach is to control the heading hierarchy carefully.
For example, instead of:
# Front Matter
## Title Page
use a custom LaTeX structure or unnumbered headings:
# Title Page {.unnumbered}
or:
# Copyright {.unnumbered}
Depending on the output format and template, this can prevent unwanted numbering and help keep the document structure clean.
6. Code Blocks
Pandoc handles fenced code blocks naturally:
```bash
sudo systemctl restart nginx
```
For example:
sudo systemctl restart nginx
Language identifiers are useful:
```php
Route::get('/health', function () {
return ['status' => 'ok'];
});
```
Pandoc can then pass the code through syntax highlighting when configured.
For example:
pandoc main.md \
--highlight-style=tango \
-o output.pdf
7. The Long Bash Command Problem
Technical documentation often contains commands such as:
docker run --name production-api --restart unless-stopped -p 8080:8080 -v /opt/application/config:/app/config:ro -e APP_ENV=production example/api-server:latest
The problem is that this line may be wider than the printable area of an A4 page.
LaTeX may produce an overfull line.
A better solution is to format the command over multiple lines:
docker run \
--name production-api \
--restart unless-stopped \
-p 8080:8080 \
-v /opt/application/config:/app/config:ro \
-e APP_ENV=production \
example/api-server:latest
This is not only better for PDF generation.
It is also easier for humans to read.
8. Long Inline Commands
Inline code can also cause problems:
Run `docker run --name production-api --restart unless-stopped -p 8080:8080 example/api-server:latest`.
For short commands this is fine.
For long commands, use a fenced block instead:
```bash
docker run \
--name production-api \
--restart unless-stopped \
-p 8080:8080 \
example/api-server:latest
```
A useful documentation rule is:
If a command is difficult to read in Markdown, it will probably be even worse in PDF.
9. Mermaid Diagrams
Pandoc does not natively turn every Mermaid code block into a rendered diagram.
For example:
```mermaid
flowchart TB
Client --> API
API --> Database
```
The Markdown parser can understand the fenced block, but a PDF engine such as XeLaTeX does not automatically know how to render Mermaid.
A common solution is to convert Mermaid separately.
Create:
architecture.mmd
with:
flowchart TB
Client --> API
API --> Database
Then generate an image:
mmdc \
-i architecture.mmd \
-o architecture.png
Now include the generated image:

This separates diagram generation from PDF generation.
10. Mermaid Workflow
A practical workflow is:
Mermaid source
|
v
mmdc
|
v
PNG / SVG
|
v
Markdown
|
v
Pandoc
|
v
PDF
For example:
mmdc \
-i assets/architecture.mmd \
-o assets/architecture.png
Then:
pandoc main.md \
--toc \
--pdf-engine=xelatex \
-o output.pdf
This approach is easier to troubleshoot than trying to make every component work inside one command.
11. PNG vs SVG
Mermaid can generate both PNG and SVG.
PNG:
mmdc \
-i architecture.mmd \
-o architecture.png
SVG:
mmdc \
-i architecture.mmd \
-o architecture.svg
SVG is useful because it is vector-based.
However, the complete toolchain matters.
If SVG rendering causes problems in the LaTeX/PDF pipeline, PNG is often the simpler solution.
For technical documentation, a good starting point is:
Mermaid → PNG → Pandoc → XeLaTeX → PDF
Once the pipeline is stable, SVG can be introduced if higher-quality vector diagrams are required.
12. Using XeLaTeX
For modern technical documents, XeLaTeX is a useful PDF engine.
Example:
pandoc main.md \
--pdf-engine=xelatex \
-o output.pdf
It provides better control over fonts and Unicode than some older LaTeX workflows.
You can specify a main font:
pandoc main.md \
--pdf-engine=xelatex \
-V mainfont="DejaVu Serif" \
-o output.pdf
For code:
-V monofont="DejaVu Sans Mono"
13. Unicode Problems
Technical documentation often contains symbols such as:
✓
→
⚠
✅
If the selected font does not contain these glyphs, XeLaTeX may display warnings such as:
Missing character
For example:
[WARNING] Missing character: There is no ✅
The problem is usually not Pandoc itself.
It is a font coverage problem.
One solution is to use a font with better Unicode coverage.
Another is to avoid unnecessary emoji in PDF source documents.
For technical books, simple ASCII characters are often safer:
[OK]
[WARNING]
[ERROR]
instead of:
✅
⚠️
❌
14. A Simple Build Command
Once the document is ready, a simple build command might be:
pandoc main.md \
--toc \
--toc-depth=2 \
--pdf-engine=xelatex \
--highlight-style=tango \
-o output.pdf
This gives us:
- Markdown input
- automatic TOC
- maximum TOC depth of 2
- XeLaTeX PDF generation
- syntax-highlighted code
- PDF output
15. Separate the Build Process
Instead of remembering a long command, create a script:
#!/usr/bin/env bash
set -e
pandoc main.md \
--toc \
--toc-depth=2 \
--pdf-engine=xelatex \
--highlight-style=tango \
-o output.pdf
echo "PDF generated: output.pdf"
Save it as:
build.sh
Then:
chmod +x build.sh
Run:
./build.sh
This makes the document reproducible.
16. A Better Project Layout
For a larger documentation project, a structure like this works well:
documentation/
├── assets/
│ ├── architecture.png
│ ├── database.png
│ └── logo.png
├── diagrams/
│ ├── architecture.mmd
│ └── database.mmd
├── chapters/
│ ├── introduction.md
│ ├── installation.md
│ └── deployment.md
├── main.md
├── build.sh
└── output/
└── documentation.pdf
The responsibilities are clear:
chapters/ → Markdown content
diagrams/ → Mermaid source
assets/ → generated/static images
main.md → document entry point
build.sh → reproducible build
output/ → generated PDF
17. The Important Lesson
Pandoc itself is only one part of the pipeline.
A real Markdown-to-PDF workflow can look like this:
Markdown
|
+---- Images
|
+---- Code
|
+---- Mermaid
|
v
Pandoc
|
v
XeLaTeX
|
v
PDF
When something goes wrong, debug the pipeline one component at a time.
For example:
Image missing
↓
Check Markdown path
Mermaid missing
↓
Check Mermaid conversion
Unicode warning
↓
Check font
Code overflowing page
↓
Reformat long command
TOC incorrect
↓
Check heading hierarchy
This mindset is much more useful than trying random Pandoc options.
Conclusion
Pandoc is powerful because Markdown can remain the source of truth while the final document can be generated in different formats.
For a reliable technical-documentation workflow, keep the pipeline simple:
Markdown
↓
Pandoc
↓
XeLaTeX
↓
PDF
And treat external content such as Mermaid diagrams as separate build inputs:
Mermaid
↓
PNG/SVG
↓
Markdown
↓
Pandoc
↓
PDF
The key is not finding one giant Pandoc command.
The key is building a reproducible document pipeline that is easy to debug.

