DEV Community

Cover image for Building a PDF from Markdown with Pandoc: Images, Code Blocks, TOC and Mermaid
hardyweb
hardyweb

Posted on

Building a PDF from Markdown with Pandoc: Images, Code Blocks, TOC and Mermaid

Pandoc is one of those tools that looks simple at first:

pandoc input.md -o output.pdf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The important point is that image paths are relative to the Markdown file or the working directory used by Pandoc.

For example:

![System Architecture](assets/architecture.png)
Enter fullscreen mode Exit fullscreen mode

If the image is located elsewhere:

docs/
├── assets/
│   └── architecture.png
└── chapters/
    └── deployment.md
Enter fullscreen mode Exit fullscreen mode

then the path from deployment.md would be:

![System Architecture](../assets/architecture.png)
Enter fullscreen mode Exit fullscreen mode

2. Cover Image

A cover image can simply be inserted using Markdown:

![Cover](assets/logo.png)
Enter fullscreen mode Exit fullscreen mode

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"
---
Enter fullscreen mode Exit fullscreen mode

Then:

pandoc main.md \
  --pdf-engine=xelatex \
  -o deployment-guide.pdf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

or the image simply does not appear in the PDF.

Consider:

project/
├── main.md
└── assets/
    └── architecture.png
Enter fullscreen mode Exit fullscreen mode

The Markdown should use:

![Architecture](assets/architecture.png)
Enter fullscreen mode Exit fullscreen mode

Then run Pandoc from the project directory:

pandoc main.md -o output.pdf
Enter fullscreen mode Exit fullscreen mode

A useful debugging technique is to verify the file first:

ls -lh assets/architecture.png
Enter fullscreen mode Exit fullscreen mode

Then test Pandoc:

pandoc main.md -o test.pdf
Enter fullscreen mode Exit fullscreen mode

If the image still does not appear, check the actual working directory:

pwd
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The heading levels included in the TOC can be controlled:

pandoc main.md \
  --toc \
  --toc-depth=2 \
  -o output.pdf
Enter fullscreen mode Exit fullscreen mode

With:

# Deployment Guide

## Installing Dependencies

### Installing PHP

### Installing Composer

## Configuring Nginx
Enter fullscreen mode Exit fullscreen mode

Using:

--toc-depth=2
Enter fullscreen mode Exit fullscreen mode

the TOC will contain:

Deployment Guide
  Installing Dependencies
  Configuring Nginx
Enter fullscreen mode Exit fullscreen mode

but not:

Installing PHP
Installing Composer
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

use a custom LaTeX structure or unnumbered headings:

# Title Page {.unnumbered}
Enter fullscreen mode Exit fullscreen mode

or:

# Copyright {.unnumbered}
Enter fullscreen mode Exit fullscreen mode

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
```
Enter fullscreen mode Exit fullscreen mode

For example:

sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Language identifiers are useful:

```php
Route::get('/health', function () {
    return ['status' => 'ok'];
});
```
Enter fullscreen mode Exit fullscreen mode

Pandoc can then pass the code through syntax highlighting when configured.

For example:

pandoc main.md \
  --highlight-style=tango \
  -o output.pdf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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`.
Enter fullscreen mode Exit fullscreen mode

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
```
Enter fullscreen mode Exit fullscreen mode

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
```
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

with:

flowchart TB
    Client --> API
    API --> Database
Enter fullscreen mode Exit fullscreen mode

Then generate an image:

mmdc \
  -i architecture.mmd \
  -o architecture.png
Enter fullscreen mode Exit fullscreen mode

Now include the generated image:

![System Architecture](assets/architecture.png)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

mmdc \
  -i assets/architecture.mmd \
  -o assets/architecture.png
Enter fullscreen mode Exit fullscreen mode

Then:

pandoc main.md \
  --toc \
  --pdf-engine=xelatex \
  -o output.pdf
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

SVG:

mmdc \
  -i architecture.mmd \
  -o architecture.svg
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For code:

-V monofont="DejaVu Sans Mono"
Enter fullscreen mode Exit fullscreen mode

13. Unicode Problems

Technical documentation often contains symbols such as:

✓
→
⚠
✅
Enter fullscreen mode Exit fullscreen mode

If the selected font does not contain these glyphs, XeLaTeX may display warnings such as:

Missing character
Enter fullscreen mode Exit fullscreen mode

For example:

[WARNING] Missing character: There is no ✅
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

instead of:

✅
⚠️
❌
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Save it as:

build.sh
Enter fullscreen mode Exit fullscreen mode

Then:

chmod +x build.sh
Enter fullscreen mode Exit fullscreen mode

Run:

./build.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And treat external content such as Mermaid diagrams as separate build inputs:

Mermaid
    ↓
PNG/SVG
    ↓
Markdown
    ↓
Pandoc
    ↓
PDF
Enter fullscreen mode Exit fullscreen mode

The key is not finding one giant Pandoc command.

The key is building a reproducible document pipeline that is easy to debug.

Top comments (0)