Next.js API Routes × Cloudflare Workers OGP API by Codex


[前提]この記事は90%AIによって生成されました。



このメモは、ro1.dev で運用している OGP API 実装を、別サービスへ横展開するための実装仕様書です。
「LLM にこの文書を渡せば、最小の補足で実装できる」粒度でまとめています。

##結論


このブログでは OGP を次の2系統で実装しています。
  1. 動的生成系: pages/api で JSX -> SVG -> PNG を生成して返す
  2. プリビルド配信系: ビルド時に PNG を大量生成し、API は 302 redirect だけ返す

この分離が効いた理由は明確で、動的生成の柔軟性を残しつつ、Cloudflare Workers のサイズ・ランタイム制約を回避できるからです。

##1. 対象アーキテクチャ

###1-1. 実行基盤

  • Next.js 15 (Pages Router)
  • API: src/pages/api/**
  • デプロイ: OpenNext + Cloudflare Workers
  • Workers 設定: wrangler.jsonc

wrangler.jsonc の要点:

{
  "main": ".open-next/worker.js",
  "minify": true,
  "compatibility_flags": ["nodejs_compat"],
  "images": {
    "binding": "IMAGES"
  },
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  }
}

IMAGES binding は、Workers ランタイムで SVG -> PNG 変換するために必須です。

###1-2. ビルド導線


package.json

{
  "scripts": {
    "sync:content-snapshots": "node scripts/generate-content-snapshots.mjs",
    "generate:ogp-prebuilt": "node scripts/generate-ogp-prebuilt-assets.mjs",
    "prebuild": "bun run sync:content-snapshots && bun run generate:ogp-prebuilt",
    "build:cf": "opennextjs-cloudflare build"
  }
}

ポイントは prebuild で OGP 静的アセットも更新することです。

##2. OGP 実装のコア

共通実装は src/lib/og/image-response.tsx に集約しています。

  • 責務
    • satori で JSX から SVG を生成
    • Node 実行時: @resvg/resvg-wasm で PNG 化
    • Workers 実行時: env.IMAGES で PNG 化
    • フォントをローカル優先で読み込み(public/fonts -> remote fallback)

実質ここが「実行環境差分の吸収層」です。

###最小形

export const createImageResponse = async (element: ReactElement, options = {}) => {
  const svg = await satori(element, { width, height, fonts });
  const png = await renderPngFromSvg(svg, fonts); // Node: resvg / Workers: IMAGES
  return new Response(png, {
    status: 200,
    headers: { "content-type": "image/png" },
  });
};

##3. 系統A: 動的生成系 API

クエリで都度画像を作る

  • 実例

    • src/pages/api/app/ogp.tsx
    • src/pages/api/og/index.tsx
  • 実装パターン

    1. runtime: "nodejs" を宣言
    2. クエリを正規化(長さ制限、空文字処理)
    3. JSX レイアウトを createImageResponse に渡す
    4. 例外時は 500 を返す

###雛形

export const config = { runtime: "nodejs" };

export default async function handler(req: NextApiRequest) {
  const title = typeof req.query.title === "string"
    ? req.query.title.slice(0, 100)
    : "Default Title";

  return await createImageResponse(<Card title={title} />, {
    width: 1200,
    height: 630,
  });
}

この系統は「ユーザー入力で即時に OGP を変えるサービス」に向いています。

##4. 系統B: プリビルド配信系 API(大量記事向け)

  • 実例

    • src/pages/api/memo/ogp.ts
    • src/pages/api/ro1dev/ogp.tsx
  • 設計

    1. ビルド時に OGP PNG を public/ogp-api/** へ生成
    2. 同時に manifest JSON を src/lib/og/generated/*.json へ生成
    3. API route はクエリをキー化して manifest を引く
    4. 見つかれば 302 redirect、なければ default 画像へ redirect

###memo API の動作仕様

  • endpoint: /api/memo/ogp
  • query:
    • page=top | post
    • title (post時のみ)
    • date (post時のみ)
  • response:
    • 302 + /ogp-api/memo/...png
    • Cache-Control: public, max-age=0, s-maxage=86400, stale-while-revalidate=604800

###ro1dev API の動作仕様

  • endpoint: /api/ro1dev/ogp
  • query: title
  • response
    • 上と同様に 302

##5. キー生成ルール

src/lib/og/prebuilt-ogp-key.ts のルールを API 側と生成側で一致させています。

  • 空白正規化: trim + 連続空白を1個
  • 長さ制限:
    • memo title: 120
    • memo date: 40
    • ro1dev title: 100
  • memo key: title + "\u0001" + date

これがズレると「manifest にあるのにヒットしない」事故になります。

##6. 生成バッチ仕様(scripts/generate-ogp-prebuilt-assets.mjs

  • やっていること

    1. snapshot JSON から全記事データを読み込む
    2. SVG テンプレートにタイトル/日付を流し込む
    3. sharp で PNG に書き出す
    4. ハッシュ付きパスを発行して manifest を作る
    5. memo-ogp-manifest.json / ro1dev-ogp-manifest.json を更新
  • この方式の利点

    • API 実行時に重い画像生成が不要
    • Workers script サイズ増加を抑えられる
    • キャッシュ制御が単純になる

##7. ページ側との接続

ページコンポーネントは API URL を直接組み立てず、専用 helper を使っています。

  • src/lib/memo/ogp.ts
  • src/lib/og/ro1dev.ts

###

const ogImage = buildMemoPostOgpUrl({ title: post.title, date: post.date });

この helper 層を置くと、将来 endpoint やキー制約を変えてもページ側の変更を局所化できます。

##8. 障害パターンと対処

###よく出る障害

  1. env.IMAGES binding is not defined
  2. render2 is not a function / OGImageResponse is not a constructor
  3. Worker size 上限超過(3MiB制限)
  4. 本番だけ fs 読み込み失敗

###対処

  1. wrangler.jsoncimages.binding=IMAGES を明示
  2. OGP を共通 createImageResponse 経由に統一
  3. プリビルド画像 + redirect 方式へ寄せる
  4. コンテンツは snapshot JSON 化して prebuild で生成

##9. 横展開テンプレート

新規サービスへ移植する場合は以下をそのまま適用できます。

  1. src/lib/og/image-response.tsx 相当を作成
  2. 動的系 endpoint を1本作る(まずは最低限)
  3. 記事/投稿が多い領域だけプリビルド系 endpoint を追加
  4. scripts/generate-ogp-prebuilt-assets.mjs をサービス用テンプレへ複製
  5. prebuild に snapshot + prebuilt 生成を組み込む
  6. OGP URL builder helper を用意してページから利用

##10. LLMへ渡す実装プロンプト


以下をそのまま指示すれば、別プロダクトでも再現しやすいです。
Next.js + Cloudflare Workers で OGP API を実装してください。

要件:
- API Routes 配下に OGP endpoint を作成
- runtime は nodejs を使用
- JSX -> SVG は satori、PNG化は
  - Node: @resvg/resvg-wasm
  - Workers: Cloudflare IMAGES binding
- createImageResponse ユーティリティに環境差分を集約
- 大量コンテンツ向けに prebuilt OGP 方式を実装
  - build 時に PNG を public/ogp-api/** へ生成
  - manifest JSON を src/lib/og/generated/** に生成
  - API は manifest lookup + 302 redirect
- title/date の正規化と最大長を API と生成側で共通化
- page 側は helper で OGP URL を組み立てる
- Cache-Control は s-maxage + stale-while-revalidate を設定

成果物:
- 共通OGPレンダラ
- 動的OGP API 1本
- prebuilt OGP API 1本
- prebuild script
- OGP URL helper
- wrangler 設定(IMAGES binding 含む)

このテンプレをベースに、サービス固有のレイアウト(色・ロゴ・文言)だけ差し替えれば実戦投入できます。