Use Cases (更新: 2026/6/2)

Claude CodeでPDF生成を実装する: Playwrightで請求書・レポートをHTMLから作る

Claude CodeでPDF生成を実装。Playwright、印刷CSS、日本語フォント、請求書、レポート、検証まで実例で解説。

Claude CodeでPDF生成を実装する: Playwrightで請求書・レポートをHTMLから作る

Webアプリに「PDFでダウンロード」ボタンを付けるだけなら簡単そうに見えます。しかし実務で作る請求書、月次レポート、修了証、見積書、監査ログのPDFは、画面のスクリーンショットとは別物です。A4で改ページが崩れないこと、背景色が消えないこと、日本語フォントが文字化けしないこと、金額が右寄せでそろうこと、印刷しても読めること、そしてあとからデザインを変えても壊れたと気づけることが必要です。

Claude Codeに「PDF生成を作って」とだけ頼むと、canvasに文字を描くサンプルや、位置をミリ単位で手書きするコードになりがちです。短い英語の証明書なら動いても、日本語の帳票、長い説明文、複数ページの表、ロゴ、フッター、電子保存向けの検索可能なテキストではすぐ限界が来ます。この記事では、初心者でも安全に始めやすい HTMLをブラウザで描画してPDF化する方式 に絞ります。

使う中心技術はPlaywrightです。Puppeteerでも同じ考え方で実装できます。公式情報は、Playwrightのpage.pdf、PuppeteerのPDF generationPDFOptions、MDNの@page印刷CSS、Claude Codeのoverviewを基準に確認してください。PDF化前のデータ作成はスプレッドシート自動化、ブラウザ検証はPlaywrightテスト、公開前の確認全体はテスト戦略も合わせて読むとつながります。

なぜHTMLからPDFを作るのか

PDF生成には大きく3つの選択肢があります。1つ目はjsPDFのようなライブラリでPDFの座標に直接文字や線を描く方法です。軽くて小さなラベルには向きますが、複雑なレイアウトでは「x座標を3mmずらす」修正が増えます。2つ目はcanvasに画面を描いて画像としてPDFに貼る方法です。見た目は再現しやすい一方、テキスト検索、コピー、アクセシビリティ、ファイルサイズで不利です。3つ目が、HTMLとCSSで帳票を作り、Chromiumの印刷エンジンでPDFにする方法です。

初心者におすすめするのは3つ目です。理由は、Web開発者がすでに知っているHTML、CSS、Flexbox、Grid、テーブル、フォント指定、@media printを使えるからです。請求書の表はtableで作れます。レポートの見出しはh1h2で表現できます。修了証の余白は@pageで制御できます。さらにPlaywrightなら、PDFを出す前のHTML画面をスクリーンショットで比較できるため、視覚回帰テストも入れやすくなります。

ただし、HTMLからPDFにすれば何でも解決するわけではありません。ブラウザの印刷CSSには独特の制約があります。画面表示で美しい影や固定ヘッダーが、印刷では邪魔になることがあります。Webフォントの読み込みが終わる前にPDF化すると文字幅が変わります。長い表の途中で改ページされると、合計欄が読みづらくなります。Claude Codeには「ブラウザ表示」ではなく「印刷物」として要件を渡す必要があります。

flowchart TD
  A["業務データ"] --> B["HTMLテンプレート"]
  B --> C["print CSSと@page"]
  C --> D["PlaywrightでChromium描画"]
  D --> E["PDF出力"]
  C --> F["スクリーンショット比較"]
  F --> G["レビューで差分確認"]

実用ユースケースを先に決める

1つ目のユースケースは請求書です。会社名、請求先、明細、税率、合計、支払期限、登録番号、振込先など、金額と責任が絡む情報を扱います。ミスがあると信頼を落とすため、金額の丸め、桁区切り、税計算、ページ下部の注記をテスト対象にします。Claude Codeには「PDFの見た目」だけでなく「税込合計の計算をテストする」まで頼みます。

2つ目は月次レポートです。広告運用、SEO、SaaSの利用状況、売上分析などで、表、文章、グラフ画像を組み合わせます。ここでは改ページと見出しの扱いが重要です。セクションの途中でタイトルだけがページ末尾に残ると読みにくくなります。break-inside: avoidや余白設計を使い、必要なら章ごとにページを分けます。

3つ目は修了証や証明書です。ロゴ、氏名、コース名、発行日、証明ID、QRコードを1ページに収めます。キャンバス画像だけで作ると見た目は簡単ですが、後から氏名検索ができず、ファイルサイズも大きくなります。HTMLテキストとして主要情報を残し、署名や装飾だけ画像にするほうが運用しやすいです。

