NaN/NaN/NaN

同じレイアウトで中身だけ変えたPDFを大量に作りたい――請求書、証明書、レポートなど、多くの業務文書がこのパターンに当てはまります。

テンプレートエンジンを使えば、HTMLのレイアウトを一度作るだけで、データを流し込んで何枚でもPDFを生成できます。

テンプレートエンジンとは

PDFテンプレートエンジンは、HTMLテンプレート内の{{変数名}}を実際のデータで置換してからPDFに変換する仕組みです。

HTMLテンプレート + データ(JSON) → PDF

テンプレートの例

<h1>修了証</h1>
<p>{{受講者名}} 殿</p>
<p>{{研修名}}を修了したことを証明します。</p>
<p>{{修了日}}</p>

データの例

{
  "受講者名": "山田 太郎",
  "研修名": "情報セキュリティ基礎",
  "修了日": "2026年3月26日"
}

結果

修了証
山田 太郎 殿
情報セキュリティ基礎を修了したことを証明します。
2026年3月26日

FUNBREW PDFでの使い方

1. テンプレートを登録する

ダッシュボードのテンプレートエディタでHTMLを作成します。CodeMirror搭載のエディタで、リアルタイムプレビューを見ながら編集できます。

2. 変数を定義する

テンプレート内で {{変数名}} の形式で変数を使います。エディタが自動で変数を検出し、変数定義パネルに表示します。

各変数には以下を設定できます:

設定項目 説明
name 変数名
required 必須かどうか
default_value 未指定時のデフォルト値
description 用途の説明

3. APIからPDF生成

curl -X POST https://pdf.funbrew.cloud/api/pdf/generate-from-template \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "certificate",
    "variables": {
      "受講者名": "山田 太郎",
      "研修名": "情報セキュリティ基礎",
      "修了日": "2026年3月26日"
    }
  }'

言語別コード例

テンプレートからPDFを生成する実装例を、主要な3言語で紹介します。言語別クイックスタートでHTML直接指定の基本を把握したうえで、テンプレート活用に進むとスムーズです。

Python

import httpx
import os

FUNBREW_PDF_API_KEY = os.environ["FUNBREW_PDF_API_KEY"]

def generate_from_template(
    template_slug: str,
    variables: dict,
) -> bytes:
    """登録済みテンプレートからPDFを生成する"""
    response = httpx.post(
        "https://pdf.funbrew.cloud/api/pdf/generate-from-template",
        headers={
            "Authorization": f"Bearer {FUNBREW_PDF_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "template": template_slug,
            "variables": variables,
        },
        timeout=30.0,
    )
    response.raise_for_status()
    return response.content

# 請求書を生成
invoice_vars = {
    "顧客名": "株式会社サンプル",
    "請求番号": "INV-2026-001",
    "発行日": "2026年3月31日",
    "支払期限": "2026年4月30日",
    "明細行": """
        <tr><td>APIベーシックプラン</td><td>1</td><td>¥3,000</td></tr>
        <tr><td>追加PDF生成(500件)</td><td>500</td><td>¥1,500</td></tr>
    """,
    "小計": "¥4,500",
    "税額": "¥450",
    "合計": "¥4,950",
}

pdf_bytes = generate_from_template("invoice", invoice_vars)
with open("invoice_2026_001.pdf", "wb") as f:
    f.write(pdf_bytes)
print("請求書を保存しました。")

PHP

<?php

function generateFromTemplate(string $templateSlug, array $variables): string
{
    $apiKey = getenv('FUNBREW_PDF_API_KEY');

    $ch = curl_init('https://pdf.funbrew.cloud/api/pdf/generate-from-template');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $apiKey,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'template'  => $templateSlug,
            'variables' => $variables,
        ]),
        CURLOPT_TIMEOUT => 30,
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        throw new RuntimeException("PDF生成に失敗しました(HTTP {$httpCode})");
    }

    return $response;
}

// 修了証を生成
$pdfBytes = generateFromTemplate('certificate', [
    '受講者名'   => '山田 太郎',
    '研修名'     => 'PHP応用セキュリティ研修',
    '修了日'     => '2026年3月31日',
    '証明番号'   => 'CERT-2026-0042',
    '発行者名'   => 'FUNBREW アカデミー',
]);

