Skip to content

PDF 导出与离线文档

将 VitePress 文档导出为 PDF 或生成离线可访问的文档,方便用户在没有网络的环境下阅读。

方案对比

方案优点缺点适用场景
Puppeteer渲染精确、支持复杂样式需要无头浏览器、速度慢需要精确还原页面样式
Prince XML专业排版、支持分页商业软件专业出版物
md-to-pdf简单快捷样式还原度低简单文档
PWA 离线自动更新、原生体验仍需浏览器离线阅读首选
HTML 离线包无需服务器、可直接打开交互受限完全离线分发

方案一:使用 Puppeteer 导出 PDF

安装依赖

bash
npm install -D puppeteer

创建导出脚本

typescript
// scripts/export-pdf.mts
import puppeteer from 'puppeteer'
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'

interface PdfOptions {
  url: string
  output: string
  format?: 'A4' | 'Letter'
  margin?: { top: string; right: string; bottom: string; left: string }
  printBackground?: boolean
}

async function exportPdf(options: PdfOptions) {
  const {
    url,
    output,
    format = 'A4',
    margin = { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
    printBackground = true
  } = options

  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  })

  const page = await browser.newPage()

  await page.goto(url, {
    waitUntil: 'networkidle0',
    timeout: 60000
  })

  // 等待页面完全渲染
  await page.waitForSelector('.VPContent', { timeout: 10000 })

  // 隐藏不需要的元素
  await page.addStyleTag({
    content: `
      .VPNav,
      .VPSidebar,
      .VPFooter,
      .back-to-top,
      .aside {
        display: none !important;
      }
      .VPContent {
        padding: 0 !important;
      }
    `
  })

  await page.pdf({
    path: output,
    format,
    margin,
    printBackground
  })

  await browser.close()
  console.log(`✅ PDF 已导出: ${output}`)
}

// 批量导出
const pages = [
  { url: 'http://localhost:5173/guide/what-is-vitepress', output: 'pdf/guide/what-is-vitepress.pdf' },
  { url: 'http://localhost:5173/guide/installation', output: 'pdf/guide/installation.pdf' },
  { url: 'http://localhost:5173/basics/markdown', output: 'pdf/basics/markdown.pdf' }
]

async function main() {
  // 确保输出目录存在
  const dirs = new Set(pages.map(p => path.dirname(p.output)))
  dirs.forEach(dir => fs.mkdirSync(dir, { recursive: true }))

  // 启动开发服务器(如果未运行)
  console.log('📋 开始导出 PDF...')

  for (const page of pages) {
    try {
      await exportPdf(page)
    } catch (err) {
      console.error(`❌ 导出失败 ${page.url}:`, err)
    }
  }

  console.log('🎉 全部导出完成!')
}

main()

添加 npm 脚本

json
{
  "scripts": {
    "export:pdf": "node scripts/export-pdf.mts"
  }
}

方案二:使用 md-to-pdf

安装

bash
npm install -D md-to-pdf

配置文件

typescript
// .md2pdf.ts
import { PdfConfig } from 'md-to-pdf'

const config: PdfConfig = {
  pdf_options: {
    format: 'A4',
    margin: {
      top: '20mm',
      right: '15mm',
      bottom: '20mm',
      left: '15mm'
    },
    printBackground: true
  },
  stylesheet: ['.vitepress/theme/styles/pdf.css'],
  body_class: ['pdf-export'],
  css: `
    .pdf-export {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      font-size: 14px;
      line-height: 1.6;
      color: #333;
    }
    .pdf-export h1 { font-size: 24px; }
    .pdf-export h2 { font-size: 20px; }
    .pdf-export h3 { font-size: 16px; }
    .pdf-export pre {
      background: #f6f8fa;
      padding: 12px;
      border-radius: 6px;
      font-size: 13px;
    }
    .pdf-export code {
      background: #f6f8fa;
      padding: 2px 6px;
      border-radius: 3px;
    }
  `
}

export default config

导出脚本

