Webサイトをレスポンシブデザインで実装した後、「そのままPDFに出力したい」という要望は開発現場で非常によく聞かれます。しかし、画面用のCSSとPDF出力の相性は必ずしも良くなく、「モバイル幅で出力された」「カラムが崩れた」「画像が枠をはみ出す」といった問題が頻発します。
この記事では、レスポンシブHTMLをPDF変換する際に直面する課題を体系的に整理し、実務で使える設計パターンとコード例を解説します。FUNBREW PDFのAPIを使った実践例も交えながら、Web表示とPDF出力を両立するテンプレート設計まで説明します。
Web用CSSとPDF用CSSの本質的な違い
まず、PDF生成エンジンがHTMLをレンダリングする仕組みを理解することが重要です。
レンダリングモデルの違い
| 項目 | Webブラウザ | PDF生成エンジン |
|---|---|---|
| 表示領域 | スクロール可能な無限高さ | 固定サイズのページ(A4等) |
| 幅 | ビューポート幅(可変) | 用紙サイズから余白を引いた固定幅 |
| インタラクション | あり(ホバー、クリック等) | なし |
| メディアタイプ | screen |
print |
| 改ページ | 存在しない | 明示的に制御が必要 |
| フォント | システムフォント + Webフォント | エンジン依存 |
PDF生成エンジン(Chromiumベースの場合)は、内部的にヘッドレスブラウザとしてHTMLをレンダリングし、それを指定された用紙サイズに分割してPDFを生成します。このレンダリング時のビューポート幅が、レスポンシブデザインの挙動に大きく影響します。
なぜレスポンシブデザインがPDFで崩れるのか
典型的なレスポンシブCSSの構造を見てみましょう。
/* デスクトップ: 2カラム */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
/* タブレット以下: 1カラム */
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
PDF生成エンジンのデフォルトビューポート幅は、多くの場合800px〜1280px程度です。しかし、API側でビューポートを明示的に指定しない場合、予期しない幅でレンダリングされる可能性があります。 結果として、意図したデスクトップレイアウトではなく、タブレットやモバイルのブレークポイントが適用されることがあります。
@media print の正しい使い方
PDF生成エンジンはprintメディアタイプでレンダリングします。@media printを正しく活用することが、PDF向けスタイル設計の基本です。
基本構造:スクリーン用とPDF用を分離する
/* ========================================
スクリーン共通スタイル
======================================== */
:root {
--color-primary: #2563eb;
--color-text: #1e293b;
--font-base: 'Noto Sans JP', sans-serif;
}
body {
font-family: var(--font-base);
color: var(--color-text);
background: #f8fafc;
}
/* ========================================
PDF専用スタイル(@media print内に集約)
======================================== */
@media print {
/* ページ全体のリセット */
body {
background: #ffffff;
font-size: 10.5pt;
line-height: 1.6;
color: #000000;
}
/* ナビゲーション・サイドバー・広告を非表示 */
nav, aside, .sidebar, .header-nav,
.cookie-banner, .chat-widget, .no-print {
display: none !important;
}
/* コンテンツエリアを全幅に */
.main-content {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 !important;
}
/* リンクのURL表示(印刷時の参考情報) */
a[href^="http"]::after {
content: " (" attr(href) ")";
font-size: 8pt;
color: #6b7280;
}
/* 内部リンクはURL非表示 */
a[href^="/"]::after,
a[href^="#"]::after {
content: none;
}
}
画面では非表示・PDFでのみ表示する要素
PDF用のカバーページや脚注など、画面表示には不要だがPDFには必要な要素を管理するパターンです。
/* 画面では非表示 */
.pdf-only {
display: none;
}
/* 画面では表示、PDFでは非表示 */
.screen-only {
display: block;
}
@media print {
.pdf-only {
display: block;
}
.screen-only {
display: none !important;
}
}
<!-- 画面ではヘッダーバーを表示、PDFではカバーページを表示 -->
<header class="screen-only">
<nav>...</nav>
</header>
<div class="pdf-only pdf-cover">
<h1>プロジェクト提案書</h1>
<p>株式会社サンプル</p>
<p>2026年4月5日</p>
</div>
色とコントラストの最適化
スクリーン向けの淡いカラーパレットはPDFで読みにくくなることがあります。
@media print {
/* シャドウをボーダーに変換(印刷では再現されない) */
.card, .panel, .box {
box-shadow: none !important;
border: 1px solid #d1d5db !important;
}
/* グラデーション背景をフラットカラーに */
.gradient-bg {
background: #2563eb !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* 薄いグレーテキストを濃くする */
.text-gray-400, .text-muted {
color: #374151 !important;
}
/* テーブルのボーダーを強調 */
table, th, td {
border-color: #374151 !important;
}
}
PDF専用レイアウトの設計パターン
パターン1:レスポンシブ→固定幅変換
最もよく使われるパターンです。スクリーンではレスポンシブ、PDFでは固定レイアウトに切り替えます。
/* スクリーン: レスポンシブグリッド */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
/* PDF: 2カラム固定レイアウト */
@media print {
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12mm;
break-inside: avoid;
}
/* カードのスタイルをPDF向けに最適化 */
.dashboard-grid .card {
border: 1pt solid #e2e8f0;
padding: 8mm;
break-inside: avoid;
}
}
パターン2:サイドバー付きレイアウトの変換
Webでよく使われる「メインコンテンツ + サイドバー」のレイアウトを、PDFでは縦積みまたは2カラムに変換します。
/* スクリーン: サイドバー付き3カラム */
.layout {
display: grid;
grid-template-columns: 240px 1fr 200px;
grid-template-areas:
"sidebar main aside";
gap: 24px;
}
/* PDF: サイドバーを非表示にしてメインを全幅に */
@media print {
.layout {
display: block;
}
.layout .sidebar {
display: none;
}
.layout .aside {
border-top: 1pt solid #e2e8f0;
padding-top: 8mm;
margin-top: 8mm;
columns: 2; /* 補足情報は2段組みで省スペース化 */
column-gap: 12mm;
}
}
パターン3:ヘッダー・フッターの固定表示
PDFの各ページに企業ロゴやページ番号を表示する、最も実用的なパターンです。
方法1: @pageマージンボックスを使う(CSS標準)
@page {
size: A4;
margin: 25mm 20mm 22mm 20mm;
/* ページ番号(下部中央) */
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 9pt;
color: #6b7280;
font-family: 'Noto Sans JP', sans-serif;
}
/* 会社名(下部左) */
@bottom-left {
content: "株式会社サンプル";
font-size: 8pt;
color: #9ca3af;
}
/* 日付(上部右) */
@top-right {
content: "2026年4月5日";
font-size: 8pt;
color: #9ca3af;
}
}
/* 表紙はヘッダー・フッター不要 */
@page :first {
@bottom-center { content: none; }
@bottom-left { content: none; }
@top-right { content: none; }
}
方法2: FUNBREW PDF の headerHtml / footerHtml オプションを使う
動的なデータ(各章のタイトル、ユーザー名など)をヘッダー・フッターに含める場合は、APIオプションが柔軟です。
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: mainContent,
options: {
format: 'A4',
margin: {
top: '25mm',
right: '20mm',
bottom: '22mm',
left: '20mm',
},
headerHtml: `
<div style="
width: 100%;
font-size: 9pt;
color: #6b7280;
font-family: 'Noto Sans JP', sans-serif;
display: flex;
justify-content: space-between;
padding: 0 20mm;
">
<span>プロジェクト提案書 v2.1</span>
<span>${new Date().toLocaleDateString('ja-JP')}</span>
</div>
`,
footerHtml: `
<div style="
width: 100%;
font-size: 9pt;
color: #6b7280;
font-family: 'Noto Sans JP', sans-serif;
text-align: center;
padding: 0 20mm;
">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
`,
},
}),
});
headerHtml/footerHtml内では
<span class="pageNumber">と<span class="totalPages">が自動的に現在ページ番号と総ページ数に置換されます。詳しくはAPIドキュメントを参照してください。
viewport幅の指定とその影響
レスポンシブPDF変換の核心部分です。viewport幅を正しく指定することで、意図したブレークポイントが適用されます。
FUNBREW PDF でのviewport指定
// デスクトップレイアウトでPDFを生成(推奨)
const options = {
format: 'A4',
viewport: {
width: 1280, // デスクトップ幅を指定
height: 900, // 高さはスクロールするので実質影響しない
deviceScaleFactor: 1,
},
};
viewport幅の選択ガイド
| viewport幅 | 適した用途 |
|---|---|
| 800〜1024px | A4縦向きに最適化されたレポート・文書 |
| 1280px | 一般的なデスクトップレイアウト |
| 1440px | ワイドスクリーン向けダッシュボード |
| 375px | モバイルレイアウトのまま出力(縦長PDF) |
viewportとPDF幅の関係
A4縦向き = 210mm × 297mm
= 約794px × 1123px(96dpi換算)
余白20mm左右の場合:
コンテンツ幅 = 210mm - 40mm = 170mm ≈ 643px
→ viewport: 800〜1000px が最もA4に最適化しやすい
実際には以下のように計算します。
// A4縦向きに最適なviewport幅を計算するユーティリティ
function calcOptimalViewport(format, marginMm) {
const PAGE_SIZES = {
A4: { width: 210, height: 297 },
Letter: { width: 215.9, height: 279.4 },
A3: { width: 297, height: 420 },
};
const page = PAGE_SIZES[format];
const contentWidthMm = page.width - marginMm.left - marginMm.right;
// 96dpiでピクセル換算(1mm = 3.7795px)
const contentWidthPx = contentWidthMm * 3.7795;
// コンテンツ幅に左右マージン分を足してviewport幅を算出
const viewportWidth = Math.round(contentWidthPx + (marginMm.left + marginMm.right) * 3.7795);
return viewportWidth;
}
// A4、左右20mmマージンの場合
const viewport = calcOptimalViewport('A4', { left: 20, right: 20 });
// → 794 (≒800px)
console.log(`推奨viewport幅: ${viewport}px`);
メディアクエリとviewportの組み合わせ
viewport幅が800pxの場合、以下のブレークポイントが適用されます。
/* viewport: 800px でPDF生成する場合の注意点 */
/* これは適用される(800px > 768px)*/
@media (min-width: 768px) {
.grid { grid-template-columns: 1fr 1fr; }
}
/* これは適用されない(800px < 1024px)*/
@media (min-width: 1024px) {
.grid { grid-template-columns: 1fr 1fr 1fr; }
}
/* PDF向け推奨: @media print で明示的に上書き */
@media print {
.grid {
grid-template-columns: 1fr 1fr; /* viewport幅に依存せず固定 */
}
}
Flexbox/GridのPDF対応状況
Chromiumベースのエンジン(FUNBREW PDFのqualityエンジン)は、現代的なCSSレイアウトを高い精度でサポートしています。
Flexboxの対応状況と注意点
/* 基本的なFlexboxは完全対応 */
.flex-container {
display: flex;
gap: 16px;
align-items: flex-start;
}
/* PDF生成で注意が必要なパターン */
@media print {
/* flex-wrap: wrap は改ページをまたいだ際に
予期しない動作をすることがある */
.flex-container {
flex-wrap: nowrap; /* PDF用は折り返し無効推奨 */
}
/* flex: 1 による均等分割はPDFでも正常動作 */
.flex-item {
flex: 1;
min-width: 0; /* overflow防止 */
}
/* Flexboxでの縦並び変換(サイドバーを下に移動) */
.sidebar-layout {
display: flex;
flex-direction: column; /* 横並びを縦並びに変換 */
}
}
CSS Gridの対応状況と注意点
/* CSS Gridは基本的に完全対応 */
.report-grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: auto;
gap: 20px;
}
/* 改ページをまたぐGridの制御 */
@media print {
.report-grid {
/* grid-auto-rows を固定するとページ内に収まりやすい */
grid-auto-rows: min-content;
}
/* Gridアイテムの改ページ制御 */
.grid-item {
break-inside: avoid;
}
/* 重要なアイテムは必ず1ページに収める */
.grid-item--important {
break-inside: avoid;
break-before: auto; /* 必要なら改ページを許可 */
}
/* 全幅アイテムの指定 */
.grid-item--full {
grid-column: 1 / -1;
break-before: page; /* 全幅コンテンツ前に改ページ */
}
}
非対応・注意が必要なCSSプロパティ
/* position: sticky はPDFで無効(印刷では意味がない) */
.sticky-header {
position: sticky;
top: 0;
}
@media print {
.sticky-header {
position: static; /* PDFでは通常フローに戻す */
}
}
/* CSS Animationはスナップショット時点の状態が出力 */
/* CSS Transitionも同様 */
/* vh/vw単位は使用可能だがviewportに依存 */
@media print {
.hero {
height: auto; /* vh指定をautoに変更 */
min-height: 0;
}
}
/* filter/backdrop-filterはエンジンによって対応状況が異なる */
@media print {
.blur-bg {
backdrop-filter: none;
background: rgba(255, 255, 255, 0.95); /* フォールバック */
}
}
画像のサイズ制御
基本:レスポンシブ画像をPDF用に固定する
/* 全画像の基本設定 */
img {
max-width: 100%;
height: auto;
display: block;
}
/* PDF専用の画像制御 */
@media print {
img {
max-width: 100% !important;
page-break-inside: avoid;
}
/* A4縦向きで使える最大高さ(余白25mm上下の場合) */
img.full-page-image {
max-height: 247mm; /* 297mm - 50mm */
width: auto;
max-width: 170mm; /* 210mm - 40mm */
object-fit: contain;
}
/* 横並び画像グループの制御 */
.image-group {
display: flex;
gap: 10mm;
break-inside: avoid;
}
.image-group img {
flex: 1;
max-width: calc(50% - 5mm); /* 2枚横並びの場合 */
height: auto;
}
}
背景色を維持する
/* 背景色・背景画像を強制出力 */
.branded-header {
background-color: #2563eb;
color: white;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* グラフや図表の背景 */
.chart-container {
background: white;
border: 1px solid #e2e8f0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
SVGとチャートの出力
/* SVGは解像度非依存でPDFに最適 */
svg {
max-width: 100%;
height: auto;
}
/* Chart.jsなどのcanvasベースのグラフは
PNG/SVGエクスポートを使用する */
canvas {
max-width: 100%;
}
@media print {
/* canvasの代替画像を表示 */
.chart-canvas {
display: none;
}
.chart-image {
display: block;
max-width: 100%;
}
}
A4/Letter等の用紙サイズ対応
@page ルールによる用紙サイズ指定
/* A4縦向き(デフォルト) */
@page {
size: A4 portrait;
margin: 20mm 15mm 18mm 15mm;
}
/* A4横向き */
@page {
size: A4 landscape;
margin: 15mm 20mm;
}
/* US Letterサイズ(北米向け) */
@page {
size: Letter;
margin: 1in 1in;
}
/* カスタムサイズ(名刺・ポスター等) */
@page {
size: 90mm 55mm; /* 名刺サイズ */
margin: 5mm;
}
ドキュメント内で複数用紙サイズを混在させる
同一PDFの中で縦向きと横向きのページを混在させる場合は名前付きページを使います。
/* デフォルト: A4縦向き */
@page {
size: A4 portrait;
margin: 20mm 15mm;
}
/* 横向きページの定義 */
@page landscape-page {
size: A4 landscape;
margin: 15mm 20mm;
}
/* 特定のセクションを横向きに */
.landscape-section {
page: landscape-page;
break-before: page;
break-after: page;
}
<section class="portrait-content">
<h2>第1章 概要</h2>
<p>通常の縦向きコンテンツ...</p>
</section>
<section class="landscape-section">
<h2>第2章 売上推移グラフ</h2>
<!-- 幅の広いグラフや表はこのセクションに配置 -->
<table class="wide-table">...</table>
</section>
<section class="portrait-content">
<h2>第3章 まとめ</h2>
<p>縦向きに戻るコンテンツ...</p>
</section>
API側での用紙サイズ指定
CSSと合わせて、API側でも用紙サイズを指定することができます。
// FUNBREW PDFのAPIオプション
const options = {
format: 'A4', // A4, Letter, A3, A5 など
landscape: false, // true で横向き
margin: {
top: '20mm',
right: '15mm',
bottom: '18mm',
left: '15mm',
},
};
CSS側の
@page { size: ... }とAPI側のformatが矛盾する場合、API側の指定が優先されます。どちらかに統一して管理することを推奨します。
実践例:Web表示とPDF出力を両立するテンプレート
実際の業務でよく使われる「レポートページ」を例に、Web表示とPDF出力を両立するテンプレートを示します。
HTMLテンプレート
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>月次レポート 2026年4月</title>
<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>
/* ========================================
CSS変数(共通)
======================================== */
:root {
--color-primary: #2563eb;
--color-secondary: #64748b;
--color-border: #e2e8f0;
--color-bg-light: #f8fafc;
--font-base: 'Noto Sans JP', sans-serif;
--radius: 8px;
}
/* ========================================
ベーススタイル(画面・PDF共通)
======================================== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-base);
color: #1e293b;
line-height: 1.6;
}
h1 { font-size: 24px; font-weight: 700; color: var(--color-primary); }
h2 { font-size: 18px; font-weight: 700; margin-top: 24px; margin-bottom: 12px; }
h3 { font-size: 15px; font-weight: 700; margin-top: 16px; margin-bottom: 8px; }
p { margin-bottom: 12px; }
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background: var(--color-bg-light);
font-weight: 700;
text-align: left;
padding: 10px 12px;
border-bottom: 2px solid var(--color-border);
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
td {
padding: 8px 12px;
border-bottom: 1px solid var(--color-border);
}
/* ========================================
スクリーン専用スタイル
======================================== */
@media screen {
body {
background: #f1f5f9;
}
.page-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.screen-nav {
background: white;
border-bottom: 1px solid var(--color-border);
padding: 16px 24px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.report-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.kpi-card {
background: white;
border-radius: var(--radius);
padding: 20px;
border: 1px solid var(--color-border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
/* ========================================
PDF専用スタイル(@media print)
======================================== */
@media print {
/* 用紙設定 */
@page {
size: A4;
margin: 20mm 15mm 18mm 15mm;
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 9pt;
color: #6b7280;
font-family: 'Noto Sans JP', sans-serif;
}
@bottom-left {
content: "月次レポート 2026年4月";
font-size: 8pt;
color: #9ca3af;
}
}
@page :first {
@bottom-center { content: none; }
@bottom-left { content: none; }
}
/* ベースリセット */
body {
background: white;
font-size: 10pt;
line-height: 1.5;
}
/* 画面専用要素を非表示 */
.screen-nav, .no-print, .screen-only {
display: none !important;
}
/* コンテナを全幅に */
.page-wrapper {
max-width: none;
margin: 0;
padding: 0;
}
/* KPIカードを2カラムグリッドに変換 */
.report-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8mm;
margin-bottom: 8mm;
}
.kpi-card {
border: 1pt solid #e2e8f0;
padding: 6mm;
break-inside: avoid;
box-shadow: none;
border-radius: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
/* テーブルの改ページ制御 */
table {
break-inside: auto;
font-size: 9pt;
}
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr { break-inside: avoid; }
/* 見出しの改ページ制御 */
h1 { font-size: 18pt; }
h2 { font-size: 13pt; break-after: avoid; }
h3 { font-size: 11pt; break-after: avoid; }
/* セクション間のページ区切り */
.report-section + .report-section {
break-before: page;
}
/* チャートのフォールバック */
.chart-canvas { display: none; }
.chart-fallback-image {
display: block;
max-width: 100%;
height: auto;
}
}
/* ========================================
共通コンポーネント
======================================== */
.kpi-label {
font-size: 12px;
color: var(--color-secondary);
margin-bottom: 4px;
}
.kpi-value {
font-size: 28px;
font-weight: 700;
color: var(--color-primary);
}
.kpi-change {
font-size: 12px;
margin-top: 4px;
}
.kpi-change.positive { color: #16a34a; }
.kpi-change.negative { color: #dc2626; }
</style>
</head>
<body>
<!-- スクリーン専用ナビゲーション -->
<nav class="screen-nav screen-only">
<span>月次レポートシステム</span>
<button onclick="window.print()" class="no-print">PDFで保存</button>
</nav>
<div class="page-wrapper">
<!-- レポートヘッダー -->
<header class="report-header">
<h1>月次パフォーマンスレポート</h1>
<p style="color: #64748b;">2026年4月 | 株式会社サンプル</p>
</header>
<!-- KPIカード -->
<section class="report-section">
<h2>主要KPI</h2>
<div class="report-grid">
<div class="kpi-card">
<div class="kpi-label">月間売上</div>
<div class="kpi-value">¥4,280万</div>
<div class="kpi-change positive">▲ 12.3% (先月比)</div>
</div>
<div class="kpi-card">
<div class="kpi-label">新規顧客数</div>
<div class="kpi-value">124件</div>
<div class="kpi-change positive">▲ 8.7% (先月比)</div>
</div>
<div class="kpi-card">
<div class="kpi-label">顧客満足度</div>
<div class="kpi-value">4.7 / 5.0</div>
<div class="kpi-change positive">▲ 0.2pt</div>
</div>
<div class="kpi-card">
<div class="kpi-label">解約率</div>
<div class="kpi-value">1.2%</div>
<div class="kpi-change positive">▼ 0.3pt (改善)</div>
</div>
</div>
</section>
<!-- 売上明細テーブル -->
<section class="report-section">
<h2>売上明細</h2>
<table>
<thead>
<tr>
<th>製品カテゴリ</th>
<th>売上</th>
<th>前月比</th>
<th>目標達成率</th>
</tr>
</thead>
<tbody>
<tr>
<td>エンタープライズプラン</td>
<td>¥2,400万</td>
<td>+15.2%</td>
<td>106%</td>
</tr>
<tr>
<td>スタンダードプラン</td>
<td>¥1,280万</td>
<td>+8.1%</td>
<td>98%</td>
</tr>
<tr>
<td>スターターパック</td>
<td>¥600万</td>
<td>+22.4%</td>
<td>120%</td>
</tr>
</tbody>
<tfoot>
<tr style="font-weight: 700;">
<td>合計</td>
<td>¥4,280万</td>
<td>+12.3%</td>
<td>104%</td>
</tr>
</tfoot>
</table>
</section>
</div><!-- /.page-wrapper -->
</body>
</html>
Node.jsでこのテンプレートからPDFを生成する
const fs = require('fs');
const fetch = require('node-fetch');
async function generateMonthlyReport(templateHtml, data) {
// テンプレートに動的データを埋め込む
const html = templateHtml
.replace('¥4,280万', `¥${data.totalSales}万`)
.replace('2026年4月', data.reportMonth);
const response = await fetch('https://pdf.funbrew.cloud/api/v1/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
format: 'A4',
viewport: { width: 1280, height: 900 },
margin: {
top: '20mm',
right: '15mm',
bottom: '18mm',
left: '15mm',
},
printBackground: true,
},
}),
});
if (!response.ok) {
throw new Error(`PDF生成エラー: ${response.status}`);
}
const pdfBuffer = await response.buffer();
fs.writeFileSync(`report-${data.reportMonth}.pdf`, pdfBuffer);
console.log('PDF生成完了:', `report-${data.reportMonth}.pdf`);
return pdfBuffer;
}
// 実行例
generateMonthlyReport(
fs.readFileSync('template.html', 'utf-8'),
{ totalSales: '4,280', reportMonth: '2026年4月' }
);
よくある問題と解決策
| 症状 | 原因 | 解決策 |
|---|---|---|
| モバイルレイアウトで出力される | viewport幅が小さい | API側でviewport幅を1280px等に指定 |
| カラムが1列になる | メディアクエリの誤適用 | @media print内でレイアウトを明示的に指定 |
| 画像がはみ出す | max-width: 100%未設定 |
全画像にmax-width: 100%; height: auto;を設定 |
| 背景色が消える | print-color-adjust未指定 |
色が必要な要素にprint-color-adjust: exactを追加 |
| Gridが崩れる | auto-fitの計算が変わる |
@media print内で列数を固定 |
| フォントが豆腐文字になる | フォントが読み込まれない | Noto Sans JPをCSSで指定またはインライン埋め込み |
| ページ内容が欠ける | 改ページ制御の問題 | break-inside: avoidを要素に追加 |
| ヘッダー/フッターが出ない | @pageマージンボックス未対応エンジン |
headerHtml/footerHtml APIオプションを使用 |
詳細なトラブルシューティングはHTML to PDFトラブルシューティングガイド、エンジン別のCSS対応状況はwkhtmltopdf vs Chromiumを参照してください。
まとめ
レスポンシブHTMLをPDF変換する際の設計の要点を整理します。
- viewport幅の指定: API側で1280px等のデスクトップ幅を明示し、意図したブレークポイントを適用する
- @media print の活用: PDFとスクリーンのスタイルを明確に分離し、不要な要素を非表示にする
- レイアウトの明示的な固定:
@media print内でFlexbox/Gridの列数や方向を明示的に指定する - 用紙サイズの指定:
@page { size: A4; }でCSSから直接用紙サイズを制御する - ヘッダー・フッター:
@pageマージンボックスまたはAPIのheaderHtml/footerHtmlオプションを使う - 画像制御:
max-width: 100%とprint-color-adjust: exactで画像の崩れと背景消失を防ぐ - 改ページ制御:
break-inside: avoidとbreak-before: pageで意図した位置で改ページ
PlaygroundでHTMLを貼り付けてリアルタイムにPDF出力を確認しながら調整するのが最も効率的です。各言語でのAPI呼び出し方法はクイックスタートガイドを参照してください。レポートPDFの実装パターンについてはビジネスレポートPDF自動生成ガイドも参考になります。
関連リンク
- HTML to PDF CSS最適化ガイド — 改ページ・余白・テーブルのCSS設計
- wkhtmltopdf vs Chromium — エンジン別CSS対応の違い
- HTML to PDFトラブルシューティング — よくあるエラーと解決策
- HTML to PDF完全ガイド — 変換手法の全体像
- PDF API本番運用チェックリスト — 本番環境での安定運用
- ビジネスレポートPDF自動生成ガイド — レポート自動化の実装例
- PDFテンプレートエンジン入門 — テンプレートと組み合わせた設計
- Playground — ブラウザでHTMLとCSSをリアルタイムテスト
- APIドキュメント — FUNBREW PDFのAPIリファレンス