NaN/NaN/NaN

「毎月末に社内向け月次報告書をPDFで作り直している」「クライアントごとにKPIレポートを手作業でまとめている」――こうした定型業務はPDF生成APIで完全に自動化できます。

この記事では、FUNBREW PDFを使ったビジネスレポートのPDF自動生成を解説します。Chart.jsによる動的グラフの描画からヘッダー・フッター・ページ番号の付与、cron定期実行、メール自動送信との連携まで、実際のNode.js・Pythonコードを交えて説明します。

ビジネスレポートPDF化のユースケース

PDF自動生成が特に効果を発揮するのは、以下の3パターンです。

1. 月次報告書(Management Report)

経営層・取締役会向けに月次・四半期で送付する業績サマリー。売上推移グラフ、コスト内訳、KPI達成率などを含むドキュメントです。手作業では数時間かかりますが、データをAPIに渡してHTMLテンプレートからPDFを生成すれば数分で完了します。

2. KPIダッシュボードレポート

Google Analytics・Salesforce・BigQueryなどからデータを取得し、棒グラフ・折れ線グラフ・パイチャートでビジュアライズしたレポートです。社内ダッシュボードはリアルタイムで便利ですが、「その時点のスナップショット」として保存・共有するにはPDFが最適です。

3. クライアントレポート(Client Report)

代理店・コンサルタント・SaaSビジネスでクライアントに送付するパフォーマンスレポートです。クライアントごとに異なるデータを差し込み、ブランドロゴ入りのフォーマットで自動生成できます。レポート生成の活用例はレポート自動化のユースケースもご覧ください。

HTML + Chart.jsでの動的グラフ生成

FUNBREW PDFはヘッドレスChromiumでHTMLをレンダリングするため、Chart.jsなどのJavaScriptライブラリを使ったグラフをそのままPDFに変換できます。

