環境
検証に使った環境は以下のとおりです。バージョンによって @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点です。
-
@pageを書いていないため、用紙が既定の Letter になっていた - 画面用の
marginやpaddingが印刷時にもそのまま効いていた - フッターの
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) — 日々の知見を発信中