4つ目は社内向けの監査レポートです。権限変更、デプロイ履歴、承認ログ、エラー件数などをPDFで保存するケースです。ここでは「証跡」として後から読み返せることが大切です。日時、環境名、生成したアプリのバージョン、入力データのハッシュなどをフッターに入れておくと、あとで調査しやすくなります。

Claude Codeへの依頼文テンプレート

Claude Codeには、使うライブラリ、触ってよいファイル、PDFの用途、ページサイズ、フォント、検証方法をまとめて渡します。曖昧な依頼にすると、既存の画面CSSを壊したり、canvasだけの実装に寄ったり、APIエンドポイントだけ作って帳票の品質確認を省いたりします。

Claude CodeでPDF生成機能を実装してください。

方針:
- HTMLテンプレートをPlaywrightのChromiumでPDF化する
- canvasだけで帳票全体を画像化しない
- A4縦、余白14mm、背景色をPDFに含める
- 日本語フォントを考慮し、Noto Sans JP / Yu Gothic / sans-serifを指定する
- 請求書、月次レポート、修了証に転用しやすい構成にする

実装:
- scripts/create-invoice-pdf.mjs を追加
- サンプルデータから out/invoice-T-2026-0602.pdf を生成
- 金額はIntl.NumberFormatで整形
- ユーザー入力はHTMLエスケープする
- page.pdfではprintBackgroundとpreferCSSPageSizeを使う

検証:
- 実行コマンドをREADMEまたは記事本文に残す
- PDF生成前のHTMLをスクリーンショット比較できる設計にする
- フォント読み込み、改ページ、背景色、合計金額を確認する

この依頼文のポイントは「何を作るか」より「何を避けるか」を明記している点です。特に帳票全体を画像化する実装は、最初の見た目だけなら成功に見えます。しかし検索できない、印刷でにじむ、差分レビューが難しい、アクセシビリティが落ちるという問題が後で効いてきます。

コピペで動くPlaywright PDF生成

次のコードはNode.jsでそのまま試せる最小構成です。新しいフォルダで実行するなら、先にPlaywrightを入れてChromiumを準備します。

npm init -y
npm pkg set type=module
npm i -D playwright
npx playwright install chromium
mkdir scripts out
node scripts/create-invoice-pdf.mjs

scripts/create-invoice-pdf.mjsを作り、次を貼り付けます。金額、明細、日本語テキスト、印刷CSS、フォント待ち、PDF出力まで含めています。

import { chromium } from "playwright";
import { mkdir } from "node:fs/promises";
import { dirname, resolve } from "node:path";

const outputPath = resolve("out/invoice-T-2026-0602.pdf");

const invoice = {
  number: "T-2026-0602",
  issuedAt: "2026-06-02",
  dueAt: "2026-06-30",
  seller: "Masa Design Lab",
  buyer: "株式会社サンプル 御中",
  note: "Claude Code導入支援と帳票テンプレート設計のご請求です。",
  items: [
    { name: "PDF帳票テンプレート設計", quantity: 1, unitPrice: 80000 },
    { name: "Playwright生成スクリプト実装", quantity: 1, unitPrice: 120000 },
    { name: "印刷CSSと日本語フォント検証", quantity: 1, unitPrice: 60000 },
  ],
  taxRate: 0.1,
};

const money = new Intl.NumberFormat("ja-JP", {
  style: "currency",
  currency: "JPY",
});

function escapeHtml(value) {
  return String(value).replace(/[&<>"']/g, (char) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#39;",
  })[char]);
}