bash
npx md-to-pdf docs/guide/*.md --pdf-dir pdf/guide

方案三:PWA 离线访问

PWA 方案允许用户在首次访问后离线浏览文档:

安装依赖

bash
npm install -D @vite-pwa/vitepress

配置 PWA

typescript
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { withPwa } from '@vite-pwa/vitepress'

export default withPwa(defineConfig({
  pwa: {
    registerType: 'autoUpdate',
    manifest: {
      name: 'VitePress 学习指南',
      short_name: 'VP 指南',
      theme_color: '#646cff',
      icons: [
        {
          src: '/pwa-192x192.png',
          sizes: '192x192',
          type: 'image/png'
        },
        {
          src: '/pwa-512x512.png',
          sizes: '512x512',
          type: 'image/png'
        }
      ]
    },
    workbox: {
      globPatterns: ['**/*.{css,js,html,svg,png,ico,txt,woff2}']
    }
  }
}))

提示

PWA 方案是离线阅读的首选方案,用户首次访问后文档会自动缓存,后续可离线访问。详见 PWA 最佳实践

方案四:HTML 离线包

将构建产物打包为可直接打开的 HTML 文件:

构建脚本

typescript
// scripts/offline-package.mts
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'

async function createOfflinePackage() {
  console.log('📦 开始创建离线文档包...')

  // 1. 构建
  console.log('1️⃣  构建站点...')
  execSync('npm run build', { stdio: 'inherit' })

  // 2. 将相对路径转为内联资源
  console.log('2️⃣  处理资源引用...')
  const distDir = 'docs/.vitepress/dist'

  // 内联关键 CSS
  const htmlFiles = findHtmlFiles(distDir)
  for (const file of htmlFiles) {
    inlineAssets(file)
  }

  // 3. 打包
  console.log('3️⃣  打包...')
  const outputName = `vitepress-docs-offline-${getDateStr()}.zip`
  execSync(`cd ${distDir} && zip -r ../../../${outputName} .`)

  console.log(`✅ 离线文档包已创建: ${outputName}`)
}

function findHtmlFiles(dir: string): string[] {
  const results: string[] = []
  const entries = fs.readdirSync(dir, { withFileTypes: true })

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)
    if (entry.isDirectory()) {
      results.push(...findHtmlFiles(fullPath))
    } else if (entry.name.endsWith('.html')) {
      results.push(fullPath)
    }
  }

  return results
}

function inlineAssets(htmlPath: string) {
  let content = fs.readFileSync(htmlPath, 'utf-8')

  // 将外部 CSS 转为内联
  content = content.replace(
    /<link[^>]*href="([^"]+\.css)"[^>]*>/g,
    (_match, cssPath) => {
      const cssFile = path.join(path.dirname(htmlPath), cssPath)
      if (fs.existsSync(cssFile)) {
        const css = fs.readFileSync(cssFile, 'utf-8')
        return `<style>${css}</style>`
      }
      return _match
    }
  )

  fs.writeFileSync(htmlPath, content, 'utf-8')
}

function getDateStr(): string {
  const d = new Date()
  return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`
}

createOfflinePackage()

添加页眉页脚

为 PDF 导出添加页眉和页脚:

typescript
// 在 Puppeteer 的 pdf 选项中添加
await page.pdf({
  path: output,
  format: 'A4',
  margin: {
    top: '25mm',
    right: '15mm',
    bottom: '25mm',
    left: '15mm'
  },
  displayHeaderFooter: true,
  headerTemplate: `
    <div style="font-size: 9px; width: 100%; text-align: center; color: #888;">
      VitePress 学习指南
    </div>
  `,
  footerTemplate: `
    <div style="font-size: 9px; width: 100%; text-align: center; color: #888;">
      <span class="pageNumber"></span> / <span class="totalPages"></span>
    </div>
  `
})

自动化导出

CI/CD 集成

yaml
# .github/workflows/export-pdf.yml
name: Export PDF

on:
  workflow_dispatch:
  release:
    types: [published]

jobs:
  export:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Build site
        run: npm run build

      - name: Export PDF
        run: npm run export:pdf

      - name: Upload PDF
        uses: actions/upload-artifact@v4
        with:
          name: pdf-documents
          path: pdf/

最佳实践

实践说明
隐藏导航元素PDF 中隐藏导航栏、侧边栏等交互元素
优化打印样式使用 @media print 添加打印专用样式
分页控制使用 break-before / break-after 控制分页
字体嵌入确保中文字体正确嵌入 PDF
链接处理将外部链接保留为可点击的超链接

相关链接

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献