Back to blog
|8 min read

OGP画像をSatoriで自動生成する仕組み

ブログ記事のOGP画像をSatori + Resvgでビルド時に自動生成する方法を解説。デザインの組み立て方からNext.jsとの統合まで。

Next.js
OGP
Satori

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 は内部で独自のレイアウトエンジンを動かしているので、gridposition: 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 画像も勝手にできる」という状態を作っておくと、ブログを書くハードルがひとつ減ります。