SNS でリンクをシェアしたときに表示されるカードの画像(OGP 画像)は、記事ごとに内容を変えたくても、1枚ずつ手作業で用意するのは大変です。Next.js の App Router には、opengraph-image というファイル規約があり、next/og の ImageResponse を使えば JSX(HTML と CSS のような書き方)から OGP 画像をコードで動的に生成できます。この記事では、静的な画像ファイルを置く方法との違いから、opengraph-image.tsx の基本、size / contentType / alt のエクスポート、動的ルートで記事タイトルを画像に載せる方法、generateImageMetadata で複数画像を出す方法、twitter-image との関係、そしてフォントとスタイルの注意点までを解説します。
目次
静的な画像を置く方法とコードで生成する方法
OGP 画像の設定方法は大きく2通りあります。1つは、あらかじめ用意した画像ファイルをルートセグメント(app 以下のフォルダ)に置く方法です。opengraph-image.png(.jpg / .jpeg / .gif も可)というファイル名でフォルダに置くだけで、Next.js がそれを認識し、<head> に og:image などの <meta> タグを自動で出力してくれます。代替テキストを付けたいときは、同じフォルダに opengraph-image.alt.txt を置き、その中身がそのまま og:image:alt になります。
もう1つが、この記事の主題であるコードで画像を生成する方法です。opengraph-image.js / opengraph-image.ts / opengraph-image.tsx というファイルを作り、画像を返す関数を export default します。デザインを固定した1枚の画像で足りるなら静的ファイルで十分ですが、記事タイトルや著者名などページごとに変わる要素を画像に載せたい場合は、コードで生成する方法が向いています。以降ではこちらを詳しく見ていきます。
opengraph-image.tsx で ImageResponse を使う基本
画像を生成するもっとも簡単な方法は、next/og の ImageResponse を使うことです。opengraph-image.tsx の中で、画像として描きたい内容を JSX で組み立て、それを new ImageResponse(...) に渡して export default の関数から返します。JSX といっても中身は <div> と style で書く HTML/CSS に近いもので、それが1枚の画像に変換されます。
import { ImageResponse } from 'next/og';
// 画像のメタデータ
export const alt = 'Acme について';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
// 画像を生成する関数
export default function Image() {
return new ImageResponse(
(
// ImageResponse に渡す JSX 要素
<div
style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Acme について
</div>
),
{
// 上でエクスポートした size を、そのまま
// ImageResponse の width / height に流用できる
...size,
}
);
}
このファイルを app/opengraph-image.tsx として置くと、サイト全体の OGP 画像がこのコードから生成されます。<div> に width: '100%' と height: '100%' を指定し、display: 'flex' で中央寄せしているのがポイントです。ImageResponse の第2引数には画像の幅と高さを渡す必要があり、ここでは ...size と書くことで size のエクスポート値を再利用しています。生成された画像は既定ではビルド時に一度だけ作られてキャッシュされるため、リクエストのたびに描画されるわけではありません。
size・contentType・alt をエクスポートして情報を渡す
先ほどのコードにあった alt / size / contentType は、Next.js が特別に扱う設定用のエクスポートです。これらを書いておくと、Next.js が <head> に出力する <meta> タグ(og:image:width など)に反映されます。それぞれの意味は次の通りです。
| エクスポートする値 | 型 | 意味 |
|---|---|---|
alt | string | 画像の代替テキスト。og:image:alt として出力される |
size | { width: number; height: number } | 画像の幅と高さ。og:image:width / og:image:height になる |
contentType | string | 画像の MIME タイプ(例: image/png)。og:image:type になる |
OGP 画像の推奨サイズは幅 1200 × 高さ 630 ピクセルなので、size にはこの値を指定するのが定番です。contentType は生成する画像の形式に合わせ、PNG なら 'image/png' を指定します。これらのエクスポートは省略も可能ですが、SNS 側に正しい情報を伝えるためにも書いておくとよいでしょう。
動的ルートで記事タイトルを画像に載せる
コード生成の一番のメリットは、ページごとに内容の違う画像を作れることです。app/posts/[slug]/opengraph-image.tsx のように動的ルートの中に置くと、export default の関数が params を受け取れます。この params は Promise なので、await して中身を取り出します。取り出した slug をもとに記事データを取得し、そのタイトルを画像に描き込めます。
import { ImageResponse } from 'next/og';
export const alt = '記事のOGP画像';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
// params は Promise で渡されるので await で取り出す
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// slug を使って記事データを取得する
const post = await fetch(`https://example.com/api/posts/${slug}`).then(
(res) => res.json()
);
return new ImageResponse(
(
<div
style={{
fontSize: 60,
background: '#0f172a',
color: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 80,
textAlign: 'center',
}}
>
{post.title}
</div>
),
{ ...size }
);
}
params は、そのファイルが置かれたセグメントまでの動的パラメータをまとめたオブジェクトに解決される Promise です。たとえば /posts/hello にアクセスされたときは { slug: 'hello' } になります。await params で slug を取り出し、fetch で記事情報を読み込み、{post.title} をそのまま JSX に埋め込めば、記事ごとにタイトル入りの OGP 画像ができあがります。なお params が Promise になったのは比較的新しいバージョンの仕様なので、古いプロジェクトでは await なしのオブジェクトとして扱う場合がある点は覚えておくとよいでしょう。
generateImageMetadata で複数の画像を出す
1つのルートから複数の画像を出したいときは、同じファイル内で generateImageMetadata をエクスポートします。この関数は画像のメタデータの配列を返し、各要素には必ず id を含めます。すると export default の画像生成関数が、その id を Promise として受け取り、どの画像を生成しているのかを判別できます。
import { ImageResponse } from 'next/og';
// 生成する画像の一覧を返す。各要素に id が必須
export function generateImageMetadata() {
return [
{
id: 'ja',
alt: '日本語版のOGP画像',
size: { width: 1200, height: 630 },
contentType: 'image/png',
},
{
id: 'en',
alt: 'OGP image (English)',
size: { width: 1200, height: 630 },
contentType: 'image/png',
},
];
}
// id は Promise で渡されるので await で取り出す
export default async function Image({ id }: { id: Promise<string> }) {
const imageId = await id;
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 72,
background: '#000',
color: '#fafafa',
}}
>
{imageId === 'ja' ? '日本語版' : 'English'}
</div>
)
);
}
generateImageMetadata が返す各オブジェクトは、id(必須)のほかに alt・size・contentType を持てます。動的ルートの中で使えば generateImageMetadata も params を受け取れるので、記事データから画像の枚数や内容を動的に決めることもできます。言語別・バリエーション別の OGP 画像を1ファイルにまとめたいときに便利な仕組みです。
twitter-image との関係
X(旧 Twitter)向けのカード画像は、opengraph-image とまったく同じ仕組みの twitter-image ファイル規約で設定します。静的なら twitter-image.png、コード生成なら twitter-image.tsx を置くだけで、Next.js が twitter:image 系の <meta> タグを出力します。書き方は opengraph-image と同一で、ImageResponse を返し、alt / size / contentType をエクスポートできます。
両方に同じデザインの画像を出したい場合は、画像を生成する処理を別ファイルの関数に切り出して opengraph-image.tsx と twitter-image.tsx の双方から呼び出すと、コードの重複を避けられます。なお twitter-image のファイルサイズは 5MB、opengraph-image は 8MB を超えるとビルドが失敗する制限がある点にも注意してください。
フォントの読み込みと使えるスタイルの制約
ImageResponse は内部で Satori というライブラリを使って JSX を画像に変換しています。ここで2つ注意点があります。1つはフォント、もう1つは使える CSS の範囲です。
まずフォントですが、日本語などの文字をきれいに描くには、使いたいフォントファイルを読み込んで ImageResponse の fonts オプションに渡す必要があります。Node.js ランタイムでは node:fs/promises の readFile でプロジェクト内のフォントファイルを読み込み、その中身を data に指定します。process.cwd() はプロジェクトのルートを指すので、そこからの相対パスでフォントを配置します。
import { ImageResponse } from 'next/og';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function Image() {
// process.cwd() はプロジェクトのルートを指す
const notoSansJP = await readFile(
join(process.cwd(), 'assets/NotoSansJP-Bold.ttf')
);
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'Noto Sans JP',
fontSize: 80,
}}
>
日本語のOGP画像
</div>
),
{
...size,
fonts: [
{
name: 'Noto Sans JP',
data: notoSansJP,
style: 'normal',
weight: 700,
},
],
}
);
}
fonts には name(fontFamily で指定する名前)、data(読み込んだフォントデータ)、style、weight を渡します。フォントを渡さないと日本語が正しく表示されないことがあるため、日本語を載せるときはほぼ必須の手順です。
もう1つの制約が CSS です。Satori が対応しているのは Flexbox を中心とした CSS の一部で、すべてのプロパティが使えるわけではありません。特に、子要素を複数持つ <div> には display: 'flex' を明示的に指定する必要があります。また display: 'grid' のような高度なレイアウトはサポートされていないため、配置は基本的に Flexbox(flexDirection や alignItems、justifyContent)で組み立てると考えておくと安全です。普段の Web ページと同じ感覚で書くと崩れることがあるので、レイアウトはシンプルに保つのがコツです。
まとめ
App Router の opengraph-image ファイル規約を使うと、OGP 画像を静的ファイルで置くだけでなく、next/og の ImageResponse を使って JSX から動的に生成できます。opengraph-image.tsx で画像用の JSX を new ImageResponse(...) に渡して export default し、alt / size / contentType をエクスポートすれば、<meta> タグまで自動で整います。動的ルートでは params(Promise)を await して記事タイトルなどを画像に載せられ、generateImageMetadata を使えば1ファイルから複数の画像を出せます。X 向けの twitter-image も同じ書き方で設定でき、日本語を載せるときは fonts でフォントを読み込み、レイアウトは Flexbox 中心の制約の中で組む、という点を押さえておけば、記事ごとに映える OGP 画像を自動生成できます。