file_put_contents('certificate.pdf', $pdfBytes);
echo "修了証を生成しました。\n";

Node.js

const fs = require('fs');

async function generateFromTemplate(templateSlug, variables) {
  const response = await fetch(
    'https://pdf.funbrew.cloud/api/pdf/generate-from-template',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ template: templateSlug, variables }),
    }
  );

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`PDF生成に失敗: ${response.status} ${body}`);
  }

  return Buffer.from(await response.arrayBuffer());
}

// 月次レポートを生成
async function generateMonthlyReport(month, salesData) {
  const chartRows = salesData
    .map(d => `<tr><td>${d.week}</td><td>¥${d.revenue.toLocaleString()}</td></tr>`)
    .join('');

  const pdf = await generateFromTemplate('monthly-report', {
    'レポート期間': month,
    '売上テーブル': chartRows,
    '合計売上': `¥${salesData.reduce((s, d) => s + d.revenue, 0).toLocaleString()}`,
  });

  fs.writeFileSync(`report-${month}.pdf`, pdf);
  console.log(`${month}のレポートを保存しました。`);
}

generateMonthlyReport('2026年3月', [
  { week: '第1週', revenue: 1240000 },
  { week: '第2週', revenue: 1580000 },
  { week: '第3週', revenue: 1120000 },
  { week: '第4週', revenue: 1860000 },
]);

実践テクニック

HTMLの繰り返し行を変数で渡す

テーブルの明細行など、行数が変動する場合はHTMLごと変数として渡します。

<!-- テンプレート -->
<table>
  <thead>
    <tr><th>品目</th><th>数量</th><th>金額</th></tr>
  </thead>
  <tbody>{{明細行}}</tbody>
</table>

API呼び出し時に、明細行のHTMLを組み立てて渡します:

const lineItems = items.map(item =>
  `<tr><td>${item.name}</td><td>${item.qty}</td><td>¥${item.price.toLocaleString()}</td></tr>`
).join('');

// variables: { "明細行": lineItems }

デフォルト値を活用する

変数にデフォルト値を設定しておくと、APIで未指定の場合にデフォルト値が使われます。

例えば「振込先」を変数にしてデフォルト値を設定しておけば、通常はデフォルトの振込先が使われ、特定の顧客だけ別の振込先を指定できます。

条件分岐はHTML側で制御

テンプレートエンジン自体にif文はありませんが、APIの呼び出し側で条件分岐してHTMLを組み立てることで対応できます。

const note = isPaid
  ? '<p style="color: green">お支払い済み</p>'
  : '<p style="color: red">未払い(期限: ' + dueDate + ')</p>';

// variables: { "支払状況": note }

テンプレートエンジン比較

PDF生成でテンプレートを使う場合、多くの開発者がHandlebars、Jinja2、Bladeといった既存のテンプレートエンジンを検討します。ここでは、各エンジンの特徴とFUNBREW PDFのアプローチの違いを比較します。

