0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Headless ChromeでHTMLレポートをPDF化したら最終ページがフッターだけになった話

0
Posted at

環境

検証に使った環境は以下のとおりです。バージョンによって @page まわりの挙動が変わる可能性があるので、まずは手元のバージョンを確認してください。

  • Node.js 20.14.0
  • puppeteer 22.12.1(同梱の Chrome for Testing 126)
  • 実行環境: Ubuntu 22.04(Docker コンテナ内)、開発は macOS 14
  • 検証ツール: poppler-utils(pdfinfo / pdftoppm)
  • レポートのHTMLはテンプレートエンジンで生成し、画面表示とPDF出力で同じHTMLを使い回す構成

事象

請求書や月次レポートを「HTMLで生成 → Headless Chrome でPDF化」する処理を組んでいたところ、2ページに収まるはずのレポートが3ページになり、最終ページがフッターだけという状態になりました。

  • 1ページ目: ヘッダー+本文(サマリ)
  • 2ページ目: 明細テーブルの続き
  • 3ページ目: フッターの著作権表示だけがポツンと1行

ブラウザで開いたときはまったく問題なく、PDF化したときだけ発生します。ページ数を返すAPIを叩いていたので、テストが「3ページ」を期待値として通ってしまい、しばらく気づけませんでした。

再現するコード(NGパターン)

画面表示用のCSSをそのまま使っていたのが原因です。典型的なのはこういう構成です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="/css/report.css">
</head>
<body>
  <header class="report-header">月次レポート 2025-05</header>
  <main>
    <section class="card">サマリ…</section>
    <table class="detail">…明細…</table>
  </main>
  <footer class="report-footer">© 2025 Example Inc.</footer>
</body>
</html>
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/report/123', {
    waitUntil: 'networkidle0',
  });
  await page.pdf({ path: 'report.pdf', printBackground: true });
  await browser.close();
})();

この状態だと、Chrome は用紙サイズを指定されていないと判断し、デフォルトの Letter(8.5in × 11in = 215.9mm × 279.4mm)でレイアウトします。A4(210mm × 297mm)より横が広く縦が短いので、縦方向の余裕が減り、フッターの margin-top ぶんだけはみ出して改ページされます。

原因の整理

発生していた問題は、突き詰めると次の3点です。

  1. @page を書いていないため、用紙が既定の Letter になっていた
  2. 画面用の margin や padding が印刷時にもそのまま効いていた
  3. フッターの margin-top が改ページを誘発し、本文が無いページが生まれた

紙のサイズは @page が決め、@media print が「紙の上での見た目」を決めます。画面表示用CSSはそのまま流用せず、印刷用に分岐させるのが安全です。

対応手順

手順1: @page で用紙と余白を明示する

@page {
  size: A4;
  /* 上右下左の順。フッター用に下を広めに取る */
  margin: 12mm 10mm 14mm;
}

手順2: @media print で本文・行間・カード余白・フッターを調整する

画面用のゆったりした余白は、紙の上では過剰です。印刷時だけ詰めます。

@media print {
  html, body {
    font-size: 10.5pt;
    line-height: 1.45;
  }

  .card {
    margin: 0 0 4mm;
    padding: 3mm 4mm;
    break-inside: avoid;   /* カードがページをまたがないように */
  }

  .detail tr {
    break-inside: avoid;
  }

  .report-footer {
    margin-top: 2mm;       /* ここを詰めるのが今回の肝 */
    font-size: 9pt;
  }
}

手順3: Puppeteer 側で preferCSSPageSize を有効にする

@page の size を実際に採用させるには、このオプションが必要です。指定しないと format(既定は Letter)が優先されます。

await page.pdf({
  path: 'report.pdf',
  printBackground: true,
  preferCSSPageSize: true,
  // margin はここでは指定しない(@page と二重に効くのを避ける)
});

変更前後の比較

観点 画面用CSSのまま print用CSSを分離
用紙サイズ Letter(既定) A4(@page で明示)
余白 画面用の padding がそのまま @page の margin で管理
フォントサイズ px 指定で大きめ pt 指定で紙に最適化
改ページ制御 なし break-inside: avoid
ページ数 3ページ(最終ページはフッターのみ) 2ページ
保守性 画面修正が印刷に波及 影響範囲を分離できる

検証: PDFを画像化して全ページを目視する

ページ数だけをアサートすると、今回のように「ページ数は増えたが内容は正しい」ケースを見逃します。テストではPDFを画像に変換して確認するのが確実です。

# ページ数と用紙サイズを確認
pdfinfo report.pdf | grep -E 'Pages|Page size'

# 全ページをPNGに書き出して目視
pdftoppm -png -r 110 report.pdf out/page

ls out/
# => page-1.png  page-2.png

CIに組み込むなら、pdfinfo の Page size が 595.276 x 841.89 pts (A4) になっていること、Pages が期待値どおりであること、そして縮小画像をアーティファクトとして保存しておくと、レビュー時に人間が確認できます。

ハマりポイント

  • preferCSSPageSize: true を忘れると、@page に size: A4 と書いても Letter のままになる(最もハマりやすい)
  • page.pdf() の margin オプションと @page の margin を両方指定すると、意図より余白が広がることがある。管理する場所はどちらか一方に寄せる
  • フッターを全ページの下端に固定したい場合は position: fixed が使えるが、「最終ページだけに出したい」場合は固定にせず通常フローに置く
  • break-inside: avoid を付けすぎると、かえって空白ページが生まれることがある。カードやテーブル行など単位を絞る
  • フォントは環境依存。Webフォントを使うなら document.fonts.ready を待ってから page.pdf() を呼ぶ
  • 開発機の macOS と CI の Linux でフォント差が出るため、font-family にフォールバックを明記しておく

FAQ

Q. 画面表示はそのままで、PDFだけ余白を詰めたい。

A. @media print の中だけで上書きすれば画面側に影響しません。@page の margin と @media print の内側の調整をセットで使い分けてください。

Q. ページ番号を入れたい。

A. @page { @bottom-right { content: counter(page); } } はChromeでは未対応です。Puppeteer の headerTemplate / footerTemplate を使い、displayHeaderFooter: true と併用するのが現実的です。

Q. px で書いたフォントサイズは直したほうがいい?

A. 印刷では pt のほうが意図が伝わります。1pt = 1/72in なので、A4の高さは約842pt。本文10.5pt、行間1.45あたりが読みやすさの目安です。

まとめ

画面表示と印刷は別物です。@page で用紙と余白を宣言し、@media print で紙のための調整をまとめておく。この2つを用意するだけで、今回のような「最終ページがフッターだけ」という事故はかなり防げます。

そして仕上げに、PDFを画像化して全ページを目視すること。ページ数やファイルサイズのアサートだけでは、レイアウト崩れは検出できません。同じような余白トラブルに遭遇したら、まず pdfinfo で用紙サイズを疑ってみてください。


この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?