2026/04/19

PlaywrightでPDFを生成する方法|コードサンプルとAPIへの移行ガイド

PlaywrightPDF生成Node.jsチュートリアル自動化

Playwrightはブラウザの自動化とE2Eテストで広く使われていますが、page.pdf()を使えばHTMLページをそのままPDFに変換できます。この記事では、Playwrightを使ったPDF生成の基礎から本番運用の課題まで、実践的なコード例とともに解説します。

既存のPuppeteerからの移行を検討している方はPuppeteerからPDF APIへの移行ガイドも参考にしてください。

Playwrightでのページ設定とインストール

インストール

npm install playwright
# Chromiumブラウザをインストール(PDF生成はChromiumのみ対応)
npx playwright install chromium

基本的なPDF生成

const { chromium } = require('playwright');

async function generatePdf(htmlContent, outputPath) {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // HTMLを直接セット
  await page.setContent(htmlContent, { waitUntil: 'networkidle' });

  // PDF生成
  const pdfBuffer = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: {
      top: '20mm',
      bottom: '20mm',
      left: '15mm',
      right: '15mm',
    },
  });

  await browser.close();
  return pdfBuffer;
}

// 使用例
const html = `
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    body { font-family: 'Noto Sans JP', sans-serif; font-size: 14px; }
    h1 { color: #1a56db; }
  </style>
</head>
<body>
  <h1>請求書</h1>
  <p>Playwrightで生成したPDFです。</p>
</body>
</html>`;

generatePdf(html, 'output.pdf').then(buf => {
  require('fs').writeFileSync('output.pdf', buf);
  console.log('PDF生成完了');
});

URLからPDFを生成する

const { chromium } = require('playwright');

async function generatePdfFromUrl(url, outputPath) {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // URLにナビゲート(フォント・画像の読み込みを待つ)
  await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });

  const pdfBuffer = await page.pdf({
    path: outputPath,  // ファイルに直接書き込む
    format: 'A4',
    printBackground: true,
  });

  await browser.close();
  return pdfBuffer;
}

generatePdfFromUrl('https://example.com', 'page.pdf');

page.pdf() オプション全解説

Playwrightのpage.pdf()が受け付けるオプションを体系的に整理します。

用紙サイズと向き

const pdf = await page.pdf({
  // 用紙フォーマット(letterがデフォルト)
  format: 'A4',  // A4, A3, Letter, Legal, Tabloid など

  // または幅・高さで指定(formatより優先度が低い)
  width: '210mm',
  height: '297mm',

  // 横向き
  landscape: true,

  // 印刷倍率(0.1〜2の範囲)
  scale: 1.0,
});

余白の設定

const pdf = await page.pdf({
  margin: {
    top: '20mm',
    bottom: '20mm',
    left: '15mm',
    right: '15mm',
  },
});

背景色・背景画像の出力

// デフォルトはfalse(背景なし)
const pdf = await page.pdf({
  printBackground: true,  // CSSの背景色・背景画像をPDFに出力
});

背景色が出力されない場合はこのオプションを確認してください。

ヘッダーとフッター

const pdf = await page.pdf({
  displayHeaderFooter: true,
  headerTemplate: `
    <div style="font-size: 10px; width: 100%; text-align: center; color: #999;">
      <span class="title"></span>
    </div>
  `,
  footerTemplate: `
    <div style="font-size: 10px; width: 100%; text-align: center; color: #999;">
      ページ <span class="pageNumber"></span> / <span class="totalPages"></span>
    </div>
  `,
  // ヘッダー・フッターを使う場合、余白を大きめに
  margin: { top: '40px', bottom: '40px', left: '20px', right: '20px' },
});

ヘッダー・フッターで使えるクラス:

クラス 出力内容
pageNumber 現在のページ番号
totalPages 総ページ数
date 現在の日付
title ページタイトル(<title>タグの値)
url ページのURL

ページ範囲の指定とタグ付きPDF

const pdf = await page.pdf({
  // 特定のページのみ出力(例: 1〜3ページと5ページ)
  pageRanges: '1-3, 5',

  // アクセシブルなタグ付きPDF(Playwright v1.42+)
  tagged: true,

  // PDFのアウトライン(目次のブックマーク)を埋め込む(v1.42+)
  outline: true,
});

CSS printメディアクエリの活用

Playwrightは@media printスタイルをデフォルトで適用します。印刷専用のCSSを記述することでPDF出力を細かく制御できます。

/* 画面での表示 */
.sidebar {
  display: block;
  width: 250px;
}

