Astro で動的 OG 画像を生成する:Satori/Canvas 連携の実装レシピ

SNS でシェアされたときに表示される OG 画像(Open Graph Image)は、クリック率を大きく左右する重要な要素です。しかし、記事ごとに画像を手動で作成するのは手間がかかりますよね。Astro を使えば、記事のタイトルや説明文から自動的に OG 画像を生成できるんです。
本記事では、Astro で Satori と Canvas を組み合わせて、動的に OG 画像を生成する実装方法を詳しく解説します。React コンポーネントのような JSX で画像デザインを定義でき、フォントやレイアウトも自由にカスタマイズできますよ。
背景
OG 画像の重要性と課題
Open Graph 画像は、Twitter(X)や Facebook などの SNS で記事がシェアされる際に表示されるサムネイル画像のことです。魅力的な OG 画像があると、ユーザーの目に留まりやすく、クリック率が向上します。
従来の OG 画像生成には、いくつかの選択肢がありました。
# | 方法 | メリット | デメリット |
---|---|---|---|
1 | 手動作成 | デザインの自由度が高い | 記事ごとに作業が必要で非効率 |
2 | 外部サービス | 簡単に導入できる | カスタマイズ性が低く、コストがかかる場合も |
3 | サーバーサイド生成 | 自動化できる | サーバー負荷が増加し、複雑な実装が必要 |
静的サイトジェネレーターである Astro を使う場合、ビルド時に OG 画像を生成できれば、サーバー負荷を気にすることなく、完全に自動化された画像生成が実現できます。
Satori と Canvas の役割
Satori は、Vercel が開発した HTML/CSS を画像に変換するライブラリです。JSX ライクな記法でデザインを定義でき、React コンポーネントを書くような感覚で画像を生成できるのが特徴ですね。
一方、Canvas API は、Node.js 環境でブラウザの Canvas 機能を利用できるようにするライブラリです。Satori が SVG を生成した後、それを PNG などの画像フォーマットに変換する役割を担います。
以下の図は、Astro で OG 画像が生成される基本的なフローです。
mermaidflowchart LR
content["記事コンテンツ<br/>(タイトル・説明)"] --> satori["Satori<br/>(JSX → SVG 変換)"]
satori --> resvg["resvg-js<br/>(SVG → PNG 変換)"]
resvg --> output["OG 画像<br/>(PNG ファイル)"]
astro["Astro ビルド"] --> content
図で理解できる要点:
- Astro ビルド時に記事のメタデータを取得
- Satori が JSX から SVG に変換
- resvg-js(または Canvas)が PNG に変換
課題
動的 OG 画像生成における技術的課題
Astro で動的に OG 画像を生成する際、いくつかの技術的な課題があります。
エンドポイントでの画像生成
Astro では、API エンドポイント(.ts
または .js
ファイル)を使って動的なレスポンスを返せます。しかし、画像を生成して返すには、適切な Content-Type ヘッダーの設定やバイナリデータの処理が必要です。
日本語フォントの扱い
Satori はデフォルトでは日本語フォントを含んでいません。そのため、日本語のタイトルを含む OG 画像を生成する場合、カスタムフォントを読み込む必要があります。フォントファイルのサイズが大きいと、ビルド時間やバンドルサイズに影響を与える可能性もあるんです。
ビルド時 vs ランタイム生成
静的サイトの場合、すべてのページの OG 画像をビルド時に生成するのが理想的ですが、ページ数が多いとビルド時間が長くなる懸念があります。一方、ランタイムで生成する場合は、サーバーレス関数のコールドスタート時間やメモリ使用量を考慮する必要があります。
以下の図は、ビルド時とランタイムでの生成方式の違いを示しています。
mermaidflowchart TB
subgraph build["ビルド時生成"]
direction LR
pages1["全ページ情報"] --> gen1["画像生成処理"]
gen1 --> static1["静的ファイル<br/>(public/)"]
end
subgraph runtime["ランタイム生成"]
direction LR
request["リクエスト"] --> endpoint["API エンドポイント"]
endpoint --> gen2["画像生成処理"]
gen2 --> response["画像レスポンス"]
end
build -->|メリット:高速配信| merit1["CDN 配信可能"]
build -->|デメリット:ビルド時間| demerit1["大量ページで時間増"]
runtime -->|メリット:柔軟性| merit2["動的パラメータ対応"]
runtime -->|デメリット:レイテンシ| demerit2["毎回生成のコスト"]
図で理解できる要点:
- ビルド時生成は CDN で高速配信できるが、ページ数が多いと時間がかかる
- ランタイム生成は柔軟だが、リクエストごとに生成コストが発生
- プロジェクトの規模に応じて最適な方式を選択する
解決策
Satori と resvg-js による画像生成アーキテクチャ
Astro で OG 画像を動的生成するには、Satori と resvg-js を組み合わせるのが最も効率的です。resvg-js は、Rust で書かれた高速な SVG レンダリングエンジンで、Node.js から利用できます。
実装の基本的な流れは以下のとおりです。
- パッケージのインストール:必要なライブラリを追加
- フォントの準備:日本語対応のフォントファイルを用意
- API エンドポイントの作成:画像生成ロジックを実装
- JSX テンプレートの定義:OG 画像のデザインを記述
- メタタグの設定:生成した画像を OG タグで参照
この構成により、記事のタイトルや説明文をパラメータとして受け取り、自動的に美しい OG 画像を生成できるようになります。
パッケージのインストール
まず、必要なパッケージをインストールしましょう。Satori と resvg-js の 2 つが主要なライブラリです。
bashyarn add satori @resvg/resvg-js
これらのパッケージの役割は以下のとおりです。
# | パッケージ | 役割 | バージョン例 |
---|---|---|---|
1 | satori | HTML/CSS を SVG に変換 | ^0.10.0 |
2 | @resvg/resvg-js | SVG を PNG に変換(Rust ベース) | ^2.6.0 |
resvg-js は、従来の Canvas API よりも高速で、依存関係も少ないため、特に推奨されますよ。
日本語フォントの準備
日本語のタイトルを表示するには、カスタムフォントが必要です。Google Fonts から Noto Sans Japanese をダウンロードして、プロジェクトに配置します。
bash# public/fonts ディレクトリを作成
mkdir -p public/fonts
# Noto Sans JP をダウンロード(例)
# Google Fonts から手動でダウンロードするか、以下のコマンドを実行
フォントファイルは public/fonts/NotoSansJP-Bold.ttf
のような形で配置するのがおすすめです。これにより、ビルド後も静的アセットとしてアクセスできます。
具体例
API エンドポイントの実装
Astro では、src/pages/
ディレクトリに .ts
ファイルを配置することで API エンドポイントを作成できます。OG 画像生成用のエンドポイントを作成しましょう。
ファイル構成
typescript// src/pages/og-image.png.ts
このファイルは /og-image.png
というパスでアクセスできるようになります。クエリパラメータで title
や description
を受け取る形にします。
必要なインポート
まず、必要なライブラリをインポートします。
typescriptimport type { APIRoute } from 'astro';
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
APIRoute
は Astro の API エンドポイント型定義ですsatori
が JSX を SVG に変換しますResvg
が SVG を PNG に変換する役割を担います- Node.js の
fs
とpath
モジュールでフォントファイルを読み込みます
フォント読み込み関数
フォントファイルを読み込む関数を定義します。この関数はビルド時に一度だけ実行されるように、エンドポイント外で定義するのがポイントです。
typescript/**
* 日本語フォントを読み込む関数
* public/fonts ディレクトリから Noto Sans JP を取得
*/
async function loadFont() {
const fontPath = join(
process.cwd(),
'public',
'fonts',
'NotoSansJP-Bold.ttf'
);
const fontData = await readFile(fontPath);
return fontData;
}
process.cwd()
でプロジェクトのルートディレクトリを取得し、そこからフォントファイルのパスを構築しています。
OG 画像テンプレートの定義
次に、OG 画像のデザインを JSX で定義します。Satori は React コンポーネントのような記法をサポートしていますが、すべての CSS プロパティが利用できるわけではない点に注意しましょう。
typescript/**
* OG 画像のテンプレート
* Flexbox を使ったレイアウトで、タイトルと説明文を配置
*/
function OGImageTemplate(
title: string,
description: string
) {
return {
type: 'div',
props: {
style: {
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1a1a2e',
fontFamily: 'Noto Sans JP',
padding: '60px',
},
children: [
{
type: 'div',
props: {
style: {
fontSize: '64px',
fontWeight: 'bold',
color: '#ffffff',
marginBottom: '32px',
textAlign: 'center',
lineHeight: '1.3',
},
children: title,
},
},
{
type: 'div',
props: {
style: {
fontSize: '32px',
color: '#a0a0a0',
textAlign: 'center',
lineHeight: '1.5',
},
children: description,
},
},
],
},
};
}
OG 画像の推奨サイズは 1200×630 ピクセル です。背景色は濃いめの色(#1a1a2e
)を使い、テキストは白(#ffffff
)で視認性を高めています。
GET ハンドラーの実装(パラメータ取得)
API エンドポイントのメイン処理を実装します。まず、クエリパラメータから title
と description
を取得する部分です。
typescriptexport const GET: APIRoute = async ({ url }) => {
// クエリパラメータから title と description を取得
const title = url.searchParams.get('title') || 'タイトルなし';
const description = url.searchParams.get('description') || '説明なし';
url.searchParams.get()
で安全にパラメータを取得し、未指定の場合はデフォルト値を使用します。
SVG 生成処理
Satori を使って JSX テンプレートから SVG を生成します。
typescript// フォントデータを読み込み
const fontData = await loadFont();
// Satori で JSX を SVG に変換
const svg = await satori(
OGImageTemplate(title, description),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Noto Sans JP',
data: fontData,
weight: 700,
style: 'normal',
},
],
}
);
fonts
オプションで読み込んだフォントデータを指定しています。weight: 700
は太字(Bold)を意味しますよ。
PNG 変換とレスポンス返却
生成された SVG を PNG に変換し、HTTP レスポンスとして返します。
typescript // resvg で SVG を PNG に変換
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: 1200,
},
});
const pngData = resvg.render();
const pngBuffer = pngData.asPng();
// PNG 画像をレスポンスとして返却
return new Response(pngBuffer, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
};
Cache-Control
ヘッダーで長期キャッシュを設定することで、同じパラメータでのリクエストを効率化できます。
以下の図は、API エンドポイントでの処理フローです。
mermaidsequenceDiagram
participant Client as ブラウザ
participant API as /og-image.png
participant Satori as Satori
participant Resvg as resvg-js
participant FS as ファイルシステム
Client->>API: GET /og-image.png?title=記事タイトル
API->>FS: フォントファイル読み込み
FS-->>API: フォントデータ
API->>Satori: JSX テンプレート + フォント
Satori-->>API: SVG データ
API->>Resvg: SVG → PNG 変換
Resvg-->>API: PNG バイナリ
API-->>Client: PNG 画像 (Content-Type: image/png)
図で理解できる要点:
- クライアントからのリクエストでタイトルを受け取る
- フォントを読み込み、Satori で SVG を生成
- resvg-js で PNG に変換してレスポンス
Astro ページでの OG タグ設定
生成した OG 画像を実際のページで使用するには、<meta>
タグを設定する必要があります。Astro コンポーネントで実装してみましょう。
レイアウトコンポーネントでの実装
astro---
// src/layouts/BaseLayout.astro
interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
// OG 画像の URL を構築
const ogImageUrl = new URL('/og-image.png', Astro.url);
ogImageUrl.searchParams.set('title', title);
ogImageUrl.searchParams.set('description', description);
---
Astro.url
を使うことで、現在のページの完全な URL を取得でき、OG 画像の絶対 URL を正しく構築できます。
メタタグの配置
生成した URL を使って、Open Graph と Twitter Card のメタタグを設定します。
astro<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="description" content={description} />
<!-- Open Graph タグ -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImageUrl.toString()} />
<meta property="og:type" content="article" />
<!-- Twitter Card タグ -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageUrl.toString()} />
</head>
<body>
<slot />
</body>
</html>
summary_large_image
を指定することで、Twitter(X)で大きなカード形式で表示されるようになりますよ。
ブログ記事での使用例
実際のブログ記事ページで、このレイアウトを使用する例です。
astro---
// src/pages/blog/my-article.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
const title = 'Astro で動的 OG 画像を生成する方法';
const description = 'Satori と Canvas を使った実装レシピを紹介します';
---
<BaseLayout title={title} description={description}>
<article>
<h1>{title}</h1>
<p>{description}</p>
<!-- 記事本文 -->
</article>
</BaseLayout>
このように記述するだけで、自動的に記事タイトルに合わせた OG 画像が生成され、SNS でのシェア時に表示されます。
カスタマイズとスタイリング
基本的な実装ができたら、デザインをさらにカスタマイズしてみましょう。
グラデーション背景の追加
背景に美しいグラデーションを追加すると、より洗練された印象になります。
typescript// OG 画像テンプレートのスタイル部分を変更
{
type: 'div',
props: {
style: {
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
fontFamily: 'Noto Sans JP',
padding: '60px',
},
// ...
},
}
ただし、Satori では CSS の linear-gradient
が完全にはサポートされていないため、代わりに複数の div
を重ねて疑似的にグラデーション効果を作る方法もあります。
ロゴやアイコンの追加
サイトのブランディングを強化するために、ロゴ画像を追加することもできます。
typescript// 画像を Base64 エンコードして埋め込む
{
type: 'img',
props: {
src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...',
style: {
width: '80px',
height: '80px',
position: 'absolute',
top: '40px',
left: '40px',
},
},
}
画像は Base64 エンコードして data:
URI として埋め込むか、外部 URL を指定できます。ただし、外部 URL の場合はビルド時にアクセスできる必要があることに注意しましょう。
パフォーマンス最適化
OG 画像生成のパフォーマンスを最適化するためのテクニックをいくつか紹介します。
フォントのキャッシング
フォントファイルの読み込みは比較的コストが高い処理なので、一度読み込んだらキャッシュするのが効果的です。
typescript// モジュールレベルでキャッシュ
let fontCache: Buffer | null = null;
async function loadFont() {
if (fontCache) {
return fontCache;
}
const fontPath = join(
process.cwd(),
'public',
'fonts',
'NotoSansJP-Bold.ttf'
);
fontCache = await readFile(fontPath);
return fontCache;
}
この実装により、2 回目以降のリクエストではファイル I/O をスキップできるため、レスポンス時間が大幅に改善されます。
ビルド時の静的生成
ページ数が限られている場合は、ビルド時にすべての OG 画像を事前生成する方法も検討できます。
typescript// src/pages/og/[slug].png.ts
import type { APIRoute } from 'astro';
import { getCollection } from 'astro:content';
// すべてのブログ記事のスラッグを取得
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.slug },
props: {
title: post.data.title,
description: post.data.description,
},
}));
}
export const GET: APIRoute = async ({ props }) => {
const { title, description } = props;
// 画像生成処理(前述と同じ)
// ...
};
この方法では、/og/my-article.png
のような静的な URL で OG 画像にアクセスでき、CDN によるキャッシングも活用できますよ。
エラーハンドリングの追加
本番環境では、適切なエラーハンドリングが重要です。
typescriptexport const GET: APIRoute = async ({ url }) => {
try {
const title =
url.searchParams.get('title') || 'タイトルなし';
const description =
url.searchParams.get('description') || '説明なし';
// 画像生成処理
// ...
return new Response(pngBuffer, {
headers: {
'Content-Type': 'image/png',
'Cache-Control':
'public, max-age=31536000, immutable',
},
});
} catch (error) {
console.error('OG 画像生成エラー:', error);
// エラー時はデフォルト画像を返す、または 500 エラー
return new Response('Internal Server Error', {
status: 500,
headers: {
'Content-Type': 'text/plain',
},
});
}
};
エラーが発生した場合でも、ユーザーに適切なレスポンスを返すことで、サイト全体の信頼性が向上します。
動作確認とデバッグ
実装が完了したら、正しく動作するか確認しましょう。
ローカル開発サーバーでの確認
bash# 開発サーバーを起動
yarn dev
ブラウザで http://localhost:4321/og-image.png?title=テスト&description=説明文
にアクセスして、画像が正しく生成されるか確認します。
SNS シミュレーターでの検証
実際に SNS でどのように表示されるかは、以下のツールで確認できます。
# | ツール | URL | 用途 |
---|---|---|---|
1 | Twitter Card Validator | https://cards-dev.twitter.com/validator | Twitter での表示確認 |
2 | Facebook Sharing Debugger | https://developers.facebook.com/tools/debug/ | Facebook での表示確認 |
3 | LinkedIn Post Inspector | https://www.linkedin.com/post-inspector/ | LinkedIn での表示確認 |
これらのツールを使えば、実際にシェアする前に OG 画像が正しく表示されるか確認できますよ。
よくあるエラーと解決方法
実装中によく遭遇するエラーとその解決方法をまとめます。
エラー 1: Error: Cannot find module '@resvg/resvg-js'
bash# 解決方法:パッケージを再インストール
yarn install
このエラーは、依存関係が正しくインストールされていない場合に発生します。node_modules
を削除して再インストールすると解決することが多いです。
エラー 2: TypeError: Cannot read property 'toString' of null
typescript// 発生条件:フォントファイルが見つからない
// 解決方法:フォントパスを確認
const fontPath = join(
process.cwd(),
'public',
'fonts',
'NotoSansJP-Bold.ttf'
);
console.log('フォントパス:', fontPath); // デバッグ用
// ファイルの存在確認
import { access } from 'node:fs/promises';
try {
await access(fontPath);
} catch {
throw new Error(
`フォントファイルが見つかりません: ${fontPath}`
);
}
フォントファイルのパスが間違っている場合に発生します。console.log
でパスを確認し、ファイルが正しい場所に配置されているか確認しましょう。
エラー 3: 日本語が表示されない(□□□ になる)
この問題は、フォントが正しく読み込まれていない場合に発生します。以下を確認してください。
- フォントファイルが存在するか
fonts
オプションでname
が JSX のfontFamily
と一致しているか- フォントファイルが破損していないか
typescript// 正しい設定例
fonts: [
{
name: 'Noto Sans JP', // ← この名前と
data: fontData,
weight: 700,
style: 'normal',
},
],
// JSX 側
style: {
fontFamily: 'Noto Sans JP', // ← これが一致している必要がある
}
まとめ
Astro で Satori と resvg-js を組み合わせることで、動的な OG 画像生成を簡単に実装できました。主なポイントをおさらいしましょう。
- Satori を使えば、JSX ライクな記法で画像デザインを定義できます
- resvg-js による PNG 変換は高速で、依存関係も少なく効率的です
- API エンドポイントとして実装することで、柔軟にパラメータを受け取れます
- 日本語フォントの読み込みとキャッシングでパフォーマンスを最適化できます
- ビルド時生成とランタイム生成を使い分けることで、プロジェクトに最適な構成を選択できます
この実装により、記事を公開するたびに手動で OG 画像を作成する手間がなくなり、一貫性のあるデザインで SNS でのシェアを促進できるようになります。Astro の静的サイト生成の強みを活かしつつ、動的な画像生成も実現できるのは素晴らしいですね。
ぜひ、あなたのプロジェクトでも試してみてください。カスタマイズの幅も広いので、ブランドに合わせたオリジナルのデザインを作り込んでいくのも楽しいですよ。
関連リンク
- article
Astro で動的 OG 画像を生成する:Satori/Canvas 連携の実装レシピ
- article
Astro の View Transitions 徹底解説:SPA 並みの滑らかなページ遷移を実装するコツ
- article
Astro × Starlight カスタマイズ大全:ドキュメントサイトをブランド化する設計術
- article
Astro で OG/構造化データ最適化:SEO を底上げする設定大全
- article
【実測検証】Remix vs Next.js vs Astro:TTFB/LCP/開発体験を総合比較
- article
Astro × Cloudflare Workers/Pages:エッジ配信で超高速なサイトを構築
- article
Convex で Presence(在席)機能を実装:ユーザーステータスのリアルタイム同期
- article
Next.js の RSC 境界設計:Client Components を最小化する責務分離戦略
- article
Mermaid 矢印・接続子チートシート:線種・方向・注釈の一覧早見
- article
Codex とは何か?AI コーディングの基礎・仕組み・適用範囲をやさしく解説
- article
MCP サーバー 設計ベストプラクティス:ツール定義、権限分離、スキーマ設計の要点まとめ
- article
Astro で動的 OG 画像を生成する:Satori/Canvas 連携の実装レシピ
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来