OGイメージの Cloudflare Workers 化検討
結論: 見送り
検討の結果現在の静的生成方式を維持することにした。
理由
- 現在の構成で十分機能している
- ビルド時間100秒は許容範囲
- 55MBのdistサイズもCDN配信なら問題なし
- OGイメージへのアクセスは実際少ない
- SNSシェア時のクローラーのみがアクセス
- 1日1万アクセス程度のサイトでは、OGイメージアクセスはごく一部
- 静的生成のメリットが大きい
- ビルド時に全画像がCDNに配置済み
- アクセス時のレイテンシがほぼゼロ
- 追加のWorkers運用コストなし
- 複雑さを増やすより、シンプルな構成を維持
- Workers化は「やれば速くなる」けど「やらなくても困らない」最適化
- メンテナンスコストを考えると現状維持が妥当
例外: 日本語クイズのOGイメージ
日本語クイズ(/japanese-writing-quiz/)のOGイメージは Workers で動的生成 している。
理由: クイズ結果(正解数/問題数)がパラメータで動的に変わるため、静的生成では対応できない。
/og/japanese-quiz?category=xxx&correct=5&total=10
このように動的なパラメータが必要なケースでは Workers 化が有効。
背景
現在OGイメージは nuxt-og-image で静的生成しており、ビルド時間とデプロイサイズに大きな影響を与えている。
現状の問題
| 項目 | 値 |
|---|---|
| OGイメージ数 | 359枚 |
| OGイメージサイズ | 55MB(dist全体の66%) |
| 推定生成時間 | 100秒(ビルド時間の52%) |
特にJリーグクラブページは60クラブ × 複数年度のバリエーションがあり、OGイメージ数が多い。
現在のアーキテクチャ
パターン1: Workers 動的生成(日本語クイズのみ)
既に apps/workers/og/ で動的生成を実装済み。
/og/japanese-quiz?category=xxx&correct=5&total=10
↓
Cloudflare Workers
↓
workers-og でPNG生成
実装: apps/workers/og/src/index.ts
import { ImageResponse, loadGoogleFont } from 'workers-og'
return new ImageResponse(html, {
width: 1200,
height: 630,
fonts: [{ name: 'Noto Sans JP', data: fontData }]
})
パターン2: nuxt-og-image 静的生成(その他全ページ)
ビルド時に全ページのOGイメージをプリレンダリング。
設定: nuxt.config.ts
ogImage: {
defaults: { width: 1200, height: 630, renderer: 'satori' },
fonts: ['Noto+Sans+JP:400', 'Noto+Sans+JP:700'],
compatibility: {
prerender: { satori: 'node', resvg: 'node' }
}
}
コンポーネント: app/components/OgImage/
OgImageTemplate.vue- 汎用テンプレートJLeagueOgImage.vue- Jリーグクラブ用
移行計画
フェーズ1: Jリーグクラブページの Workers 化
最もバリエーションが多いJリーグクラブページから開始。
1-1. Workers に Jリーグ用エンドポイント追加
// apps/workers/og/src/index.ts に追加
if (url.pathname === '/og/jleague') {
return handleJLeagueImage(url)
}
async function handleJLeagueImage(url: URL): Promise<Response> {
const clubName = url.searchParams.get('club') || ''
const clubColor = url.searchParams.get('color') || '#1a1a2e'
const html = `
<div style="width: 1200px; height: 630px; display: flex; flex-direction: column;
justify-content: center; align-items: center;
background: linear-gradient(135deg, ${clubColor} 0%, #1a1a2e 100%);
font-family: Noto Sans JP, sans-serif;">
<!-- SVGシールドアイコン -->
<svg ...>...</svg>
<div style="font-size: 64px; font-weight: 700; color: white;">
${clubName}
</div>
<div style="font-size: 32px; color: rgba(255,255,255,0.9); margin-top: 24px;">
財務データ分析
</div>
</div>
`
return new ImageResponse(html, {
width: 1200,
height: 630,
fonts: [{ name: 'Noto Sans JP', data: await getFontData() }]
})
}
1-2. Nuxt側でOGイメージURLを変更
<!-- pages/financial-quiz/jleague/club/[id].vue -->
<script setup>
const club = ... // クラブデータ取得
useHead({
meta: [
{
property: 'og:image',
content: `https://og-image-worker.number55number55.workers.dev/og/jleague?club=${encodeURIComponent(club.name)}&color=${encodeURIComponent(club.color)}`
}
]
})
</script>
1-3. nuxt-og-image からJリーグページを除外
// nuxt.config.ts
nitro: {
prerender: {
ignore: [
/\/financial-quiz\/jleague\/club\// // Workers で生成
]
}
}
フェーズ2: 汎用テンプレートの Workers 化
ブログ記事など、汎用的なOGイメージも Workers 化。
// /og/article?title=xxx&date=2025-12-30
async function handleArticleImage(url: URL): Promise<Response> {
const title = url.searchParams.get('title') || ''
const date = url.searchParams.get('date') || ''
// OgImageTemplate.vue と同等のHTMLを生成
const html = `...`
return new ImageResponse(html, { ... })
}
フェーズ3: nuxt-og-image の完全削除
全てのOGイメージを Workers 化した後:
nuxt.config.tsからnuxt-og-imageモジュールを削除app/components/OgImage/を削除- ビルドサイズが 55MB 削減
期待効果
| 項目 | Before | After |
|---|---|---|
| OGイメージ生成時間 | 100秒 | 0秒 |
| dist サイズ | 83MB | 28MB |
| OGイメージ更新 | 再ビルド必要 | 即時反映 |
実装の注意点
フォント読み込み
workers-og では Google Fonts からフォントを動的にロードする:
const fontData = await loadGoogleFont({
family: 'Noto Sans JP',
weight: 400,
text: '使用する文字のサブセット'
})
ポイント: text パラメータで使用文字を限定するとフォントサイズが小さくなり、レイテンシが改善。
SVG の取り扱い
workers-og は Satori ベースなので、SVG を直接 HTML に埋め込める。ただし、複雑なSVGは簡略化が必要な場合あり。
キャッシュ戦略
Workers でキャッシュヘッダーを設定:
return new ImageResponse(html, {
width: 1200,
height: 630,
headers: {
'Cache-Control': 'public, max-age=86400, s-maxage=604800'
}
})
関連ファイル
apps/workers/og/src/index.ts- 既存の Workers 実装apps/web/app/components/OgImage/JLeagueOgImage.vue- 移植対象のVueコンポーネントapps/web/app/components/OgImage/OgImageTemplate.vue- 汎用テンプレートapps/web/nuxt.config.ts- OGイメージ設定
検討時の比較
静的生成 vs Workers 動的生成
| 観点 | 静的生成(現在) | Workers 動的生成 |
|---|---|---|
| アクセス時レイテンシ | ほぼ0ms(CDN直接配信) | 初回50〜100ms、2回目以降0ms |
| ビルド時間 | +100秒 | 0秒 |
| distサイズ | +55MB | 0MB |
| キャッシュ | 最初からCDNにある | 初回アクセスで生成→CDNキャッシュ |
| 運用コスト | なし | Workers の管理が必要 |
| 柔軟性 | 再ビルドが必要 | パラメータで動的変更可能 |
Workers 化が有効なケース
- 頻繁にデプロイする(1日に何度も)→ ビルド時間削減の恩恵大
- OGイメージにリアルタイムデータを表示(例:株価、スコア)
- バリエーションが無限(ユーザー生成コンテンツなど)
- パラメータで動的に変化する(例:クイズ結果)
静的生成で十分なケース
- デプロイ頻度が低い(週数回程度)
- OGイメージの種類が有限(ブログ記事、固定ページ)
- シンプルな構成を維持したい
参考リンク
- workers-og - Cloudflare Workers 用 OG 画像生成ライブラリ
- Satori - HTML/CSS から SVG への変換ライブラリ
- nuxt-og-image - Nuxt 用 OG 画像モジュール