OGP画像をSatoriで自動生成する仕組み
ブログ記事のOGP画像をSatori + Resvgでビルド時に自動生成する方法を解説。デザインの組み立て方からNext.jsとの統合まで。
OGP画像、手作りしてませんか?
ブログ記事を SNS でシェアしたとき、リンクカードに表示される画像。これが OGP(Open Graph Protocol)画像 です。
記事ごとにデザインツールで画像を作る方法もありますが、正直めんどくさい。記事を書くたびに Figma を開いてテキストを差し替えて書き出して…というのは続かないです。
このサイトでは、ビルド時にスクリプトが全記事の OGP 画像を自動生成しています。記事の MDX ファイルを置くだけで、タイトルやタグを反映した OGP 画像が勝手にできあがります。
技術スタック
使っているのはこの 2 つです。
- Satori — React 的な要素定義を SVG に変換するライブラリ(Vercel 製)
- Resvg — SVG を PNG にレンダリングするライブラリ(Rust 製、高速)
Vercel の @vercel/og を使ったことがある人もいるかもしれません。あれの内部で使われているのが Satori です。今回は Edge Runtime ではなくビルドスクリプトで使うので、Satori を直接使っています。
全体の流れ
MDX ファイル → gray-matter でメタデータ抽出 → Satori で SVG 生成 → Resvg で PNG 変換 → public/og/{slug}.png に保存
ビルドコマンドに組み込んであるので、npm run build するだけで全記事の OGP 画像が生成されます。
{
"scripts": {
"build": "node scripts/generate-og-images.mjs && next build",
"generate-og": "node scripts/generate-og-images.mjs"
}
}
Satori の仕組み
Satori の面白いところは、JSX っぽいオブジェクト構造で画像レイアウトを定義できる ことです。CSS の Flexbox がそのまま使えるので、Web フロントエンドの知識がそのまま活きます。
const element = {
type: 'div',
props: {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '60px 80px',
background: 'linear-gradient(135deg, #0a0a0f 0%, #111118 50%, #0d0d14 100%)',
},
children: [
// タイトルやタグなどの要素
],
},
}
ただし、使える CSS プロパティには制限があります。Satori は内部で独自のレイアウトエンジンを動かしているので、grid や position: absolute の一部挙動など、サポートされていないものもあります。基本的に Flexbox ベースで組む のが安全です。
デザインの組み立て
このサイトの OGP 画像は、こんな構成にしています。
┌────────────────────────────────────┐
│ [Tag1] [Tag2] [Tag3] │
│ │
│ 記事タイトル │
│ (長いタイトルは自動で文字サイズ調整) │
│ │
│────────────────────────────────────│
│ 🟣 Iori Nakata 2026/03/10│
│ iori-nakata.gohard.jp │
└────────────────────────────────────┘
ポイントをいくつか紹介します。
タイトルの文字サイズを動的に調整
タイトルが長いと画像からはみ出すので、文字数に応じてフォントサイズを変えています。
{
type: 'div',
props: {
style: {
fontSize: title.length > 30 ? '48px' : '56px',
fontWeight: 700,
color: '#ededf0',
lineHeight: 1.3,
},
children: title,
},
}
シンプルだけど効果的です。もっと細かく制御したければ、文字数の閾値を増やせばいい。
タグをバッジ風に表示
タグは border-radius: 9999px で丸い pill 形状にしています。背景は透明で、ボーダーだけ薄く見せる控えめなデザインです。
tags.map((tag) => ({
type: 'div',
props: {
style: {
padding: '6px 16px',
borderRadius: '9999px',
border: '1px solid rgba(140, 140, 180, 0.3)',
fontSize: '14px',
color: 'rgba(180, 180, 220, 0.8)',
},
children: tag,
},
}))
フォントの読み込み
Satori はフォントデータを直接渡す必要があります。Google Fonts の CSS を取得してフォント URL を抽出し、バイナリをダウンロードしています。
async function loadFont() {
const res = await fetch(
'https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap',
)
const css = await res.text()
const fontUrls = [...css.matchAll(/url\(([^)]+)\)/g)].map((m) => m[1])
return Promise.all(
fontUrls.map(async (url) => {
const fontRes = await fetch(url)
return Buffer.from(await fontRes.arrayBuffer())
}),
)
}
日本語フォントは Noto Sans JP、英数字は Inter を使っています。日本語と英語で異なるフォントを使い分けることで、両方きれいに表示されます。
SVG から PNG への変換
Satori が出力するのは SVG です。OGP 画像には PNG が必要なので、@resvg/resvg-js で変換します。
import { Resvg } from '@resvg/resvg-js'
const svg = await satori(element, {
width: 1200,
height: 630,
fonts,
})
const resvg = new Resvg(svg, {
fitTo: { mode: 'width', value: 1200 },
})
const pngData = resvg.render()
const pngBuffer = pngData.asPng()
fs.writeFileSync(`public/og/${slug}.png`, pngBuffer)
1200×630px は OGP 画像の推奨サイズです。Twitter(X)でも Facebook でもきれいに表示されます。
Next.js のメタデータとの連携
生成した画像は public/og/{slug}.png に置かれるので、Next.js の generateMetadata で参照するだけです。
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [`/og/${params.slug}.png`],
},
twitter: {
card: 'summary_large_image',
},
}
}
@vercel/og を使わなかった理由
@vercel/og は Edge Runtime 上で動的に OGP 画像を生成する仕組みです。リクエストのたびに画像を生成するので、柔軟性は高い。
ただ、このサイトの場合は記事数も少ないし、内容は静的です。わざわざリクエストごとに生成する理由がない。ビルド時に一括生成して静的ファイルとして配信するほうが、レスポンスも速いしシンプルです。
記事数が数百を超えてビルド時間が気になり始めたら、そのとき @vercel/og への移行を検討すればいいかなと思っています。
まとめ
- Satori + Resvg で OGP 画像をビルド時に自動生成
- Flexbox ベースのレイアウト定義なので、Web フロントエンドの知識がそのまま使える
npm run buildに組み込むだけで、記事を追加するたびに自動で画像が生成される- 少ない記事数なら動的生成(
@vercel/og)よりビルド時生成のほうがシンプル
「記事を書いたら OGP 画像も勝手にできる」という状態を作っておくと、ブログを書くハードルがひとつ減ります。