/* PDF出力時はサイドバーを非表示 */
@media print {
  .sidebar {
    display: none;
  }

  /* 改ページ制御 */
  .page-break {
    break-before: page;
    page-break-before: always; /* IE対応 */
  }

  /* セクション内で改ページさせない */
  .invoice-section {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  /* 見出しが単独でページ末尾に残らないように */
  h1, h2, h3 {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* 用紙サイズと余白 */
  @page {
    size: A4;
    margin: 20mm 15mm;
  }

  @page :first {
    margin-top: 30mm; /* 1ページ目だけ余白を広げる */
  }
}

screenメディアで出力する場合

@media printではなく@media screenスタイルでPDFを出力したい場合はemulateMediaを使います:

// printメディアではなくscreenメディアを使う
await page.emulateMedia({ media: 'screen' });

const pdf = await page.pdf({ printBackground: true });

実践的なサンプル:日本語請求書PDF

const { chromium } = require('playwright');

const invoiceData = {
  invoiceNumber: 'INV-2026-0042',
  issueDate: '2026年4月19日',
  dueDate: '2026年4月30日',
  clientName: '株式会社サンプル',
  items: [
    { name: 'Webシステム開発', qty: 1, unitPrice: 100000 },
    { name: 'デザイン作成', qty: 1, unitPrice: 30000 },
  ],
};

function buildInvoiceHtml(data) {
  const subtotal = data.items.reduce((s, i) => s + i.qty * i.unitPrice, 0);
  const tax = Math.round(subtotal * 0.1);
  const total = subtotal + tax;

  const rows = data.items.map(item => `
    <tr>
      <td>${item.name}</td>
      <td style="text-align:right;">${item.qty}式</td>
      <td style="text-align:right;">¥${item.unitPrice.toLocaleString()}</td>
      <td style="text-align:right;">¥${(item.qty * item.unitPrice).toLocaleString()}</td>
    </tr>
  `).join('');

  return `<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: 'Noto Sans JP', sans-serif; font-size: 13px; color: #1a1a1a; }
    @page { size: A4; margin: 15mm 20mm; }
    .page { max-width: 210mm; padding: 20px; }
    .header { display: flex; justify-content: space-between; margin-bottom: 32px; border-bottom: 3px solid #1a56db; padding-bottom: 16px; }
    .title { font-size: 32px; font-weight: 700; color: #1a56db; }
    table { width: 100%; border-collapse: collapse; margin-top: 24px; }
    th { background: #1a56db; color: #fff; padding: 10px 12px; text-align: left; }
    td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
    .total { font-size: 20px; font-weight: 700; color: #1a56db; }
  </style>
</head>
<body>
  <div class="page">
    <div class="header">
      <div class="title">請 求 書</div>
      <div style="text-align:right; color:#6b7280; font-size:12px;">
        <strong>${data.invoiceNumber}</strong><br>
        発行日: ${data.issueDate}<br>
        期限: ${data.dueDate}
      </div>
    </div>
    <p><strong>${data.clientName} 御中</strong></p>
    <table>
      <thead><tr><th>品目</th><th>数量</th><th>単価</th><th>小計</th></tr></thead>
      <tbody>${rows}</tbody>
    </table>
    <p style="text-align:right; margin-top:16px;">小計: ¥${subtotal.toLocaleString()}</p>
    <p style="text-align:right;">消費税(10%): ¥${tax.toLocaleString()}</p>
    <p class="total" style="text-align:right; margin-top:8px;">合計: ¥${total.toLocaleString()}</p>
  </div>
</body>
</html>`;
}

async function main() {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.setContent(buildInvoiceHtml(invoiceData), { waitUntil: 'networkidle' });

  const buf = await page.pdf({
    format: 'A4',
    printBackground: true,
    margin: { top: '15mm', bottom: '15mm', left: '20mm', right: '20mm' },
    displayHeaderFooter: true,
    footerTemplate: `<div style="font-size:9px;width:100%;text-align:center;color:#9ca3af;">
      Generated by Playwright — ページ <span class="pageNumber"></span> / <span class="totalPages"></span>
    </div>`,
  });

  require('fs').writeFileSync('invoice.pdf', buf);
  await browser.close();
  console.log('invoice.pdf を生成しました');
}

main();

Playwrightを本番で使う際の課題

PlaywrightによるPDF生成は手軽ですが、本番環境では以下の課題に直面します。

1. メモリ消費

Chromiumプロセスは1インスタンスあたり150〜300MBのメモリを消費します。同時リクエストが増えると線形にメモリが増加し、OOMクラッシュの原因になります。

// ブラウザのプール管理(並列数を制限)
const { chromium } = require('playwright');

class BrowserPool {
  constructor(size = 3) {
    this.size = size;
    this.pool = [];
    this.queue = [];
  }

  async acquire() {
    if (this.pool.length < this.size) {
      const browser = await chromium.launch({ headless: true });
      return browser;
    }
    return new Promise((resolve) => this.queue.push(resolve));
  }

  release(browser) {
    if (this.queue.length > 0) {
      const next = this.queue.shift();
      next(browser);
    } else {
      browser.close();
    }
  }
}

2. コールドスタート

Chromiumの起動には1〜3秒かかります。リクエストごとに起動・終了するとレイテンシが大きくなります。ブラウザを再利用(Keep-Alive)することでコールドスタートを回避できますが、メモリリークのリスクが生じます。

3. サーバーレス環境での制限

Lambda・Cloud Run・Vercel Functionsなど、サーバーレス環境ではChromiumバイナリの配置やメモリ制限に悩まされます。

環境 最大メモリ コールドスタート Chromium対応
AWS Lambda 10GB 1〜3秒 chrome-aws-lambda
Cloud Run 32GB 数百ms 標準Dockerイメージで可
Vercel Functions 3GB 1〜2秒 制限あり(@sparticuz/chromium

4. フォントの問題

Linuxサーバーには日本語フォントが標準でインストールされていません。DockerfileでNoto Sans JPをインストールする例:

FROM node:20-slim

# Chromiumの依存パッケージ + 日本語フォント
RUN apt-get update && apt-get install -y \
  chromium \
  fonts-noto-cjk \
  && rm -rf /var/lib/apt/lists/*

# Playwright用環境変数
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

CMD ["node", "server.js"]

FUNBREW PDF APIへの移行パス

PlaywrightでのPDF生成が本番規模に達したとき、FUNBREW PDF APIへの移行が現実的な選択肢になります。

なぜAPIへの移行を検討するか

課題 Playwright自前 FUNBREW PDF API
メモリ管理 自前でプール管理必要 API側で自動スケール
コールドスタート 起動に1〜3秒 数百ms以内(マネージド)
日本語フォント Dockerfileで手動設定 Noto Sans JPプリインストール
監視・エラー検知 自前で実装 APIメトリクス・Webhookあり
インフラコスト サーバー常時稼働 使用分だけ課金
メンテナンス Playwright/Chromiumバージョン管理 不要

移行の手順

既存のPlaywrightコードからFUNBREW PDF APIへの移行は、HTMLを生成する部分はそのままにAPI呼び出し部分だけを差し替えるだけです:

// Before: Playwright
const { chromium } = require('playwright');

async function generatePdf(html) {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'networkidle' });
  const buf = await page.pdf({ format: 'A4', printBackground: true });
  await browser.close();
  return buf;
}

// After: FUNBREW PDF API(HTMLの生成ロジックはそのまま)
async function generatePdf(html) {
  const response = await fetch('https://pdf.funbrew.cloud/api/pdf/generate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      html,
      options: {
        engine: 'quality',  // Chromiumベース(Playwrightと同等品質)
        format: 'A4',
        printBackground: true,
        margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
      },
    }),
  });
  const result = await response.json();
  return result.data.download_url;  // PDF URL
}

移行後もHTMLテンプレートはそのまま再利用できます。Playwright固有の@media printスタイルはFUNBREW PDFでも同様に動作します。

詳細な移行手順はPuppeteerからPDF APIへの移行ガイドを参照してください(Playwrightにも同様に適用できます)。

よくある質問(FAQ)

Q: Playwrightで生成したPDFとFUNBREW PDFの出力品質は同じですか?

はい。FUNBREW PDFのqualityエンジンはChromiumベースで動作するため、Playwrightと同じレンダリングエンジンを使用しています。CSSの解釈・フォントレンダリング・改ページの挙動は同等です。

Q: Playwrightで複数ページのPDFを生成するには?

単一の長いHTMLを渡すだけで自動的に複数ページに分割されます。@pageルールで用紙サイズと余白を指定し、break-before: pagebreak-inside: avoidで改ページを制御します。

Q: Playwright PDF生成をTypeScriptで書くには?

import { chromium } from 'playwright';

async function generatePdf(html: string): Promise<Buffer> {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.setContent(html, { waitUntil: 'networkidle' });
  const buf = await page.pdf({
    format: 'A4',
    printBackground: true,
  });
  await browser.close();
  return buf;
}

まとめ

  • Playwright page.pdf() はChromiumベースで、printBackgrounddisplayHeaderFootertaggedなど豊富なオプションを持つ
  • CSS @media print@page を活用して改ページ・余白・ヘッダーを細かく制御できる
  • 本番の課題: メモリ消費・コールドスタート・フォント設定・サーバーレス環境の制限
  • スケール時の選択肢: FUNBREW PDF APIへ移行するとHTML生成ロジックを変えずにインフラ管理を省略できる

PlaygroundでブラウザからPDF出力を確認したり、APIドキュメントでFUNBREW PDFのオプションを確認してみてください。

関連記事

Powered by FUNBREW PDF