Pandoc 模板引擎实战:开发者自动化文档生成的终极指南

深入解析 Pandoc 模板系统核心原理,手把手教你用 Lua Filter、自定义模板和 CI/CD 流水线构建自动化文档生成管线,附完整可运行代码和多方案对比数据。

开发者效率 2026-05-30 15 分钟

如果你还在手动复制粘贴 Markdown 到 Word、用浏览器打印 PDF、或者为每次报告格式调整浪费半小时——那你需要认识 Pandoc。作为 Hacker News 上 361 分热议的开发者工具,Pandoc 的模板引擎能让你用一条命令把 Markdown 源文件转换成精美的 HTML、PDF、DOCX、EPUB 甚至幻灯片。本文不是 Pandoc 的入门科普,而是从模板系统原理到企业级自动化流水线,带你掌握用代码驱动文档生成的完整技术栈。

📝 一、Pandoc 模板系统核心原理

1.1 为什么开发者需要 Pandoc?

大多数开发者对文档工具的认知停留在「Markdown → HTML」的单向转换。但实际开发中,文档需求远比这复杂:

  • API 文档需要同时输出 HTML(在线浏览)和 PDF(离线阅读)
  • 技术方案需要生成 DOCX 发给产品经理审阅
  • 发布说明需要从 Git commit 自动生成 CHANGELOG
  • 产品报告需要统一的品牌样式(Logo、字体、页眉页脚)

传统做法是维护多份格式文件,或者用 Word 手动排版。Pandoc 的模板系统彻底解决了这个问题——一份 Markdown 源文件 + 一套模板 = 任意格式输出

📌 记住:Pandoc 的核心价值不是「格式转换」,而是内容与样式的分离。你只需专注于写作内容,模板负责控制最终呈现。

1.2 模板变量系统

Pandoc 模板使用 $ 包裹的变量语法,支持条件判断和循环。理解变量系统是掌握模板引擎的第一步:

变量 类型 说明 示例
$title$ string 文档标题 从 YAML metadata 读取
$author$ list 作者列表 支持多作者
$date$ string 日期 默认格式 YYYY-MM-DD
$body$ string 文档正文 Markdown 转换后的内容
$toc$ boolean 是否生成目录 通过 --toc 启用
$meta.title$ string 自定义 metadata YAML frontmatter 中的字段
$if(variable)$ - 条件判断 变量存在时渲染
$for(list)$ - 循环 遍历列表变量

下面是一个最小的 HTML 模板示例:

<!-- 最小 Pandoc HTML 模板:pandoc-minimal.html -->
<!DOCTYPE html>
<html lang="$lang$">
<head>
  <meta charset="utf-8">
  <title>$title$</title>
  $if(css)$
  <link rel="stylesheet" href="$css$">
  $endif$
  $if(math)$
  $math$
  $endif$
</head>
<body>
  $if(toc)$
  <nav id="TOC">
    $toc$
  </nav>
  $endif$
  <main>
    $body$
  </main>
</body>
</html>

使用命令:

# 用自定义模板将 Markdown 转为 HTML
pandoc input.md \
  --template=pandoc-minimal.html \
  --toc \
  --metadata title="API 文档 v2.0" \
  -o output.html

💡 提示:--template 参数指定模板文件路径,模板中的 $body$ 会被替换为 Markdown 转换后的 HTML 内容。所有 YAML frontmatter 中的字段都可以通过 $meta.fieldname$ 访问。

1.3 Metadata(元数据)系统

Pandoc 支持通过 YAML frontmatter 注入自定义元数据,这些元数据在模板中可以自由引用:

# Markdown 文件的 YAML frontmatter 示例
---
title: "用户管理系统 API 文档"
version: "2.1.0"
author:
  - name: "张三"
    email: "zhangsan@example.com"
  - name: "李四"
    email: "lisi@example.com"