グラフ入りHTMLテンプレートの作成

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Hiragino Sans', 'Noto Sans JP', sans-serif;
      font-size: 13px;
      color: #1e293b;
      background: #fff;
      padding: 40px;
    }
    h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
    h2 { font-size: 15px; font-weight: 600; margin-bottom: 16px; color: #334155; }
    .meta { color: #64748b; font-size: 12px; margin-bottom: 32px; }
    .kpi-grid {
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      gap: 16px;
      margin-bottom: 32px;
    }
    .kpi-card {
      background: #f8fafc;
      border: 1px solid #e2e8f0;
      border-radius: 8px;
      padding: 16px;
    }
    .kpi-label { font-size: 11px; color: #64748b; margin-bottom: 4px; }
    .kpi-value { font-size: 24px; font-weight: 700; }
    .kpi-change { font-size: 11px; margin-top: 4px; }
    .kpi-change.up { color: #16a34a; }
    .kpi-change.down { color: #dc2626; }
    .chart-section { margin-bottom: 32px; }
    canvas { max-width: 100%; }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
</head>
<body>

  <h1>月次業績レポート</h1>
  <p class="meta">対象期間: {{period}} / 作成日: {{created_at}}</p>

  <!-- KPIカード -->
  <div class="kpi-grid">
    <div class="kpi-card">
      <div class="kpi-label">売上合計</div>
      <div class="kpi-value">¥{{revenue}}</div>
      <div class="kpi-change up">▲ {{revenue_change}}% 前月比</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">新規顧客</div>
      <div class="kpi-value">{{new_customers}}</div>
      <div class="kpi-change up">▲ {{customer_change}}% 前月比</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">成約率</div>
      <div class="kpi-value">{{conversion_rate}}%</div>
      <div class="kpi-change down">▼ {{conversion_change}}% 前月比</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">解約率</div>
      <div class="kpi-value">{{churn_rate}}%</div>
      <div class="kpi-change up">▲ 改善</div>
    </div>
  </div>

  <!-- 売上推移グラフ -->
  <div class="chart-section">
    <h2>売上推移(過去6ヶ月)</h2>
    <canvas id="revenueChart" height="120"></canvas>
  </div>

  <!-- 商品別売上グラフ -->
  <div class="chart-section">
    <h2>商品カテゴリ別売上構成</h2>
    <canvas id="categoryChart" height="100"></canvas>
  </div>

  <script>
    // データはAPIリクエスト時に埋め込む
    const months = {{months_json}};
    const revenueData = {{revenue_data_json}};
    const categoryLabels = {{category_labels_json}};
    const categoryData = {{category_data_json}};

    new Chart(document.getElementById('revenueChart'), {
      type: 'bar',
      data: {
        labels: months,
        datasets: [{
          label: '売上(万円)',
          data: revenueData,
          backgroundColor: 'rgba(59, 130, 246, 0.7)',
          borderColor: 'rgb(59, 130, 246)',
          borderWidth: 1,
          borderRadius: 4,
        }]
      },
      options: {
        responsive: true,
        animation: false,   // PDF生成時はアニメーション不要
        plugins: { legend: { display: false } },
        scales: { y: { beginAtZero: true } }
      }
    });

    new Chart(document.getElementById('categoryChart'), {
      type: 'doughnut',
      data: {
        labels: categoryLabels,
        datasets: [{
          data: categoryData,
          backgroundColor: ['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6'],
        }]
      },
      options: {
        responsive: true,
        animation: false,
        plugins: { legend: { position: 'right' } }
      }
    });
  </script>
</body>
</html>

animation: false は必須設定です。アニメーション中にPDFが生成されるとグラフが空白になるため、必ず無効化してください。

ヘッダー・フッター・ページ番号付き業務レポートの実装

業務レポートではA4全体にヘッダーとフッターが入ることが多いです。FUNBREW PDFのAPIオプションで設定できます。

ヘッダー・フッターのHTMLテンプレート

<!-- headerTemplate: ページ上部に表示(高さは margin.top と合わせる) -->
<div style="
  width: 100%;
  padding: 0 40px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 10px;
  color: #94a3b8;
  font-family: 'Hiragino Sans', sans-serif;
">
  <span>株式会社サンプル / 月次業績レポート</span>
  <span>CONFIDENTIAL</span>
</div>

<!-- footerTemplate: ページ下部に表示 -->
<div style="
  width: 100%;
  padding: 0 40px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 10px;
  color: #94a3b8;
  font-family: 'Hiragino Sans', sans-serif;
">
  <span>作成日: 2026年4月1日</span>
  <!-- <span class="pageNumber"> と <span class="totalPages"> はChromiumの組み込み変数 -->
  <span>ページ <span class="pageNumber"></span> / <span class="totalPages"></span></span>
</div>

class="pageNumber"class="totalPages" はChromium(Puppeteer)の組み込み変数で、ページ番号と総ページ数が自動的に挿入されます。

Node.jsでのAPI呼び出し実装

基本的なレポートPDF生成

import fs from 'fs';
import path from 'path';

const API_KEY = process.env.FUNBREW_PDF_API_KEY;
const API_URL = 'https://pdf.funbrew.cloud/api/v1/pdf/generate';

async function generateMonthlyReport(reportData) {
  // HTMLテンプレートを読み込み、変数を置換
  const templatePath = path.join('templates', 'monthly-report.html');
  let html = fs.readFileSync(templatePath, 'utf8');

  // プレースホルダーを実データで置換
  html = html
    .replace('{{period}}', reportData.period)
    .replace('{{created_at}}', new Date().toLocaleDateString('ja-JP'))
    .replace('{{revenue}}', reportData.revenue.toLocaleString())
    .replace('{{revenue_change}}', reportData.revenueChange)
    .replace('{{new_customers}}', reportData.newCustomers)
    .replace('{{customer_change}}', reportData.customerChange)
    .replace('{{conversion_rate}}', reportData.conversionRate)
    .replace('{{conversion_change}}', reportData.conversionChange)
    .replace('{{churn_rate}}', reportData.churnRate)
    // グラフデータはJSON形式で埋め込む
    .replace('{{months_json}}', JSON.stringify(reportData.months))
    .replace('{{revenue_data_json}}', JSON.stringify(reportData.revenueData))
    .replace('{{category_labels_json}}', JSON.stringify(reportData.categoryLabels))
    .replace('{{category_data_json}}', JSON.stringify(reportData.categoryData));

  const response = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      html,
      options: {
        format: 'A4',
        margin: {
          top: '60px',    // ヘッダー分の余白
          bottom: '50px', // フッター分の余白
          left: '0px',
          right: '0px',
        },
        displayHeaderFooter: true,
        headerTemplate: buildHeaderTemplate('月次業績レポート'),
        footerTemplate: buildFooterTemplate(),
        printBackground: true,
        waitForNetworkIdle: true,   // Chart.jsのCDN読み込みを待つ
      },
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`PDF generation failed: ${error.message}`);
  }

  // PDFバイナリを返す
  return Buffer.from(await response.arrayBuffer());
}

function buildHeaderTemplate(title) {
  return `
    <div style="width:100%;padding:0 40px;display:flex;justify-content:space-between;
      align-items:center;font-size:10px;color:#94a3b8;
      font-family:-apple-system,sans-serif;">
      <span>株式会社サンプル / ${title}</span>
      <span>CONFIDENTIAL</span>
    </div>`;
}

function buildFooterTemplate() {
  return `
    <div style="width:100%;padding:0 40px;display:flex;justify-content:space-between;
      align-items:center;font-size:10px;color:#94a3b8;
      font-family:-apple-system,sans-serif;">
      <span>${new Date().toLocaleDateString('ja-JP')} 作成</span>
      <span>ページ <span class="pageNumber"></span> / <span class="totalPages"></span></span>
    </div>`;
}

// 使用例
const pdfBuffer = await generateMonthlyReport({
  period: '2026年3月',
  revenue: 12500000,
  revenueChange: 8.3,
  newCustomers: 47,
  customerChange: 12.1,
  conversionRate: 23.4,
  conversionChange: -1.2,
  churnRate: 1.8,
  months: ['10月', '11月', '12月', '1月', '2月', '3月'],
  revenueData: [980, 1050, 1120, 1080, 1150, 1250],
  categoryLabels: ['SaaS', 'コンサル', '保守', 'その他'],
  categoryData: [65, 20, 10, 5],
});

fs.writeFileSync('monthly-report-2026-03.pdf', pdfBuffer);
console.log('レポート生成完了');

Pythonでの実装

import os
import json
import requests
from datetime import datetime
from pathlib import Path

API_KEY = os.environ["FUNBREW_PDF_API_KEY"]
API_URL = "https://pdf.funbrew.cloud/api/v1/pdf/generate"

def generate_monthly_report(report_data: dict) -> bytes:
    """月次レポートPDFを生成してバイト列で返す"""
    template_path = Path("templates") / "monthly-report.html"
    html = template_path.read_text(encoding="utf-8")

    # プレースホルダー置換
    replacements = {
        "{{period}}": report_data["period"],
        "{{created_at}}": datetime.now().strftime("%Y年%m月%d日"),
        "{{revenue}}": f"{report_data['revenue']:,}",
        "{{revenue_change}}": str(report_data["revenue_change"]),
        "{{new_customers}}": str(report_data["new_customers"]),
        "{{customer_change}}": str(report_data["customer_change"]),
        "{{conversion_rate}}": str(report_data["conversion_rate"]),
        "{{conversion_change}}": str(report_data["conversion_change"]),
        "{{churn_rate}}": str(report_data["churn_rate"]),
        "{{months_json}}": json.dumps(report_data["months"], ensure_ascii=False),
        "{{revenue_data_json}}": json.dumps(report_data["revenue_data"]),
        "{{category_labels_json}}": json.dumps(report_data["category_labels"], ensure_ascii=False),
        "{{category_data_json}}": json.dumps(report_data["category_data"]),
    }
    for placeholder, value in replacements.items():
        html = html.replace(placeholder, value)

    response = requests.post(
        API_URL,
        headers={"X-API-Key": API_KEY},
        json={
            "html": html,
            "options": {
                "format": "A4",
                "margin": {"top": "60px", "bottom": "50px", "left": "0px", "right": "0px"},
                "displayHeaderFooter": True,
                "headerTemplate": build_header("月次業績レポート"),
                "footerTemplate": build_footer(),
                "printBackground": True,
                "waitForNetworkIdle": True,
            },
        },
    )
    response.raise_for_status()
    return response.content


def build_header(title: str) -> str:
    style = "width:100%;padding:0 40px;display:flex;justify-content:space-between;" \
            "align-items:center;font-size:10px;color:#94a3b8;font-family:sans-serif;"
    return f'<div style="{style}"><span>株式会社サンプル / {title}</span><span>CONFIDENTIAL</span></div>'


def build_footer() -> str:
    style = "width:100%;padding:0 40px;display:flex;justify-content:space-between;" \
            "align-items:center;font-size:10px;color:#94a3b8;font-family:sans-serif;"
    date_str = datetime.now().strftime("%Y年%m月%d日")
    return (
        f'<div style="{style}">'
        f'<span>{date_str} 作成</span>'
        f'<span>ページ <span class="pageNumber"></span> / <span class="totalPages"></span></span>'
        f'</div>'
    )


# 使用例
if __name__ == "__main__":
    data = {
        "period": "2026年3月",
        "revenue": 12_500_000,
        "revenue_change": 8.3,
        "new_customers": 47,
        "customer_change": 12.1,
        "conversion_rate": 23.4,
        "conversion_change": -1.2,
        "churn_rate": 1.8,
        "months": ["10月", "11月", "12月", "1月", "2月", "3月"],
        "revenue_data": [980, 1050, 1120, 1080, 1150, 1250],
        "category_labels": ["SaaS", "コンサル", "保守", "その他"],
        "category_data": [65, 20, 10, 5],
    }
    pdf = generate_monthly_report(data)
    Path("monthly-report-2026-03.pdf").write_bytes(pdf)
    print("レポート生成完了")

cronによる定期実行スケジューリング

毎月1日の朝9時にレポートを自動生成・送信するには、cron(Unix/Linux)またはNode.jsのcronライブラリを使います。

Node.js(node-cron)

import cron from 'node-cron';
import { generateMonthlyReport } from './report-generator.js';
import { sendReportEmail } from './mailer.js';
import { fetchReportData } from './data-fetcher.js';

// 毎月1日 09:00 に実行
cron.schedule('0 9 1 * *', async () => {
  console.log('月次レポート生成開始:', new Date().toISOString());

  try {
    // 前月のデータを取得
    const lastMonth = getPreviousMonth();
    const reportData = await fetchReportData(lastMonth);

    // PDF生成
    const pdfBuffer = await generateMonthlyReport(reportData);

    // メール送信
    await sendReportEmail({
      to: ['ceo@example.com', 'cfo@example.com'],
      subject: `【月次レポート】${lastMonth.label}業績サマリー`,
      body: `${lastMonth.label}の月次業績レポートを添付します。`,
      attachment: {
        filename: `monthly-report-${lastMonth.slug}.pdf`,
        content: pdfBuffer,
        contentType: 'application/pdf',
      },
    });

    console.log('月次レポート送信完了');
  } catch (err) {
    console.error('レポート生成エラー:', err);
    // エラー通知(Slackなど)
    await notifyError(err);
  }
});

function getPreviousMonth() {
  const now = new Date();
  const year = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear();
  const month = now.getMonth() === 0 ? 12 : now.getMonth();
  return {
    label: `${year}年${month}月`,
    slug: `${year}-${String(month).padStart(2, '0')}`,
    year,
    month,
  };
}

Python(APScheduler)

from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetime
from dateutil.relativedelta import relativedelta
from report_generator import generate_monthly_report
from mailer import send_report_email
from data_fetcher import fetch_report_data

scheduler = BlockingScheduler(timezone="Asia/Tokyo")

@scheduler.scheduled_job("cron", day=1, hour=9, minute=0)
def monthly_report_job():
    print(f"月次レポート生成開始: {datetime.now()}")
    try:
        # 前月データを取得
        last_month = datetime.now() - relativedelta(months=1)
        report_data = fetch_report_data(last_month.year, last_month.month)

        # PDF生成
        pdf = generate_monthly_report(report_data)

        # メール送信
        send_report_email(
            to=["ceo@example.com", "cfo@example.com"],
            subject=f"【月次レポート】{last_month.year}年{last_month.month}月業績サマリー",
            pdf=pdf,
            filename=f"monthly-report-{last_month.strftime('%Y-%m')}.pdf",
        )
        print("月次レポート送信完了")
    except Exception as e:
        print(f"エラー: {e}")
        notify_error(e)

if __name__ == "__main__":
    scheduler.start()

Linuxのcrontabで設定する場合

# crontab -e で編集
# 毎月1日 09:00 に実行
0 9 1 * * cd /opt/myapp && node generate-report.js >> /var/log/report.log 2>&1

Webhookを使ったメール自動送信との連携

FUNBREW PDFのWebhookを利用すると、PDF生成完了後の処理(メール送信・Slack通知・DBへの記録)を非同期で行えます。詳しい実装方法はWebhook連携ガイドをご覧ください。

Webhookペイロードの例

{
  "event": "pdf.generated",
  "timestamp": "2026-04-01T09:00:05Z",
  "data": {
    "id": "pdf_rpt_abc123",
    "filename": "monthly-report-2026-03.pdf",
    "file_size": 182450,
    "download_url": "https://pdf.funbrew.cloud/dl/abc123?expires=1743600000",
    "generation_time_ms": 3200
  }
}

Webhookを受け取ってメール送信(Node.js)

import express from 'express';
import nodemailer from 'nodemailer';

const app = express();
app.use(express.json());

app.post('/webhooks/pdf-report', async (req, res) => {
  const { event, data } = req.body;

  if (event !== 'pdf.generated') {
    return res.json({ received: true });
  }

  // PDFのダウンロードURLをメール本文に含める
  const transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
  });

  await transporter.sendMail({
    from: 'reports@example.com',
    to: ['ceo@example.com', 'cfo@example.com'],
    subject: '【月次レポート】2026年3月分が完成しました',
    html: `
      <p>月次業績レポートが生成されました。</p>
      <p><a href="${data.download_url}">こちらからダウンロード</a>(24時間有効)</p>
      <p>ファイルサイズ: ${(data.file_size / 1024).toFixed(1)} KB</p>
    `,
  });

  res.json({ received: true });
});

app.listen(3000);

メール送信の基本的な実装については請求書PDFの自動送信も参考になります。

複数クライアント向けレポートの一括生成

代理店・コンサル企業では、クライアントごとにデータを差し替えてレポートを一括生成する需要があります。バッチ処理APIを使うと効率的です。

// 全クライアント分のレポートを一括生成
const clientReports = await Promise.all(
  clients.map(async (client) => {
    const data = await fetchClientData(client.id, targetMonth);
    const html = buildReportHtml(data, client.branding);
    return {
      clientId: client.id,
      html,
      filename: `report-${client.slug}-${targetMonth}.pdf`,
    };
  })
);

// バッチAPIで一度に送信
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/batch', {
  method: 'POST',
  headers: {
    'X-API-Key': API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: clientReports.map(r => ({
      html: r.html,
      filename: r.filename,
      options: {
        format: 'A4',
        margin: { top: '60px', bottom: '50px', left: '0px', right: '0px' },
        displayHeaderFooter: true,
        headerTemplate: buildHeaderTemplate(r.clientId),
        footerTemplate: buildFooterTemplate(),
        printBackground: true,
        waitForNetworkIdle: true,
      },
    })),
  }),
});

