• #og-image
  • #cloudflare-workers
  • #performance
  • #build-optimization
  • #decision

OGイメージの Cloudflare Workers 化検討

結論: 見送り

検討の結果現在の静的生成方式を維持することにした。

理由

  1. 現在の構成で十分機能している
    • ビルド時間100秒は許容範囲
    • 55MBのdistサイズもCDN配信なら問題なし
  2. OGイメージへのアクセスは実際少ない
    • SNSシェア時のクローラーのみがアクセス
    • 1日1万アクセス程度のサイトでは、OGイメージアクセスはごく一部
  3. 静的生成のメリットが大きい
    • ビルド時に全画像がCDNに配置済み
    • アクセス時のレイテンシがほぼゼロ
    • 追加のWorkers運用コストなし
  4. 複雑さを増やすより、シンプルな構成を維持
    • 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 化した後:

  1. nuxt.config.ts から nuxt-og-image モジュールを削除
  2. app/components/OgImage/ を削除
  3. ビルドサイズが 55MB 削減

期待効果

項目BeforeAfter
OGイメージ生成時間100秒0秒
dist サイズ83MB28MB
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サイズ+55MB0MB
キャッシュ最初からCDNにある初回アクセスで生成→CDNキャッシュ
運用コストなしWorkers の管理が必要
柔軟性再ビルドが必要パラメータで動的変更可能

Workers 化が有効なケース

  • 頻繁にデプロイする(1日に何度も)→ ビルド時間削減の恩恵大
  • OGイメージにリアルタイムデータを表示(例:株価、スコア)
  • バリエーションが無限(ユーザー生成コンテンツなど)
  • パラメータで動的に変化する(例:クイズ結果)

静的生成で十分なケース

  • デプロイ頻度が低い(週数回程度)
  • OGイメージの種類が有限(ブログ記事、固定ページ)
  • シンプルな構成を維持したい

参考リンク

  • workers-og - Cloudflare Workers 用 OG 画像生成ライブラリ
  • Satori - HTML/CSS から SVG への変換ライブラリ
  • nuxt-og-image - Nuxt 用 OG 画像モジュール