OGP 表示されない問題と戦った記録
Next.js の App Router で OGP を設定して、X でカード表示されるまでに結構苦戦した時の備忘録。未実装だった時からの時系列順。
1. 一旦普通のアプローチで OGP を出せる様にしてみる
サイトには app/layout.tsx に title と description があるだけで、OpenGraph タグも Twitter Card タグも一切ない状態からスタート。
やったこと
app/layout.tsxにmetadataBase(https://www.ryu-reu.me)を設定し、デフォルト OGP と Twitter Card(summary)を追加app/blog/page.tsxに静的メタデータを追加app/robots.tsを新設。/upload、/login、/api/を Disallowapp/sitemap.tsを新設。Prisma から公開記事を全件取得して動的生成
起きたこと
記事詳細ページ(app/posts/[slug]/page.tsx)は "use client" だったので generateMetadata() が使えなかった。
解決策: app/posts/[slug]/layout.tsx を Server Component として新設し、そこに generateMetadata() を書いた。ページ本体はクライアントコンポーネントのままで OK。Next.js の App Router では、layout と page は独立してサーバー/クライアントを選べるのがポイント。
layout 側では Prisma から DB を取得し、本文から Markdown 記法や HTML タグを除去して先頭 120 文字を description として使うようにした。果たしてこれは本当に description なんでしょうかって脳内ライコスが話しかけてくる。
2. OGP 画像の動的生成
記事をシェアした時にタイトル入りのいい感じの画像を出したくなった。よくあるやつ。
app/api/og/route.tsx を新規作成
Next.js の ImageResponse(next/og、内部は Satori)を使って動的に画像を生成する API を作った。
slugクエリパラメータで記事を DB 取得- 本文から最初の画像 URL(
or<img src="url">)を抽出してサムネイルに - 背景色
#232946、タイトル白、サイト名ピンク(#eebbc3) - 画像サイズ: 1200x630(Twitter 推奨)
- サイト名の横にアバター画像
フォント指定
デフォルトのフォントだと味気なかったので、Google Fonts から Yusei Magic の ttf を fetch してImageResponseのfontsオプションに渡した。(これのせいで後でちょっと痛い目を見る)
const fontData = await fetch(
"https://fonts.gstatic.com/s/yuseimagic/v12/yYLt0hbAyuCmoo5wlhPkpjHR.ttf"
).then((res) => res.arrayBuffer());layout.tsx 側では、Twitter Card を summary_large_image に変更し、OGP 画像 URL を /api/og?slug=... に向けた。
3. X でカード画像が表示されない: その1
デプロイして実際に X でシェアしたらカード画像が出ない。
調査
メタタグを直接 fetch して確認したが、すべて正しく出力されていた。
<meta property="og:image" content="https://www.ryu-reu.me/api/og?slug=..." />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />問題ないように見える…
Twitter Card Validator の警告
Twitter Card Validator で検証したらこんな警告が出ていた。
WARN: The image URL https://www.ryu-reu.me/api/og?slug=...
specified by the 'twitter:image' metatag may be restricted
by the site's robots.txt file, which will prevent Twitter
from fetching it.原因: robots.txt
app/robots.ts で /api/ を全面 Disallow していたのが原因だった。
Twitterbot は robots.txt を尊重するため、/api/og?slug=... の画像 URL を取得しに来た時点でブロックされていた。OGP の画像 URL が /api/ 配下にあるのに、そのパスを robots.txt で拒否していたら、そりゃクローラーは画像を取れない。
教訓: robots.txt で /api/ を一括 Disallow すると、OGP 画像のようなクローラーに取得してほしいエンドポイントもブロックされる。
修正
robots.txt の設定を見直して /api/og を許可するように変更した。
3. X でカード画像が表示されない: その 2
robots.txt 直しても全然 OGP 表示されなくて泣いてた。 状況としては、"ブラウザで直接叩いて画像は出るが、opengraph.xyz などで見ると unreachable"
Gemini に聞いてみたらこのケースはクローラーのタイムアウトが原因なんだって。実際そうで、4.51 秒、ひどい時は 9.62 秒くらいかかっていて草だった。
試したこと
- DB 接続の排除
- フォントのローカル化
- Cache-Control ヘッダー
- Edge Runtime 化
- アバター base64 インライン化
DB 接続の排除
もともと API route 内で slug から DB を引いていたが、layout の generateMetadata() で既にデータを持っているのでわざわざ API 側で再取得する必要がなかった。
変更: クエリパラメータを ?slug=... から ?title=...&desc=... に変更し、API route 側は DB に一切触らないように。
フォントのローカル化
Google Fonts から毎リクエスト ttf を fetch していたのをやめて、public/fonts/YuseiMagic-Regular.ttf(約 3MB)をローカルに配置。fs/promises の readFile で読み込み、メモリにキャッシュする形にした。
let fontCache: ArrayBuffer | null = null;
async function getFont() {
if (fontCache) return fontCache;
const data = await readFile(
path.join(process.cwd(), "public/fonts/YuseiMagic-Regular.ttf")
);
fontCache = data.buffer;
return fontCache;
}Cache-Control ヘッダー
生成した画像のレスポンスに以下を追加。
Cache-Control: public, max-age=86400, stale-while-revalidate=604800ブラウザ 1 日キャッシュ + 7 日間の stale-while-revalidate。記事タイトルが頻繁に変わるものではないので、これで十分。
以下の二つは Claude に提案してもらって、そのまま実装してもらった
Edge Runtime 化
- runtime = "nodejs" → "edge" に変更
- fs/promises + path を全部消して、import.meta.url パターンに切り替えた
- フォント読み込みがモジュールスコープの fetch になってるから、ビルド時にバンドラーが解決してくれる。実行時は即 ArrayBuffer 返すだけ
アバター base64 インライン化
- me.png も同じく import.meta.url で読んで btoa で base64 変換
- data:image/png;base64,... を OgCard に直接渡すから、Satori が画像取りに行くネットワーク往復がゼロになった
4. ついでにやったこと
念のため以下も追加した。
twitter:siteとtwitter:creatorに自分の X アカウントopenGraph.siteNameに「りゆうの実験場」
<head prefix="og: http://ogp.me/ns# ..."> の追加も検討したが、Next.js App Router では <head> タグの属性を直接制御できない。ただ、モダンなクローラーは prefix 属性なしでも正常に OGP を読み取れるため、実害はなかった。
OgCard コンポーネントの切り出し
route.tsx 内の JSX が肥大化していたので、app/components/OgCard/OgCard.tsx として切り出した。Storybook からも確認できるようになり、デザイン調整がやりやすくなった。
5. vercel 無料枠故
The Edge Function "api/og" size is 2.37 MB and your plan size limit is 1 MB.
お金かけてなくてごめん。これはフォントのせい。
Edge Runtime -> Node.js に戻して GET 事無き
まとめ
- robots.txt を見直すんだ!!!!
- OPG 生成にかかってる時間をちゃんとチェックしような!!!!
最終的に OGP の生成は 9.62s → 623ms になった。元が遅いのもあるけど 15 倍で流石に大草原。