作品OGPを事前生成中心へ移行した理由と設計
orimemoでは、作品詳細ページのSNS共有カードをより安定して表示するために、 作品OGPの仕組みを 都度生成中心 から 事前生成中心の静的配信 に移行しました。
この記事では、
- 技術面でなぜその移行が合理的だったのか
- 仕様面でどこが重要だったのか
- どんな利点があるのか
- どの程度ほかのページやサービスにも転用しやすいのか
を、現在の実装に沿って整理します。
背景
作品ページのOGPは、単なるタイトル画像ではなく、 以下のような情報を含む「作品カード」として見せたい要件がありました。
- 作品画像
- タイトル
- カテゴリ
- 作者
- デザイナー
- 難易度
- 折った日
この要件自体は自然です。 SNS上で共有された時点で、作品の内容がひと目で伝わるからです。
一方で、OGP画像は通常の画面表示とは違い、 人間ではなく外部クローラが取得する画像です。 そのため、通常のアプリ画面よりも、以下を優先する必要があります。
- 常に公開URLだけで取得できること
- 短く安定したURLであること
- 失敗時にタイムアウトしにくいこと
- 画像形式とレスポンスヘッダが安定していること
以前の方式と、その合理性
最初の方式は、ページ側で作品情報をクエリに展開し、
/api/og/works?... に渡してその場でPNGを生成するものでした。
この方式にも合理性はありました。
1. DBアクセスを画像エンドポイントから切り離しやすい
ページ側で必要な情報を整形して渡してしまえば、 OGPエンドポイントは「与えられた情報を描画するだけ」で済みます。
- 依存関係が少ない
- 認証を避けやすい
- デバッグしやすい
という利点がありました。
2. 実装速度が速い
next/og の ImageResponse で描画し、
作品画像を埋め込めば、比較的短い実装でリッチなOGPを作れます。
3. 仕様的にも一応は成立する
OGPの仕様上は、og:image や twitter:image が
有効な画像URLを返せばよく、クエリ付きURL自体は問題ありません。
つまり、初期段階では十分に合理的な構成でした。
どこが限界だったのか
問題は、クローラが本番環境でその場生成を叩く ことでした。
とくにCloudflare Workers上では、
- 元画像の取得
webpの正規化- フォント読み込み
- JSXレイアウトの描画
- PNG生成
を1リクエスト内で完結させる必要があります。
これが重なると、作品によっては Worker の CPU 制限に達し、
Error 1102 と exceededCpu が発生しました。
ここで重要なのは、 「アクセスが多いから落ちた」のではなく、1回の生成コストが高すぎた」 という点です。
つまり、都度生成を速くする工夫だけでは限界があり、 アーキテクチャそのものを見直す必要がありました。
現在のアーキテクチャ
現在は、以下の構成にしています。
- 作品ページは
twitter:image/og:imageに短いURLを出す - URLは
https://orimemo.com/og/works/{workId}?version=...&rev=... - そのURLはまずR2上の生成済みPNGを探す
- あればそのまま返す
- なければ一度だけ生成してR2へ保存する
- 保存・更新時は
waitUntilで背景生成を走らせる - 既存の公開作品はバックフィルで先回り生成する
実態としては、完全なオンデマンド生成でも、 完全なビルド時固定画像でもありません。
orimemoの現在地は、 事前生成中心 + 未生成時の一回生成 + 保存後の背景温め というハイブリッドです。
この設計が技術的に合理的な理由
1. 高コスト処理をクローラのホットパスから外せる
SNSクローラが画像を取りに来た瞬間に、 重い変換と描画を毎回やるのが危険でした。
現在は、生成済みPNGがあれば、 クローラはR2に保存された静的画像を読むだけです。
これにより、CPU制限に達しにくくなります。
2. 画像形式を常に image/png に固定できる
元画像が jpg でも webp でも、
最終的に返すOGPはPNGです。
この統一はかなり重要です。 クローラごとのデコーダ差異や、可変な変換経路を減らせます。
3. URLを短く、意味的に安定させられる
現在のURLは workId ベースです。
長いクエリに作品情報を全展開しないため、
- URLが短い
- 共有しやすい
- クローラ視点でも扱いやすい
- 「この画像はこの作品のもの」と意味が明確
という状態になります。
4. 更新の無効化戦略を組み込みやすい
現在は version と rev をURLに含めています。
さらにR2の保存キーにも、
workId- 作品バージョン
- 更新時刻
- レンダラの版番号
を反映しています。
これにより、画像を差し替えたくなったときに 「古いキャッシュを壊しつつ、新しい画像だけを出す」がやりやすくなります。
5. 失敗点を分離できる
都度生成だと、クローラの1回のアクセスにすべての失敗が乗ります。
現在は少なくとも論点を分けられます。
- 作品データの取得
- OGP描画
- PNG保存
- 配信
どこが壊れたかを見分けやすくなり、運用もかなり楽になります。
仕様面での合理性
OGPまわりは、見た目だけでなく「クローラにどう見えるか」が本質です。 その意味で、今回の構成は仕様面でも合理的です。
公開URLだけで完結している
クローラは通常、ログインもCookieも持ちません。
そのため、twitter:image / og:image は
公開URLだけで完結している必要があります。
今回の /og/works/{id} はそこを満たしています。
robots.txt と競合しにくい
/api/ 配下の画像URLは、運用によっては
robots.txt とぶつかりやすくなります。
OGP専用の /og/ 配下に出したことで、
「共有用の公開画像」であることがURL構造としても明確になりました。
クローラ依存の差に強い
X、Slack、Discord、LINE、メッセージアプリ系のクローラは、 それぞれ細かな癖があります。
ただし共通しているのは、
- 短く安定した絶対URL
- 公開アクセス可能
- 標準的な画像形式
- タイムアウトしにくいレスポンス
を好むことです。
今回の構成は、その共通条件に寄せています。
利点
この構成の利点をまとめると、以下です。
安定性が高い
- CPU超過しにくい
- クローラが失敗しにくい
- 本番だけ壊れる、が起きにくい
運用しやすい
- バックフィルできる
- 作品更新後に背景で温められる
- 画像キーが決定的で追跡しやすい
キャッシュに強い
- R2とCDNに乗せやすい
- immutable戦略が取りやすい
- 再生成タイミングを制御しやすい
実装責務が明確
- metadata生成
- OGPデータ整形
- レンダリング
- 保存
- 配信
が分離されるので、保守しやすくなります。
汎用性
この設計は、折り紙作品OGPだけの特殊解ではありません。
次のようなケースにもそのまま応用できます。
- 記事ごとのSNSカード
- ユーザープロフィールの共有カード
- ECの商品カード
- イベントページの告知画像
- ダッシュボードのスナップショット共有
共通パターンは同じです。
- 表示用データを純関数で整形する
- 描画は専用のレンダラに閉じ込める
- 配信用URLは短く固定する
- 生成結果をオブジェクトストレージへ保存する
- 更新時にだけ再生成する
特に、serverless / edge 環境で重い画像合成を扱うなら、この構成はかなり汎用的です。
なぜ「完全静的」ではなくハイブリッドなのか
全作品を完全に事前生成しておく設計もありえます。 ただ、orimemoでは以下の事情があります。
- 作品が後から追加される
- 公開/非公開が切り替わる
- バージョンが増える
- 描画ロジックの更新で再生成したい
このため、 未生成なら一度だけ作る という逃げ道を残しておく方が実運用に向いています。
同時に、保存時の背景生成とバックフィルを組み合わせることで、 実際のクローラ到達時には大半が「生成済み」である状態を目指せます。
このバランスが、いまのorimemoでは最も実務的でした。
まとめ
作品OGPを事前生成中心へ移行した理由は単純で、 共有カードは「描けること」より「確実に返せること」の方が重要だからです。
今回の設計で得られたポイントは次の通りです。
- その場生成は初期実装としては合理的
- ただし本番クローラの取得経路では重すぎることがある
- OGPは短い公開URLと静的配信に寄せるほど安定する
- 事前生成、背景温め、バックフィルの組み合わせが運用に強い
- この構成はほかのSNSカードにも流用しやすい
OGPは見た目の装飾ではなく、配信設計そのものです。 とくにCloudflare Workersのような制約のある環境では、 「画像をどう描くか」よりも、 どのタイミングで描き、どこに保存し、どのURLで返すか まで含めて設計する方が、最終的には強い実装になります。
orimemo開発チーム
orimemoの技術的な側面を深掘りし、実装の舞台裏をお届けしています。 フィードバックや質問があれば、GitHubでお気軽にお声がけください。