「毎月末に社内向け月次報告書を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ステップで実現できます。
- HTMLテンプレートにChart.jsグラフを埋め込む —
animation: falseを忘れずに - APIオプションでヘッダー・フッター・ページ番号を設定 —
displayHeaderFooter: true+headerTemplate/footerTemplate - cronまたはAPSchedulerで定期実行 — 前月データ取得→PDF生成→メール送信の一連フロー
- Webhookで非同期処理 — 生成完了後にSlack通知・メール送信をトリガー
手作業のレポート作業から解放されたい方は、まず無料プランで30件/月試せます。PlaygroundでHTMLテンプレートをすぐに試し、ドキュメントでAPIオプションの詳細を確認してください。
関連リンク
- レポート自動化のユースケース — ビジネスレポートの活用パターン
- PDF API Webhook連携ガイド — 生成完了通知・Slack連携
- PDF一括生成ガイド — 複数レポートの同時生成
- 請求書PDFの自動生成 — メール送信付きの自動化パターン
- HTML to PDF CSS tips — グラフ・レイアウトの描画ベストプラクティス
- PDF APIセキュリティガイド — 社外秘データの安全な取り扱い
- HTML to PDF APIクイックスタート — Node.js・Python・PHPの基本
- HTML to PDF API比較 2026年版 — 主要サービスとの比較
- PDFテンプレートエンジン入門 — 変数・ループ・条件分岐
- APIリファレンス — エンドポイントの詳細仕様