OGP Generator
動的OGP画像の生成方法|@vercel/og、Puppeteerの実装パターン
2025-11-16
前提:動的生成は「余力があれば」
静的OGP画像で十分なケースが多い。動的生成は運用コストが上がる。
まずは OGP設定完全ガイド で基本を押さえる。
動的OGP画像とは
記事ごとにタイトルや著者名を埋め込んだOGP画像を自動生成する仕組み。
| 静的OGP | 動的OGP |
|---|---|
| 全ページ同じ画像 | ページごとに異なる画像 |
| 画像を手作業で作成 | 自動生成 |
| 運用コスト低 | 運用コスト高 |
使いどころ
- ブログ記事(タイトルを埋め込み)
- ユーザープロフィール(名前を埋め込み)
- 商品ページ(商品名を埋め込み)
実装方法の比較
| 方法 | メリット | デメリット |
|---|---|---|
| @vercel/og(Satori) | 高速、Next.js統合 | CSS制約あり |
| Puppeteer / Playwright | HTML/CSSそのまま | 重い、サーバー必要 |
| Canvas API | 軽量 | 日本語フォントが面倒 |
| 外部サービス | 実装不要 | コスト、依存 |
Next.jsなら @vercel/og(Satori)一択。
@vercel/og での実装
基本構造
// app/api/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") ?? "Default Title";
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#111",
color: "#fff",
fontSize: 64,
fontWeight: 700,
}}
>
{title}
</div>
),
{ width: 1200, height: 630 }
);
}
metadataでの指定
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
const ogUrl = `/api/og?title=${encodeURIComponent(post.title)}`;
return {
openGraph: {
images: [ogUrl],
},
};
}
日本語フォントの読み込み
Satoriはデフォルトで日本語非対応。フォントを明示的に読み込む必要がある。
// app/api/og/route.tsx
import { ImageResponse } from "next/og";
export const runtime = "edge";
export async function GET(request: Request) {
// フォントファイルを読み込み
const notoSans = await fetch(
new URL("../../assets/NotoSansJP-Bold.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") ?? "タイトル";
return new ImageResponse(
(
<div
style={{
fontFamily: "Noto Sans JP",
fontSize: 48,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#fff",
color: "#111",
}}
>
{title}
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: notoSans,
style: "normal",
weight: 700,
},
],
}
);
}
フォントファイルの配置
app/assets/NotoSansJP-Bold.ttf に配置。
Google Fonts からダウンロード可能。
複雑なレイアウト
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
padding: 60,
backgroundColor: "#fff",
}}
>
{/* ロゴ */}
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<img
src="https://example.com/logo.png"
width={48}
height={48}
alt=""
/>
<span style={{ fontSize: 24, color: "#666" }}>サイト名</span>
</div>
{/* タイトル */}
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
fontSize: 56,
fontWeight: 700,
color: "#111",
lineHeight: 1.3,
}}
>
{title}
</div>
{/* フッター */}
<div style={{ fontSize: 20, color: "#999" }}>
{new Date().toLocaleDateString("ja-JP")}
</div>
</div>
),
{ width: 1200, height: 630 }
);
Satoriの制約
Flexboxベースだが、すべてのCSSが使えるわけではない。
使えるもの
- display: flex
- flexDirection, alignItems, justifyContent
- padding, margin, gap
- backgroundColor, color
- fontSize, fontWeight, lineHeight
- border, borderRadius
- position: absolute / relative
使えないもの
- Grid
- transform(一部)
- box-shadow(限定的)
- filter
- animation
キャッシュ戦略
OGP画像は頻繁に変わらないため、積極的にキャッシュ。
return new ImageResponse(jsx, {
width: 1200,
height: 630,
headers: {
"Cache-Control": "public, max-age=86400, s-maxage=86400",
},
});
Vercel Edge Functionsなら自動的にCDNキャッシュされる。
Puppeteer / Playwrightでの実装
HTMLテンプレートをそのままスクリーンショットする方法。
const puppeteer = require("puppeteer");
async function generateOgImage(title, outputPath) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(`
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700&display=swap" rel="stylesheet">
<style>
body {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
height: 630px;
background: #111;
color: #fff;
font-family: 'Noto Sans JP', sans-serif;
font-size: 48px;
}
</style>
</head>
<body>${title}</body>
</html>
`);
await page.screenshot({ path: outputPath });
await browser.close();
}
ビルド時に生成してstaticファイルとして配置する運用が一般的。
設計のポイント
| 観点 | 推奨 |
|---|---|
| タイトル文字数 | 30文字以内 |
| フォントサイズ | 48〜64px |
| 余白 | 上下左右60px以上 |
| コントラスト | 背景と文字で4.5:1以上 |
まとめ
| 判断 | 正解 |
|---|---|
| Next.jsで動的生成 | @vercel/og(Satori) |
| 日本語対応 | フォントファイルを読み込み |
| 複雑なレイアウト | Flexboxで組む |
| ビルド時生成 | Puppeteer |
| 静的で十分なら | 動的生成しない |
動的OGP画像は便利だが、運用コストも上がる。静的で十分なケースが多い。
関連記事
- OGP設定完全ガイド — 設定の判断基準
- OGPキャッシュ問題と解決策 — キャッシュクリアの詳細