Next.js ブログに動的 OG 画像を自動生成する
目次
課題
SNS でブログ記事をシェアしたとき、すべての記事に同じ汎用画像が表示されていた。記事の区別がつかないのは、2言語で定期的に記事を公開するブログとしてはまずい。
各記事のタイトル・カテゴリ・日付・著者名を含む固有の OG 画像を、ランタイムコストゼロでビルド時に自動生成したかった。
アプローチの選定
動的 OG 画像の生成にはいくつかの方法があるが、追加依存ゼロで済む Next.js App Router の opengraph-image.tsx ファイル規約 を採用した。
| 方式 | 利点 | 欠点 |
|---|---|---|
| 外部サービス(Cloudinary 等) | 設定が簡単 | 外部依存、コスト |
| Canvas API | 柔軟 | Node.js 環境でのセットアップが複雑 |
| opengraph-image.tsx | Next.js 組み込み、追加依存なし | Satori の制約あり |
決め手は、next/og(Satori + Resvg)が Next.js にバンドル済みであること、ルートセグメントにファイルを置くだけで og:image メタタグが自動設定されること、generateStaticParams でビルド時に全画像を静的生成できること。generateMetadata の手動変更も不要だ。
opengraph-image.tsx の仕組み
この仕組みがかなりエレガントだと感じた。opengraph-image.tsx をルートセグメントに配置すると、Next.js が自動的に以下を行う:
- default export 関数を呼び出して
ImageResponseを生成 - 生成された画像をそのルートの
.pngとして配信 og:image、og:image:width、og:image:height、twitter:imageメタタグをページの<head>に挿入
メタタグの手動管理は一切不要。ファイルベースのメタデータ API がすべてを処理してくれる。
Satori の落とし穴: インラインスタイルのみ
ドキュメントではあまり強調されていないが、Satori はインラインスタイルの Flexbox レイアウトしかサポートしていない。 className も Tailwind も CSS-in-JS も使えない。すべてのスタイルを style={{ }} オブジェクトで書く必要がある。これが OG 画像レイアウト設計における最大の制約だ。
レイアウトは React コンポーネントとして src/lib/og-image.tsx に定義し、ブログのモノクロデザインを踏襲した:
export function OgImageLayout({
title,
date,
category,
author,
siteName,
}: OgImageLayoutProps) {
return (
<div
style={{
width: "1200px",
height: "630px",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: "60px 80px",
backgroundColor: "#fafafa",
fontFamily: "IBM Plex Sans, IBM Plex Sans JP",
}}
>
{/* 上部: カテゴリバッジ + 日付 */}
{/* 中央: タイトル(長いタイトルは 52px → 42px に自動縮小) */}
{/* 下部: 著者名 + サイト名 */}
</div>
);
}デザイン上のポイント:
- タイトルの自動調整: 40文字を超えるとフォントサイズを 52px → 42px に縮小
- 配色: 背景
#fafafa、テキスト#111111、サブテキスト#737373でモノクロテーマと統一 - サイズ: 1200x630px(OGP 推奨サイズ)
フォントの扱い
Satori は CSS の @font-face を使えず、フォントデータを ArrayBuffer として渡す必要がある。IBM Plex Sans / IBM Plex Sans JP を使っているため、Bold の TTF ファイルをローカルにバンドルした:
src/assets/fonts/
IBMPlexSans-Bold.ttf # Latin 文字用(約 200KB)
IBMPlexSansJP-Bold.ttf # 日本語用(約 5.5MB)
外部 URL から fetch する方法もあるが、ビルド時のネットワーク障害を避けるためローカルバンドルを選択した。フォント読み込み関数はモジュールレベル変数にキャッシュし、複数記事の画像生成時にディスクを何度も読まないようにしている。
ルートの実装
opengraph-image.tsx はブログ記事のページと同じディレクトリに配置する。興味深い点として、page.tsx と opengraph-image.tsx の両方に同じ generateStaticParams ロジックが必要になるため、共通の generateBlogStaticParams() ユーティリティに切り出して重複を排除した。
ルート自体はフォントを読み込み、既存のヘルパー関数(getPostBySlug、getCategoryName、formatDate)で記事データを取得し、レイアウトコンポーネントとフォント設定を渡した ImageResponse を返す。alt、size、contentType の named export で Next.js にメタタグ生成方法を伝える。
まとめ
- ファイルベースのメタデータ API は強力 —
opengraph-image.tsxをルートセグメントに置くだけで、メタタグ生成をすべて自動化できる。Next.js App Router の過小評価されている機能の一つだ。 - Satori のインラインスタイル制約が最大の壁 — Flexbox とインラインスタイルを前提にレイアウトを設計すること。Tailwind コンポーネントの再利用は諦めた方がいい。
- バイリンガルサイトではフォントをローカルバンドルする — CJK フォントは大きい(日本語で約 5.5MB)が、代替手段はビルド時のネットワーク障害だ。モジュールレベルでフォントデータをキャッシュして、繰り返しの読み込みを避ける。
generateStaticParamsは共有する — ユーティリティに切り出すことで、page.tsxとopengraph-image.tsxの間のドリフトを防げる。