const { data } = await response.json();
console.log(`${data.results.length}件のクライアントレポートを生成しました`);

よくある質問

Chart.jsのグラフが空白になる

animation: false が設定されていない、もしくは waitForNetworkIdle: true でCDNの読み込みを待っていないことが原因です。CDNを使う代わりにChart.jsをHTMLにインライン埋め込みすると、ネットワーク依存がなくなり安定します。詳しい描画トラブルへの対処はHTML→PDF変換CSSガイドを参照してください。

A4以外のサイズには対応できるか

format オプションで "A3""Letter""Legal" などを指定できます。またピクセル指定("2480px 3508px" など)でカスタムサイズにも対応しています。

レポートのページ数が多い場合はどうする

特定の要素で強制的に改ページしたい場合は、CSSの page-break-before: always または break-before: page を使います。

<div style="break-before: page;">
  <!-- ここから新しいページ -->
</div>

日本語フォントが文字化けする

FUNBREW PDFにはNoto Sans JPがプリインストールされているため、font-family: 'Noto Sans JP', sans-serif; を指定するだけで日本語が正しく表示されます。追加のフォント設定は不要です。

セキュリティ面が不安

社外秘のビジネスレポートを扱う場合、通信はSSL/TLS暗号化、認証はAPIキー(X-API-Key ヘッダー)、生成ファイルのダウンロードURLには有効期限が設定されます。詳細はPDF APIセキュリティガイドをご確認ください。

Playgroundで試してみる

実際のコードを書く前に、PlaygroundでHTMLをペーストしてPDF出力を確認できます。グラフテンプレートのデバッグにも便利です。

まとめ

ビジネスレポートのPDF自動生成をまとめると、以下の4ステップで実現できます。

  1. HTMLテンプレートにChart.jsグラフを埋め込むanimation: false を忘れずに
  2. APIオプションでヘッダー・フッター・ページ番号を設定displayHeaderFooter: true + headerTemplate / footerTemplate
  3. cronまたはAPSchedulerで定期実行 — 前月データ取得→PDF生成→メール送信の一連フロー
  4. Webhookで非同期処理 — 生成完了後にSlack通知・メール送信をトリガー

手作業のレポート作業から解放されたい方は、まず無料プランで30件/月試せますPlaygroundでHTMLテンプレートをすぐに試し、ドキュメントでAPIオプションの詳細を確認してください。

関連リンク

Powered by FUNBREW PDF