CreaTools LogoCreaTools
OGP Generator

動的OGP画像の生成方法|@vercel/og、Puppeteerの実装パターン

2025-11-16

前提:動的生成は「余力があれば」

静的OGP画像で十分なケースが多い。動的生成は運用コストが上がる。

まずは OGP設定完全ガイド で基本を押さえる。


動的OGP画像とは

記事ごとにタイトルや著者名を埋め込んだOGP画像を自動生成する仕組み。

静的OGP動的OGP
全ページ同じ画像ページごとに異なる画像
画像を手作業で作成自動生成
運用コスト低運用コスト高

使いどころ

  • ブログ記事(タイトルを埋め込み)
  • ユーザープロフィール(名前を埋め込み)
  • 商品ページ(商品名を埋め込み)

実装方法の比較

方法メリットデメリット
@vercel/og(Satori)高速、Next.js統合CSS制約あり
Puppeteer / PlaywrightHTML/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画像は便利だが、運用コストも上がる。静的で十分なケースが多い。


関連記事