function renderInvoiceHtml(data) {
  const subtotal = data.items.reduce(
    (sum, item) => sum + item.quantity * item.unitPrice,
    0,
  );
  const tax = Math.floor(subtotal * data.taxRate);
  const total = subtotal + tax;

  const rows = data.items.map((item) => {
    const amount = item.quantity * item.unitPrice;
    return `<tr>
      <td>${escapeHtml(item.name)}</td>
      <td class="num">${item.quantity}</td>
      <td class="num">${money.format(item.unitPrice)}</td>
      <td class="num">${money.format(amount)}</td>
    </tr>`;
  }).join("");

  return `<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Invoice ${escapeHtml(data.number)}</title>
  <style>
    @page {
      size: A4;
      margin: 14mm;
    }

    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      color: #202124;
      font-family: "Noto Sans JP", "Yu Gothic", "Hiragino Sans", Arial, sans-serif;
      font-size: 12px;
      line-height: 1.7;
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
    }

    .sheet {
      min-height: 269mm;
      display: flex;
      flex-direction: column;
      gap: 18px;
    }

    header {
      display: flex;
      justify-content: space-between;
      gap: 24px;
      border-bottom: 3px solid #1f5eff;
      padding-bottom: 16px;
    }

    h1 {
      margin: 0 0 10px;
      font-size: 28px;
      letter-spacing: 0;
    }

    .meta {
      text-align: right;
      color: #4b5563;
    }

    .parties {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 20px;
    }

    .box {
      border: 1px solid #d7dce5;
      border-radius: 6px;
      padding: 14px;
      background: #f8fafc;
    }

    table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 8px;
    }

    th {
      background: #eef3ff;
      color: #1f2937;
      text-align: left;
    }

    th,
    td {
      border-bottom: 1px solid #d7dce5;
      padding: 10px 8px;
      vertical-align: top;
    }

    .num {
      text-align: right;
      white-space: nowrap;
    }

    .totals {
      margin-left: auto;
      width: 260px;
    }

    .totals div {
      display: flex;
      justify-content: space-between;
      padding: 6px 0;
    }

    .grand-total {
      margin-top: 4px;
      border-top: 2px solid #1f5eff;
      font-size: 18px;
      font-weight: 700;
    }

    .avoid-break {
      break-inside: avoid;
      page-break-inside: avoid;
    }

    footer {
      margin-top: auto;
      border-top: 1px solid #d7dce5;
      padding-top: 10px;
      color: #6b7280;
      font-size: 10px;
    }
  </style>
</head>
<body>
  <main class="sheet">
    <header>
      <div>
        <h1>請求書</h1>
        <div>${escapeHtml(data.buyer)}</div>
      </div>
      <div class="meta">
        <div>請求書番号: ${escapeHtml(data.number)}</div>
        <div>発行日: ${escapeHtml(data.issuedAt)}</div>
        <div>支払期限: ${escapeHtml(data.dueAt)}</div>
      </div>
    </header>

    <section class="parties avoid-break">
      <div class="box">
        <strong>請求元</strong><br>
        ${escapeHtml(data.seller)}
      </div>
      <div class="box">
        <strong>備考</strong><br>
        ${escapeHtml(data.note)}
      </div>
    </section>

    <section>
      <table>
        <thead>
          <tr>
            <th scope="col">品目</th>
            <th scope="col" class="num">数量</th>
            <th scope="col" class="num">単価</th>
            <th scope="col" class="num">金額</th>
          </tr>
        </thead>
        <tbody>${rows}</tbody>
      </table>
    </section>

    <section class="totals avoid-break">
      <div><span>小計</span><span>${money.format(subtotal)}</span></div>
      <div><span>消費税</span><span>${money.format(tax)}</span></div>
      <div class="grand-total"><span>合計</span><span>${money.format(total)}</span></div>
    </section>

    <footer>
      Generated by Playwright. Verify layout, fonts, totals, and page breaks before sending.
    </footer>
  </main>
</body>
</html>`;
}

async function createPdf() {
  await mkdir(dirname(outputPath), { recursive: true });
  const browser = await chromium.launch();

  try {
    const page = await browser.newPage();
    await page.setContent(renderInvoiceHtml(invoice), { waitUntil: "networkidle" });
    await page.evaluate(() => document.fonts.ready);
    await page.emulateMedia({ media: "print" });
    await page.pdf({
      path: outputPath,
      printBackground: true,
      preferCSSPageSize: true,
      tagged: true,
      margin: { top: "0", right: "0", bottom: "0", left: "0" },
    });
    console.log(`Created ${outputPath}`);
  } finally {
    await browser.close();
  }
}

await createPdf();

この実装では、余白を@pageに寄せ、page.pdf側の余白を0にしています。二重に余白を指定すると、A4に収まるはずの表が次ページへ押し出されることがあります。printBackground: trueは、背景色やヘッダー色をPDFに出すために重要です。preferCSSPageSize: trueは、CSSの@page sizeを優先したいときに使います。PuppeteerでもPage.pdf()PDFOptionsでほぼ同じ設計にできます。

印刷CSSと日本語フォントの注意点

帳票CSSでは、まずページサイズと余白を固定します。@page { size: A4; margin: 14mm; }をベースにし、画面用の余白と印刷用の余白を混ぜないようにします。WebページのカードUIをそのままPDFに流用すると、影、角丸、背景グラデーションが多すぎて印刷時に重く見えます。PDF用テンプレートは、画面ページとは分けるほうが保守しやすいです。

日本語ではフォントが重要です。ローカル開発ではYu Gothicで問題なく見えても、LinuxのCIやサーバーではそのフォントがありません。DockerやCIでPDF生成するなら、Noto Sans CJK系のフォントを入れるか、配布可能なフォントを明示的に読み込む必要があります。生成前にdocument.fonts.readyを待つのは、フォント読み込み前の文字幅でPDF化される事故を減らすためです。

