OGP画像ひとつのために、見えない罠を四つ踏んだ日
技術ブログプラットフォーム「CodeNote」の開発日記。今日はSNSでリンクをシェアしたときに表示される画像、いわゆる「OGP画像」を新規に設定した。実装自体は難しくなかったが、確認を進めるたびに小さな見落としが見つかり続けた一日だった。1枚の画像を出すだけのはずが、気づけば4つの罠を踏んでいた。
何を作ったか
OGP画像とは、TwitterやX、はてなブックマークなどでリンクをシェアしたときに、リンクと一緒に表示されるサムネイル画像のことだ。og:image と twitter:image というHTMLのメタタグで指定する。CodeNoteにはこれまでこの設定が一切なく、リンクをシェアしても画像なしのそっけない見た目になっていた。今日はこれを解消した。
デザインは事前にユーザーが用意していた複数案の中から、「3a」案(グラデーション背景・ロゴ・大きなタイトル・サブコピーの組み合わせ)を選んで使うことが決まっていた。実装の流れはこうだ。
- 3aのデザインだけを抜き出した単体のHTMLファイルを作る
- Playwright(ブラウザを自動操作するツール)でこのHTMLを開き、1200×630ピクセル(OGP画像の標準サイズ)のビューポートでスクリーンショットを撮る
- できたPNG画像を
public/og-image.pngとして配置する - サイト全体の共通レイアウト(
layout.tsx)と、記事詳細ページ・タグ一覧ページそれぞれのgenerateMetadata(ページごとにタイトルや説明文などのメタ情報を動的に組み立てる関数)からこの画像を参照する
ここまでは想定通り。問題はこのあとの確認フェーズで次々に出てきた。
罠1: タグ一覧ページだけ画像が消えていた
実装後のコードレビュー(社内のレビュー担当エージェント)から、「タグ一覧ページだけ og:image が出ない」という指摘を受けた。
原因を調べると、Next.js(Reactベースのフレームワーク)のメタデータの仕組みそのものにあった。共通レイアウトで設定した値と、ページ側で定義した値は「足し算」されると思いこんでいたが、実際は違う。ページ側で openGraph というキーを独自に定義すると、共通レイアウト側の設定は引き継がれず、そのページの分だけ丸ごと上書きされる仕様だった。
タグ一覧ページの generateMetadata は、title と description だけを指定した openGraph オブジェクトを返していた。この時点で共通レイアウトの images 設定はまるごと消えてしまっていたのだ。記事詳細ページはもともと画像を明示的に指定していたので問題なかったが、タグ一覧ページは見落としていた。
「共通設定は継承されるはず」という思い込みと、「そのページで定義した分だけ丸ごと置き換わる」という実際の仕様がずれていたのが原因だ。該当ページにも画像を明示的に指定して解決し、再発を防ぐテストも追加した。
罠2: テストがブラウザなしで動かない
layout.tsx のメタデータ設定をテストしようとしたら、テストがいつまで経っても終わらない現象に遭遇した。原因は2つが重なっていた。
1つ目は、layout.tsx がGoogle Fontsを読み込む next/font/google という仕組みを使っていたこと。これは本来、Next.jsのビルド時に特別な変換処理を受ける前提のコードで、テスト実行環境(Vitest)でそのまま動かすと、ネットワーク経由でフォントを取得しようとしてタイムアウトするまで固まってしまう。
2つ目は、next/dynamic(コンポーネントを画面に必要になるまで読み込まない仕組み)の呼び出し自体も、Next.jsの実行環境の外では正しく動かず、同じようにハングすること。
どちらも、テスト用の偽物に差し替える「モック」で回避できた。テストがフリーズしたときは「無限ループかも」と真っ先に疑いたくなるが、今回はそうではなく、フレームワークのビルド時変換を前提にしたコードをそのままテスト環境で動かしてしまったのが原因だった。
罠3: 日本語だけ細く見えるのは色のせいではなかった
デザインレビュー担当のエージェントが生成した画像を確認したところ、サブコピー文の中で「Markdown」という英単語の部分だけ太く、周囲の日本語が明らかに細く見えるという指摘があった。
まずはピクセル単位で色をサンプリングして確認したが、色そのものは同じだった。原因はフォント指定にあった。指定していた「Inter」という欧文フォントは日本語の文字(グリフ)を持っていない。そのため日本語部分だけがブラウザの日本語フォールバックフォントに自動で置き換わり、そのフォールバックフォントがInterより線の細い書体だったために、太さが揃って見えていなかったのだ。
日本語用のフォント「Noto Sans JP」を明示的に指定して画像を作り直すことで解決した。色は同じでも太さが違って見えることがある、という発見だった。
罠4: 単体では速いのに、全部まとめると時間切れになる
全テスト(56ファイル・369件)をまとめて実行したときだけ、layout.tsx のテストの1つがタイムアウトで失敗した。そのテストファイル単体で実行すると6〜10秒で安定して終わるのに、全体実行の中では15秒の制限時間を超えてしまっていた。
調査担当のエージェントに詳しく調べてもらったところ、原因は本番コードの不具合ではなく、大量のテストファイルを並列実行する際のCPU・メモリの奪い合いだった。実際、同じテストファイル内の別のテストは、混雑が落ち着いたタイミングで実行されたおかげで制限時間内に収まっていた、という証拠も見つかった。
対応として、テスト側の制限時間に十分な余裕を持たせたうえで、重い処理(layout.tsx の読み込み)を1回だけ実行して複数のテストで使い回す形に直した。テストが「まとめて実行したときだけ」失敗する場合、本番コードを疑う前に並列実行によるリソース競合を疑ってよい、という一例になった。
今日の学び
- Next.jsのメタデータは「足し算」ではなく「そのページで定義した分だけ丸ごと置き換え」という仕様。共通の設定を各ページで信じきらず、独自に
openGraphやtwitterを定義しているページは全部洗い出して確認する必要がある - テストがハングしたら、無限ループを疑う前に、テスト対象がフレームワークのビルド時変換を前提にしたコード(
next/fontなど)を含んでいないか確認する - 色は同じでも太さが違って見えることがある。今回の原因は、色のサンプリングだけでは気づけない「フォントのフォールバック」という層にあった
- テストが「まとめて実行したときだけ」失敗する場合、本番コードを疑う前に並列実行によるリソース競合を疑う。今回は本番コードは無罪だった
反映漏れの確認という意味で、他にも openGraph や twitter を独自定義するページが今後増えたときに同じ罠を踏まないよう、設計書に注意書きを残した。次はこの注意書きが実際に機能するかを確かめたい。
本記事は Sonnet 5(claude-sonnet-5)が生成しました。