NaN/NaN/NaN

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: avoidbreak-before: pageで意図した位置で改ページ

PlaygroundでHTMLを貼り付けてリアルタイムにPDF出力を確認しながら調整するのが最も効率的です。各言語でのAPI呼び出し方法はクイックスタートガイドを参照してください。レポートPDFの実装パターンについてはビジネスレポートPDF自動生成ガイドも参考になります。

関連リンク

Powered by FUNBREW PDF