date: "2026-05-31"
lang: "zh-CN"
company: "示例科技有限公司"
logo: "./assets/logo.png"
header-includes:
  - \usepackage{fancyhdr}
  - \pagestyle{fancy}
---

在模板中这样使用:

<!-- 引用自定义 metadata 的模板片段 -->
<header>
  $if(logo)$
  <img src="$logo$" alt="$company$ logo" class="logo">
  $endif$
  <h1>$title$</h1>
  <p class="version">版本:$version$</p>
  <div class="authors">
    $for(author)$
    <span>$author.name$ &lt;$author.email$&gt;</span>
    $endfor$
  </div>
  <time>$date$</time>
</header>

⚡ **关键结论:**YAML frontmatter 是 Pandoc 模板系统的「数据层」。把所有可变信息都放在 frontmatter 中,模板只负责渲染,这是实现「一份源文件、多种输出格式」的核心设计模式。

🔧 二、自定义模板实战:从 HTML 到 PDF

2.1 企业级 HTML 模板

下面是一个生产可用的 HTML 文档模板,包含响应式布局、代码高亮、目录导航等特性:

<!-- 企业级 Pandoc HTML 模板:enterprise.html -->
<!DOCTYPE html>
<html lang="$lang$">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>$title$ — $company$</title>
  <style>
    :root {
      --primary: #2563eb;
      --bg: #ffffff;
      --text: #1f2937;
      --code-bg: #f3f4f6;
      --border: #e5e7eb;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, "Noto Sans SC", sans-serif;
      max-width: 900px; margin: 0 auto;
      padding: 2rem; color: var(--text);
      line-height: 1.8;
    }
    header { border-bottom: 2px solid var(--primary); padding-bottom: 1rem; margin-bottom: 2rem; }
    header h1 { color: var(--primary); font-size: 1.8rem; }
    .meta { color: #6b7280; font-size: 0.9rem; margin-top: 0.5rem; }
    nav#TOC {
      background: var(--code-bg); border-radius: 8px;
      padding: 1.5rem; margin-bottom: 2rem;
    }
    nav#TOC ul { list-style: none; padding-left: 1.2rem; }
    nav#TOC > ul { padding-left: 0; }
    nav#TOC a { color: var(--primary); text-decoration: none; }
    nav#TOC a:hover { text-decoration: underline; }
    h2 { color: var(--primary); margin: 2rem 0 1rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
    h3 { margin: 1.5rem 0 0.75rem; }
    pre {
      background: var(--code-bg); border-radius: 6px;
      padding: 1rem; overflow-x: auto; margin: 1rem 0;
    }
    code { font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 0.9em; }
    :not(pre) > code { background: var(--code-bg); padding: 0.2em 0.4em; border-radius: 3px; }
    table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
    th, td { border: 1px solid var(--border); padding: 0.6rem 1rem; text-align: left; }
    th { background: var(--code-bg); font-weight: 600; }
    blockquote { border-left: 4px solid var(--primary); padding: 0.5rem 1rem; margin: 1rem 0; background: #eff6ff; }
    @media print {
      body { max-width: 100%; }
      nav#TOC { break-after: page; }
      pre { white-space: pre-wrap; word-wrap: break-word; }
    }
    $if(highlighting-css)$
    $highlighting-css$
    $endif$
  </style>
  $for(header-includes)$
  $header-includes$
  $endfor$
</head>
<body>
  <header>
    <h1>$title$</h1>
    <div class="meta">
      $if(version)$版本 $version$ · $endif$
      $for(author)$$author.name$$sep$, $endfor$ · $date$
      $if(company)$ · $company$$endif$
    </div>
  </header>
  $if(toc)$
  <nav id="TOC">
    <h2>目录</h2>
    $toc$
  </nav>
  $endif$
  <main>
    $body$
  </main>
  <footer style="margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: #9ca3af; font-size: 0.85rem;">
    由 Pandoc 自动生成 · $date$
  </footer>
</body>
</html>

使用方式:

# 生成带目录的 HTML 文档
pandoc api-doc.md \
  --template=enterprise.html \
  --toc \
  --toc-depth=3 \
  --highlight-style=tango \
  --metadata company="示例科技" \
  -o api-doc.html

2.2 PDF 模板(通过 LaTeX)

Pandoc 生成 PDF 的核心是通过 LaTeX 引擎。中文文档需要特别配置字体和编码:

% 企业级 Pandoc PDF 模板:enterprise-pdf.tex
\documentclass[$if(fontsize)$$fontsize$$else$12pt$endif$]{article}

% === 中文支持 ===
\usepackage[UTF8]{ctex}
\usepackage{fontspec}
\setmainfont{Noto Serif CJK SC}
\setsansfont{Noto Sans CJK SC}
\setmonofont{JetBrains Mono}[Scale=0.85]

% === 页面布局 ===
\usepackage[a4paper, margin=2.5cm]{geometry}
\usepackage{fancyhdr}
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{$if(company)$$company$$endif$}
\fancyhead[R]{$title$}
\fancyfoot[C]{\thepage}
\renewcommand{\headrulewidth}{0.4pt}

% === 样式 ===
\usepackage{xcolor}
\definecolor{primary}{RGB}{37, 99, 235}
\usepackage{hyperref}
\hypersetup{colorlinks=true, linkcolor=primary, urlcolor=primary}
\usepackage{booktabs}  % 更好看的表格
\usepackage{listings}  % 代码块
\lstset{
  basicstyle=\small\ttfamily,
  backgroundcolor=\color{gray!10},
  frame=single,
  breaklines=true
}

% === 标题信息 ===
\title{\color{primary}\Large\bfseries $title$}
\author{$for(author)$$author.name$ \\ \small $author.email$$sep$ \and $endfor$}
\date{$date$}

\begin{document}
$if(highlighting-macros)$
$highlighting-macros$
$endif$

\maketitle
$if(toc)$
\tableofcontents
\newpage
$endif$

$body$

\end{document}

生成 PDF 的命令:

# 用 XeLaTeX 引擎生成中文 PDF(推荐)
pandoc api-doc.md \
  --template=enterprise-pdf.tex \
  --pdf-engine=xelatex \
  --toc \
  --highlight-style=tango \
  -V fontsize=11pt \
  -o api-doc.pdf

⚠️ **警告:**生成中文 PDF 必须使用 xelatexlualatex 引擎,pdflatex 不支持中文。同时确保系统安装了中文字体(如 Noto CJK 系列)。Ubuntu/Debian 下执行 sudo apt install texlive-xetex fonts-noto-cjk

2.3 多格式输出对比

输出格式 模板类型 引擎 适用场景 复杂度
HTML .html 模板 Pandoc 内置 在线文档、博客 ⭐ 低
PDF .tex LaTeX 模板 XeLaTeX 正式报告、合同 ⭐⭐⭐ 高
DOCX 参考文档 .docx Pandoc 内置 协作审阅、交付 ⭐⭐ 中
EPUB .html 模板 + CSS Pandoc 内置 电子书 ⭐⭐ 中
幻灯片 .html reveal.js Pandoc 内置 技术分享 ⭐⭐ 中

DOCX 格式的模板比较特殊——它使用一个参考文档(reference doc)而非文本模板:

# 生成参考文档(只需执行一次)
pandoc -o custom-reference.docx --print-default-data-file reference.docx

# 用 Word 打开 custom-reference.docx 修改样式
# 然后用它作为模板
pandoc input.md --reference-doc=custom-reference.docx -o output.docx

🚀 三、Lua Filter 与自动化流水线

3.1 Lua Filter:Pandoc 的「插件系统」

Lua Filter 是 Pandoc 最强大的扩展机制——它允许你在转换过程中拦截和修改文档的 AST(抽象语法树)。这比简单的模板变量灵活得多。

场景一:自动为外部链接添加图标和 target 属性

-- filter: external-links.lua
-- 自动为外部链接添加 target="_blank" 和 CSS class
function Link(el)
  if el.target:match("^https?://") then
    el.attributes["target"] = "_blank"
    el.attributes["rel"] = "noopener noreferrer"
    el.classes:insert("external-link")
  end
  return el
end
# 使用 Lua Filter
pandoc input.md --lua-filter=external-links.lua -o output.html

场景二:自动为代码块添加行号和复制按钮

-- filter: code-enhance.lua
-- 为代码块添加行号和复制按钮(HTML 输出)
local code_block_count = 0

function CodeBlock(block)
  code_block_count = code_block_count + 1
  local id = "code-block-" .. code_block_count
  local lang = block.classes[1] or "text"

  local wrapper = '<div class="code-block" id="' .. id .. '">'
  wrapper = wrapper .. '<div class="code-header">'
  wrapper = wrapper .. '<span class="lang">' .. lang .. '</span>'
  wrapper = wrapper .. '<button onclick="copyCode(\'' .. id .. '\')">复制</button>'
  wrapper = wrapper .. '</div>'
  wrapper = wrapper .. '<pre><code class="language-' .. lang .. '">'
  wrapper = wrapper .. block.text
  wrapper = wrapper .. '</code></pre></div>'

  return pandoc.RawBlock("html", wrapper)
end

场景三:自动生成 API 端点表格

-- filter: api-table.lua
-- 从特殊的代码块自动生成 API 端点表格
-- 使用方法:在 Markdown 中写 ```api-endpoint 代码块
function CodeBlock(block)
  if block.classes[1] == "api-endpoint" then
    local rows = {}
    for line in block.text:gmatch("[^\n]+") do
      local method, path, desc = line:match("^(%u+)%s+(%S+)%s+(.+)$")
      if method then
        table.insert(rows, {method, path, desc})
      end
    end

    local header = pandoc.Row({
      pandoc.Cell({pandoc.Plain({pandoc.Strong(pandoc.Str("方法"))})}),
      pandoc.Cell({pandoc.Plain({pandoc.Strong(pandoc.Str("路径"))})}),
      pandoc.Cell({pandoc.Plain({pandoc.Strong(pandoc.Str("说明"))})}),
    })
    local body_rows = {}
    for _, row in ipairs(rows) do
      table.insert(body_rows, pandoc.Row({
        pandoc.Cell({pandoc.Plain({pandoc.Str(row[1])})}),
        pandoc.Cell({pandoc.Plain({pandoc.Code(row[2])})}),
        pandoc.Cell({pandoc.Plain({pandoc.Str(row[3])})}),
      }))
    end

    local tbl = pandoc.Table(
      pandoc.Caption({pandoc.Plain({pandoc.Str("API 端点列表")})}),
      {pandoc.AlignLeft, pandoc.AlignLeft, pandoc.AlignLeft},
      pandoc.TableHead({header}),
      {pandoc.TableBody(body_rows)},
      pandoc.TableFoot()
    )
    return tbl
  end
end

在 Markdown 中这样使用:

```api-endpoint
GET    /api/users     获取用户列表
POST   /api/users     创建新用户
GET    /api/users/:id 获取单个用户
PUT    /api/users/:id 更新用户信息
DELETE /api/users/:id 删除用户
```

这会自动渲染为一个格式化的表格,无需手动维护 Markdown 表格语法。

💡 **提示:**Lua Filter 可以组合使用。--lua-filter=a.lua --lua-filter=b.lua 会按顺序执行多个 Filter,每个 Filter 处理上一个 Filter 的输出。建议将不同功能拆分为独立的 Filter 文件。

3.2 CI/CD 自动化流水线

将 Pandoc 集成到 CI/CD 中,实现文档的自动构建和发布。以下是一个完整的 GitHub Actions 配置:

# .github/workflows/docs.yml
# 文档自动构建和发布流水线
name: Build Docs

on:
  push:
    branches: [main]
    paths: ['docs/**']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 安装 Pandoc 和 LaTeX
        run: |
          sudo apt-get update
          sudo apt-get install -y pandoc texlive-xetex fonts-noto-cjk

      - name: 安装 Pandoc Filters
        run: |
          # 安装 pandoc-crossref(交叉引用)
          wget https://github.com/lierdakil/pandoc-crossref/releases/latest/download/pandoc-crossref-Linux-XeLaTeX.tar.xz
          tar xf pandoc-crossref-Linux-XeLaTeX.tar.xz
          sudo mv pandoc-crossref /usr/local/bin/

      - name: 构建 HTML 文档
        run: |
          for f in docs/*.md; do
            name=$(basename "$f" .md)
            pandoc "$f" \
              --template=templates/enterprise.html \
              --lua-filter=filters/external-links.lua \
              --lua-filter=filters/code-enhance.lua \
              --toc --toc-depth=3 \
              --highlight-style=tango \
              --metadata date="$(date +%Y-%m-%d)" \
              -o "dist/${name}.html"
          done

      - name: 构建 PDF 文档
        run: |
          for f in docs/*.md; do
            name=$(basename "$f" .md)
            pandoc "$f" \
              --template=templates/enterprise-pdf.tex \
              --pdf-engine=xelatex \
              --toc \
              --highlight-style=tango \
              -o "dist/${name}.pdf"
          done

      - name: 部署到 GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

3.3 Makefile 封装:一条命令搞定一切

# Makefile
# 文档构建自动化:make html / make pdf / make all

SOURCES := $(wildcard docs/*.md)
HTML_OUTPUT := $(patsubst docs/%.md, dist/%.html, $(SOURCES))
PDF_OUTPUT := $(patsubst docs/%.md, dist/%.pdf, $(SOURCES))

TEMPLATE_HTML := templates/enterprise.html
TEMPLATE_PDF := templates/enterprise-pdf.tex
FILTERS := --lua-filter=filters/external-links.lua --lua-filter=filters/code-enhance.lua

.PHONY: all html pdf clean

all: html pdf

html: $(HTML_OUTPUT)

pdf: $(PDF_OUTPUT)

dist/%.html: docs/%.md $(TEMPLATE_HTML) $(wildcard filters/*.lua)
	@mkdir -p dist
	pandoc $< \
		--template=$(TEMPLATE_HTML) \
		$(FILTERS) \
		--toc --toc-depth=3 \
		--highlight-style=tango \
		--metadata date="$$(date +%Y-%m-%d)" \
		-o $@
	@echo "✅ 生成 HTML: $@"

dist/%.pdf: docs/%.md $(TEMPLATE_PDF)
	@mkdir -p dist
	pandoc $< \
		--template=$(TEMPLATE_PDF) \
		--pdf-engine=xelatex \
		--toc \
		--highlight-style=tango \
		-o $@
	@echo "✅ 生成 PDF: $@"

clean:
	rm -rf dist/

使用方式:

make all      # 构建所有格式
make html     # 只构建 HTML
make pdf      # 只构建 PDF
make clean    # 清理输出

关键结论:Pandoc + Make + CI/CD 的组合是目前最成熟的文档自动化方案。相比 MkDocs、Docusaurus 等专用文档工具,Pandoc 的优势在于输出格式不受限——同一份源文件可以输出 HTML、PDF、DOCX、EPUB、reveal.js 幻灯片,而专用工具通常只支持 HTML。

📊 四、Pandoc vs 其他文档工具对比

特性 Pandoc MkDocs Docusaurus VitePress
输入格式 Markdown/LaTeX/DOCX/… Markdown MDX Markdown
输出格式 HTML/PDF/DOCX/EPUB/幻灯片 HTML HTML HTML
PDF 输出 ✅ 原生支持 ❌ 需插件 ❌ 不支持 ❌ 不支持
DOCX 输出 ✅ 原生支持 ❌ 不支持 ❌ 不支持 ❌ 不支持
模板系统 ✅ 完整的模板引擎 Jinja2 React 组件 Vue 组件
可编程性 ✅ Lua Filter Python 插件 JS 插件 Vue 插件
搜索 ❌ 需外部方案 ✅ 内置 ✅ 内置 ✅ 内置
版本控制 ✅ 纯文本 ✅ 纯文本 ✅ 纯文本 ✅ 纯文本
学习曲线 ⭐⭐⭐ 高 ⭐ 低 ⭐⭐ 中 ⭐⭐ 中
适用场景 多格式文档、报告、论文 项目文档 技术文档站 技术文档站

💡 **提示:**Pandoc 和专用文档工具不是互斥的。很多团队用 Pandoc 处理需要 PDF/DOCX 输出的正式文档,用 VitePress/MkDocs 搭建在线文档站。两者可以共享同一套 Markdown 源文件。

⚠️ 五、避坑指南与最佳实践

5.1 常见坑点

  • 直接用 pandoc 生成 PDF 却不指定 --pdf-engine=xelatex — 默认的 pdflatex 不支持中文,会报编码错误
  • 模板中使用未定义的变量 — Pandoc 不会报错,只是静默输出空字符串,导致页面出现空白
  • 在 YAML frontmatter 中使用复杂 Markdown — 模板变量不会二次解析 Markdown,需要在模板中用 $rawAttribute$ 处理
  • 忽略 --standalone 参数 — 不加此参数时 Pandoc 只输出 body 内容,不包含完整的 HTML 文档结构

5.2 最佳实践

  • 版本锁定 Pandoc — 不同版本的 Pandoc 模板语法可能有差异,在 CI/CD 中固定版本(pandoc --version
  • 模板继承 — 用 $--include-before$$--include-after$ 实现模板片段复用
  • 增量构建 — 用 Make 的依赖检查,只重新构建修改过的文件
  • 测试模板 — 为模板编写测试用例,确保变量缺失时有合理的默认值
# 锁定 Pandoc 版本的安装方式
# macOS
brew install pandoc@3.6.4

# Ubuntu/Debian(下载特定版本 .deb)
wget https://github.com/jgm/pandoc/releases/download/3.6.4/pandoc-3.6.4-1-amd64.deb
sudo dpkg -i pandoc-3.6.4-1-amd64.deb

🎯 总结

Pandoc 模板系统的核心价值可以用一句话概括:内容与表现的彻底分离。开发者只需要维护一份 Markdown 源文件,通过模板和 Filter 的组合,可以生成任意格式、任意样式的文档输出。

实施路线图:

  1. **第 1 天:**安装 Pandoc,用默认模板生成 HTML 和 PDF,熟悉基本流程
  2. **第 3 天:**创建自定义 HTML 模板,加入品牌样式和目录导航
  3. **第 1 周:**编写 Lua Filter 处理自定义语法(如 API 端点表格)
  4. **第 2 周:**配置 CI/CD 流水线,实现文档自动构建和发布

📌 记住:不要一开始就追求完美的模板。先用 Pandoc 默认模板跑通流程,再逐步定制样式。模板系统的价值在于渐进式优化——你可以随时修改模板而不影响内容源文件。

相关工具推荐:

  • Pandoc — 文档转换的「瑞士军刀」,支持 40+ 种格式互转
  • Pandoc Lua Filters 官方仓库github.com/pandoc/lua-filters,大量现成的 Filter
  • Quarto — 基于 Pandoc 的新一代科学出版系统,适合数据报告
  • DocToc — 自动生成 Markdown 目录的 CLI 工具
  • GitHub Actions for Pandoc — 预配置的 CI/CD 模板

📚 相关文章