比較項目 Handlebars Jinja2 Blade FUNBREW PDF
言語 JavaScript Python PHP 言語非依存(API)
条件分岐 {{#if}} {% if %} @if アプリ側で制御
ループ {{#each}} {% for %} @foreach アプリ側で制御
フィルター ヘルパー関数 パイプ式 PHP関数 アプリ側で処理
PDF出力 別途変換必要 別途変換必要 別途変換必要 API一発

従来のテンプレートエンジンの課題

Handlebars、Jinja2、Bladeはそれぞれ優れたテンプレートエンジンですが、PDF生成に使う場合にはいくつかの課題があります。

  1. 言語に依存する: HandlebarsはJavaScript、Jinja2はPython、BladeはPHPのエコシステムでしか使えません。チームの技術スタックが変わるとテンプレートの移行が必要になります。
  2. PDF変換が別途必要: テンプレートからHTMLを生成した後、wkhtmltopdfやPuppeteerなどの別ツールでPDFに変換する必要があります。変換ツールのインストール・設定・メンテナンスが運用負荷になります。
  3. テンプレート内にロジックが混在する: 条件分岐やループがテンプレートに入ると、デザイナーとの協業が難しくなり、テストも複雑になります。

FUNBREW PDFのアプローチ

FUNBREW PDFでは、テンプレートには純粋なHTMLと{{変数名}}プレースホルダーだけを置き、条件分岐・ループ・データ整形はすべてアプリケーションコード側で行います。

このアプローチの利点は以下の通りです:

  • 言語非依存: REST APIなので、Python、PHP、Node.js、Go、Rubyなど、どの言語からでも同じテンプレートを利用できます
  • テンプレートがシンプル: HTMLとCSSの知識だけでテンプレートを作成・編集でき、デザイナーとの分業がしやすい
  • テストが簡単: ロジックはアプリ側にあるので、通常のユニットテストでカバーできます
  • インフラ不要: PDF変換エンジンの管理が不要で、APIを呼ぶだけでPDFが生成されます

実際のAPI仕様や対応エンジンの詳細はドキュメントで確認できます。

高度な条件分岐パターン

前述の通り、FUNBREW PDFのテンプレートエンジン自体にif文はありませんが、アプリケーション側で条件分岐を行ってからHTMLを変数として渡すことで、高度な表現が可能です。

複数条件の分岐(switch風パターン)

ステータスに応じて異なるバッジを表示する例です:

function statusBadge(status) {
  const badges = {
    paid:      '<span style="background:#22c55e;color:#fff;padding:4px 12px;border-radius:4px">支払済み</span>',
    pending:   '<span style="background:#f59e0b;color:#fff;padding:4px 12px;border-radius:4px">処理中</span>',
    overdue:   '<span style="background:#ef4444;color:#fff;padding:4px 12px;border-radius:4px">期限超過</span>',
    cancelled: '<span style="background:#6b7280;color:#fff;padding:4px 12px;border-radius:4px">キャンセル</span>',
  };
  return badges[status] || badges.pending;
}

// variables: { "ステータスバッジ": statusBadge(invoice.status) }

セクション全体の表示・非表示

データの有無に応じて、テンプレート内のセクション全体を出し分けることもできます:

# 割引セクション(割引がある場合のみ表示)
if discount_amount > 0:
    discount_section = f"""
    <div style="border:1px dashed #f59e0b;padding:12px;margin:16px 0;border-radius:8px">
      <strong>割引適用</strong>: {discount_name}<br>
      割引額: ¥{discount_amount:,}
    </div>
    """
else:
    discount_section = ""

# variables: { "割引セクション": discount_section }

ロケール対応の金額フォーマット

日本語と英語で通貨表示を切り替える例です:

def format_currency(amount, locale="ja"):
    if locale == "ja":
        return f"¥{amount:,.0f}"
    elif locale == "en":
        return f"${amount:,.2f}"
    elif locale == "eu":
        return f"€{amount:,.2f}"
    else:
        return f"{amount:,.2f}"

# 日本向け請求書
variables = {
    "合計金額": format_currency(49500, "ja"),   # → ¥49,500
    "通貨表記": "日本円(税込)",
}

# 海外向け請求書
variables = {
    "合計金額": format_currency(450.00, "en"),   # → $450.00
    "通貨表記": "USD (tax included)",
}

多言語対応の実践例については証明書自動発行ガイドも参考になります。

ループとネストデータの処理

配列データからHTMLを動的に組み立てる方法を紹介します。テンプレートにはループ構文がありませんが、アプリケーション側でHTMLを生成してから変数として渡すことで、柔軟な繰り返し処理が可能です。

複雑なネストテーブルの生成

部門ごとにグループ化された明細を持つテーブルの例です:

function buildDepartmentTable(departments) {
  let html = '';
  for (const dept of departments) {
    // 部門ヘッダー行
    html += `<tr style="background:#f1f5f9">
      <td colspan="4" style="font-weight:bold;padding:8px">${dept.name}</td>
    </tr>`;
    // 部門内の各明細行
    for (const item of dept.items) {
      html += `<tr>
        <td style="padding:4px 8px 4px 24px">${escapeHtml(item.name)}</td>
        <td style="text-align:right">${item.quantity}</td>
        <td style="text-align:right">¥${item.unitPrice.toLocaleString()}</td>
        <td style="text-align:right">¥${(item.quantity * item.unitPrice).toLocaleString()}</td>
      </tr>`;
    }
    // 部門小計行
    const subtotal = dept.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0);
    html += `<tr style="border-top:1px solid #cbd5e1">
      <td colspan="3" style="text-align:right;padding:4px 8px"><em>小計</em></td>
      <td style="text-align:right;font-weight:bold">¥${subtotal.toLocaleString()}</td>
    </tr>`;
  }
  return html;
}

// variables: { "部門別明細": buildDepartmentTable(data.departments) }

配列データから複数ページを生成

従業員ごとに1ページの給与明細を生成するパターンです。CSSのpage-break-afterを活用します:

def build_payslip_pages(employees):
    pages = []
    for i, emp in enumerate(employees):
        page_break = 'page-break-after: always;' if i < len(employees) - 1 else ''
        page = f"""
        <div style="{page_break} padding: 40px;">
          <h2>{emp['name']} 様 給与明細</h2>
          <table style="width:100%;border-collapse:collapse">
            <tr><td>基本給</td><td style="text-align:right">¥{emp['base_salary']:,}</td></tr>
            <tr><td>残業手当</td><td style="text-align:right">¥{emp['overtime']:,}</td></tr>
            <tr><td>通勤手当</td><td style="text-align:right">¥{emp['commute']:,}</td></tr>
            <tr style="border-top:2px solid #000;font-weight:bold">
              <td>支給合計</td>
              <td style="text-align:right">¥{emp['base_salary'] + emp['overtime'] + emp['commute']:,}</td>
            </tr>
          </table>
          <p style="margin-top:24px;color:#6b7280;font-size:12px">発行日: {emp['issue_date']}</p>
        </div>
        """
        pages.append(page)
    return "\n".join(pages)

# variables: { "給与明細ページ": build_payslip_pages(employees) }

ページ区切りやレイアウトの制御には、CSSの@media printが有効です。詳しくはHTML to PDF CSS設計ガイドを参照してください。

空配列のハンドリング

データが空の場合に「データなし」のメッセージを表示する例です:

function buildTableOrEmpty(items, emptyMessage = 'データがありません') {
  if (!items || items.length === 0) {
    return `<tr><td colspan="4" style="text-align:center;color:#9ca3af;padding:24px">${emptyMessage}</td></tr>`;
  }
  return items.map(item =>
    `<tr>
      <td>${escapeHtml(item.name)}</td>
      <td style="text-align:right">${item.qty}</td>
      <td style="text-align:right">¥${item.price.toLocaleString()}</td>
      <td style="text-align:right">¥${(item.qty * item.price).toLocaleString()}</td>
    </tr>`
  ).join('');
}

// variables: { "明細行": buildTableOrEmpty(order.items) }

テンプレートのバージョン管理

本番環境でテンプレートを運用する際は、バージョン管理が重要です。テンプレートの変更によって既存の帳票が崩れるリスクを防ぐためのプラクティスを紹介します。

テンプレートスラッグにバージョンを付ける

テンプレートのslugにバージョンサフィックスを付けて管理します:

invoice-v1      ← 現在の本番テンプレート
invoice-v2      ← 新デザインのテスト版
invoice-v2-bold ← A/Bテスト用バリエーション

API呼び出し時にslugを切り替えるだけで、テンプレートの切り替えが可能です:

const templateSlug = featureFlag('new_invoice_design')
  ? 'invoice-v2'
  : 'invoice-v1';

const pdf = await generateFromTemplate(templateSlug, variables);

A/Bテストの実施

テンプレートのデザインを比較検証したい場合、スラッグの切り替えだけで簡単にA/Bテストを実施できます:

import random

def get_template_slug(base_slug, ab_test_config=None):
    if ab_test_config and ab_test_config.get("enabled"):
        variants = ab_test_config["variants"]  # {"invoice-v2": 50, "invoice-v2-bold": 50}
        rand = random.randint(1, 100)
        cumulative = 0
        for variant, weight in variants.items():
            cumulative += weight
            if rand <= cumulative:
                return variant
    return base_slug

template = get_template_slug("invoice-v1", {
    "enabled": True,
    "variants": {"invoice-v2": 50, "invoice-v2-bold": 50}
})

ロールバック戦略

テンプレートの更新で問題が発生した場合のロールバック手順です:

  1. 旧バージョンを残す: テンプレートを更新する際は上書きせず、新しいslugで登録する
  2. 環境変数で切り替え: 使用するテンプレートslugを環境変数や設定ファイルで管理し、デプロイなしで切り替えられるようにする
  3. 生成履歴を保持: どのテンプレートバージョンでPDFを生成したかをログに記録し、問題発生時にトレースできるようにする

本番環境での安定運用についてはPDF API本番運用チェックリストも併せて確認してください。テンプレート運用の自動化についてはPlaygroundで実際に動作確認しながら進めるのがおすすめです。

ユースケース別テンプレート設計

請求書

変数: 顧客名、請求番号、発行日、明細行、小計、税額、合計、支払期限、振込先

ポイント: 明細行はHTMLで渡す、合計金額はサーバー側で計算

証明書・修了証

変数: 受講者名、研修名、修了日、証明番号、発行者名

ポイント: A4横向き、フォントサイズ大きめ、中央揃えレイアウト

月次レポート

変数: レポート期間、サマリーHTML、グラフ画像URL、詳細テーブル

ポイント: グラフはBase64画像として埋め込むか、事前に画像URLを用意

テンプレート設計のベストプラクティス

テンプレートを効率的に管理するためのポイントをまとめます。

  • 再利用性を意識する: 1つのテンプレートで複数のユースケースに対応できるよう、ロゴや会社名もデフォルト値付きの変数にしておく
  • スタイルはインラインで: メールクライアントと同様に、PDF生成でもインラインスタイルが最も安定して動作します
  • フォント指定を明示する: 日本語の場合はfont-family: 'Noto Sans JP', sans-serifを指定。FUNBREW PDFではプリインストール済みです
  • 印刷用CSSを活用: @media printでページ区切りやヘッダー/フッターを制御できます。エンジン選択によって対応状況が異なります

テンプレート設計をAPI比較の観点から検討したい場合は、HTML to PDF API比較も参考にしてください。

トラブルシューティング

テンプレートPDF生成でよくある問題と解決策をまとめます。より網羅的なエラー対応はHTML→PDFトラブルシューティング完全ガイドを参照してください。

変数が置換されない({{変数名}}のまま出力される)

原因: APIリクエストの変数名とテンプレート内のプレースホルダーが一致していません。

対処: 変数名は大文字・小文字を区別します。{{Student_Name}}{{student_name}}は別の変数です。登録済みの変数名はAPIで確認できます:

# テンプレートが期待する変数を確認
curl -s -H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
  "https://pdf.funbrew.cloud/api/templates/certificate" | jq '.variables[].name'

必須変数の未指定エラー

原因: requiredに設定された変数がAPIリクエストに含まれていません。

対処: 変数を指定するか、テンプレートエディタでdefault_valueを設定してオプショナルにします。

テーブルの行が正しく表示されない

原因: 明細行の値にHTMLの特殊文字(<>&)が含まれ、エスケープされていません。

対処: 動的な値はHTMLに埋め込む前にサニタイズします:

function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

const lineItems = items.map(item =>
  `<tr><td>${escapeHtml(item.name)}</td><td>${item.qty}</td><td>¥${item.price.toLocaleString()}</td></tr>`
).join('');

レイアウトが崩れる

原因: CSS GridやFlexboxがfastエンジン(wkhtmltopdf)で正しくレンダリングされないことがあります。

対処: レイアウトが複雑なテンプレートでは"engine": "quality"(Chromium)を指定してください。エンジンの違いはwkhtmltopdf vs Chromiumで詳しく解説しています。

PDFが空白または途中で切れる

原因: 外部リソース(フォント、画像)がレンダリング時に読み込めませんでした。

対処: 画像はBase64エンコードでインライン埋め込みするか、公開URLを使用してください。すべてのスタイルをインラインで記述するのが最も安定します。本番環境での安定運用についてはPDF API本番運用チェックリストも参考にしてください。

まとめ

テンプレートエンジンを使ったPDF生成のポイント:

  1. HTMLテンプレートを一度作れば何度でも再利用
  2. {{変数名}}でデータを動的に埋め込み
  3. デフォルト値で柔軟な運用
  4. 明細行はHTMLごと変数として渡す
  5. ユーザー入力はサニタイズしてからHTMLに埋め込む

テンプレートエディタで実際に試してみましょう → ダッシュボードにログイン

まだアカウントがない方は無料で登録できます。Playgroundでブラウザから手軽にテンプレートの動作確認もできます。

関連リンク

Powered by FUNBREW PDF