改ページでは、合計欄、署名欄、表の小さなグループにbreak-inside: avoidを使います。ただし、要素が1ページより大きければ避けようがありません。長い明細表では、1ページに収めるより、表ヘッダーを各ページで読める設計にするほうが現実的です。完璧にページ番号やヘッダーを制御したい場合は、ブラウザPDFだけでなく専用帳票エンジンも候補になりますが、最初からそこへ行くと実装コストが上がります。

失敗例と落とし穴

一番多い失敗は、PDFを「画面キャプチャ」として扱うことです。canvasや巨大なPNGをPDFに貼ると、初回レビューではきれいに見えます。しかしユーザーが文字を検索できず、ファイルサイズが大きくなり、拡大時にぼやけ、差分テストもしづらくなります。グラフや署名のように画像が必要な場所だけ画像にし、本文、金額、見出し、日付はHTMLテキストのまま残すのが基本です。

次に、背景色が消える問題です。ブラウザの印刷では背景を省く設定が絡むため、printBackground: trueを忘れるとヘッダーの帯や薄い表背景が消えます。CSS側でもprint-color-adjust: exactを指定しておくと、色の再現性が上がります。ただし、色だけで重要情報を伝えないことも大切です。

3つ目は、外部画像やWebフォントの読み込み待ち不足です。page.setContentした直後にPDF化すると、ロゴが出なかったり、フォントがフォールバックしたりします。外部URLを使う場合は、ネットワークに依存しないようにロゴをローカル配信にする、Base64埋め込みにする、またはpage.gotoで実際の印刷用URLを開いてnetworkidleを待つ設計にします。

4つ目は、ユーザー入力をHTMLに直接差し込むことです。PDFはサーバー側で作ることが多いため安全そうに見えますが、HTMLテンプレートに名前や備考をそのまま入れると、意図しないタグやスクリプトが混ざります。サンプルコードのように最低限のHTMLエスケープを入れ、実サービスではテンプレートエンジンのエスケープ規則も確認します。

スクリーンショットと目視で検証する

PDF生成はユニットテストだけでは足りません。計算結果のテスト、HTMLの存在確認、PDFファイル生成確認に加えて、見た目の回帰確認を入れます。Playwright Testを使っているプロジェクトなら、印刷用URLを開き、emulateMedia({ media: "print" })を使ってスクリーンショットを保存します。

import { expect, test } from "@playwright/test";

test("invoice print layout is stable", async ({ page }) => {
  await page.goto("/invoices/T-2026-0602/print");
  await page.emulateMedia({ media: "print" });

  await expect(page.getByRole("heading", { name: "請求書" })).toBeVisible();
  await expect(page.getByText("PDF帳票テンプレート設計")).toBeVisible();
  await expect(page.getByText("¥286,000")).toBeVisible();
  await expect(page).toHaveScreenshot("invoice-print-a4.png", {
    fullPage: true,
    maxDiffPixelRatio: 0.01,
  });
});

このテストはPDFそのものを完全に検査するものではありません。PDF化直前の印刷HTMLを検査するためのものです。実務では、これに加えてPDFファイルのバイト数が0でないこと、保存先に作られること、合計金額がサーバー側計算と一致すること、長い明細で2ページ目ができても合計欄が欠けないことを確認します。

収益化につなげるCTA設計

PDF生成は、記事サイトやSaaSの収益導線と相性が良い機能です。たとえば無料の見積書テンプレート、チェックリストPDF、レポートサンプルを配布し、詳細な実装テンプレートや導入相談へつなげられます。ただし、AdSenseやアフィリエイトのために薄いPDF記事を量産しても評価されません。実際に動くコード、失敗例、検証結果、業務での使いどころが必要です。

ClaudeCodeLabでは、日常のClaude Codeコマンドは無料チートシート、実装テンプレートやプロンプト集は教材一覧、チームの帳票生成・レビュー・検証フローを整えたい場合はClaude Code研修・相談に分けています。PDF生成も、単発のスクリプトではなく「データ作成、テンプレート、生成、検証、配布」を流れとして整えると、請求書、レポート、証明書に横展開できます。

この記事で紹介した内容は、ローカルのNode.js環境でPlaywrightのChromiumを入れ、請求書PDFを生成する前提で確認しました。特に効果があったのは、PDFを直接見て終わりにせず、印刷用HTMLをスクリーンショット比較の対象にしたことです。Masaの実務でも、合計金額の計算ミスより、フォント読み込み前のPDF化、背景色の消失、長い備考欄による改ページ崩れのほうが見落とされやすいです。Claude Codeには最初から「動くコード」と「確認証跡」を同時に求めると、公開前レビューがかなり楽になります。

#Claude Code #PDF生成 #jsPDF #Puppeteer #帳票
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。