Next.jsでOGP画像を自動生成する【next/ogでテキストを描画しない理由】
このブログでは、SNSでシェアされたときに表示されるOGP画像(サムネイル)を、記事ごとに自動生成しています。画像を1枚1枚手作業で用意するのではなく、コードだけでカテゴリーごとに配色の違う画像を出し分ける仕組みです。この記事では、その実装内容と、途中でぶつかった「日本語テキストの文字化け」問題への対処法をまとめます。
💭 実装してみて思うこと
最初は「記事タイトルをそのまま画像に描画すればいいのでは」と安易に考えていたのですが、実際にやってみると日本語フォントの扱いが思ったより面倒でした。結果的に「テキストを描画しない」という割り切った方針に落ち着いたのですが、これはこれで運用の手間がかからず、今のところ気に入っています。
next/ogでOGP画像を自動生成する仕組み
Next.jsにはnext/ogという、JSXの記述からPNG画像を動的に生成できる仕組みが用意されています。opengraph-image.tsxというファイル名で配置しておくと、Next.js側が自動的にOGP用の画像エンドポイントとして認識してくれます。
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default function Image() {
return new ImageResponse(
(
<div style={{ width: "100%", height: "100%", display: "flex", backgroundColor: "#f1ece6" }}>
{/* ここにJSXで画像の中身を書く */}
</div>
),
{ ...size }
);
}このファイルをルート直下(src/app/opengraph-image.tsx)に置けばサイト全体のデフォルトOGP画像になり、src/app/blog/[slug]/opengraph-image.tsxのように動的ルート配下に置けば、記事ごとに異なる画像を生成できます。
記事ごとにカテゴリーで配色を変える
このブログでは記事を「AIツール」「レンタルサーバー」「ノーコード」「Next.js制作Tips」の4カテゴリーに分類しています。せっかく記事ごとにOGP画像を生成できるなら、カテゴリーが一目で分かるようにしたいと考え、カテゴリーごとに異なる図形の組み合わせを描画するようにしました。
export function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return new ImageResponse(
(
<div style={{ width: "100%", height: "100%", display: "flex", backgroundColor: "#f1ece6", position: "relative" }}>
{compositions[post.category]}
</div>
),
{ ...size }
);
}generateStaticParamsで全記事のslugを渡しておくことで、ビルド時に記事数ぶんのOGP画像がまとめて生成されます。カテゴリーごとの見た目は、円・角丸の四角形・三角形などをposition: absoluteで組み合わせただけのシンプルな構成ですが、記事一覧やSNSのタイムライン上で並んだときに「なんとなく系統が違う記事だ」と分かる程度の効果はあります。
あえて記事タイトルを画像に描画しなかった理由
当初は記事タイトルの文字列をそのままOGP画像に描画するつもりでした。ところがnext/ogはEdge Runtime上で動作する都合上、日本語フォントを別途読み込んで渡さないと、環境によっては文字化け(いわゆる豆腐文字)が発生するリスクがあります。フォントファイルをfetchで読み込んでImageResponseのfontsオプションに渡す方法自体は存在するのですが、
- ビルド時にフォントファイルの読み込みが失敗すると全記事のOGP生成がコケる
- Webフォントよりファイルサイズの大きい日本語フォントを毎回埋め込むことになる
- 記事タイトルが長い場合の折り返し・省略処理も自前で書く必要がある
といった手間が重なるため、今回は「OGP画像にはテキストを描画しない」という割り切った方針にしました。実際にSNSでシェアされたときの見た目を確認すると、画像に文字がなくてもog:title・twitter:titleとしてタイトル文字列自体はSNS側が別途表示してくれるため、情報としては欠けません。画像側は色と図形だけでカテゴリーの雰囲気を伝える、という役割分担です。
実装してみて気づいたこと
- Edge Runtimeの制約は事前に調べておく:
next/ogはNode.jsの通常のAPI Routesとは動作環境が異なるため、使えるライブラリ・使えないライブラリがある点は最初に確認しておくとよさそうです - 「完璧を目指さない」判断も実装のうち: 日本語テキスト描画にこだわって時間をかけるより、テキストなしで運用を割り切った方が、結果的に全記事に安定してOGP画像を出せる状態を早く作れました
- 静的生成なので実行時コストがかからない:
generateStaticParamsでビルド時に画像を生成しておく方式のため、記事を閲覧するたびに画像生成処理が走るわけではありません
よくある質問
Q. OGP画像が反映されない・古い画像のままSNSに表示されます
A. X(旧Twitter)やFacebookなど多くのSNSは、一度取得したOGP情報をキャッシュします。各SNSが提供しているデバッグツール(Card ValidatorやシェアデバッガーなどのURLプレビュー確認ツール)でURLを再取得させると、最新の画像に更新されることが多いです。
Q. opengraph-image.tsxとog:imageメタタグを両方設定する必要がありますか?
A. いいえ、opengraph-image.tsx(または.jpg/.pngなどの静的画像ファイル)を規約通りの場所に置いておけば、Next.jsがog:imageタグの出力まで自動で面倒を見てくれます。手動でmetadataにog:imageを書く必要はありません。
Next.jsの実装TipsについてはApp RouterとPages Routerの違いや画像・フォント最適化の記事でも扱っているので、あわせてどうぞ。