この記事では、satoriresvg を使用して、ブログ記事単位でOG画像を自動生成する方法を解説します。

最終的には、以下のような画像が生成されるようになります。

OG Image

はじめに

satori は HTML要素をSVGに変換する ライブラリで、文字やレイアウトを自由にカスタマイズできる点が魅力です。
resvg は SVGからPNGへ変換 するライブラリです。
Astroで動的に生成したSVGをPNGに変換し、それをOG画像として利用する流れになります。

astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
vite: {
ssr: {
external: ["@resvg/resvg-js"],
},
optimizeDeps: {
exclude: ["@resvg/resvg-js"]
},
build: {
rollupOptions: {
external: ["@resvg/resvg-js"]
}
},
},
});

外部パッケージをSSRのバンドル対象から外すことでエラーを回避します。

処理の流れ

  1. OG画像用のHTMLを作成
  2. satoriでHTMLをSVGに変換
  3. resvgでSVGをPNGに変換
  4. 出来上がった画像をAstroの静的ファイルエンドポイントで配信

実装

OG画像の生成コンポーネント

まずは OG 画像を生成するための処理をまとめたファイルを作成します。Astro では React などのフレームワークを併用できるため、ここでは tsxファイル として実装します。

  1. ライブラリのインストール
Terminal window
yarn add react
yarn add -D satori @resvg/resvg-js @types/react
  1. フォントファイルの準備

satoriで文字を正しくレンダリングするため、使用したいフォントデータを読み込みます。
フォントファイル(例: NotoSansJP-Bold.ttf)を src/assets/ などに配置しておきます。
フォントファイルの読み込みについては、こちらの記事が参考になりました。

  1. OG画像を生成する関数を作る
src/utils/ogp.tsx
import React from "react";
import { Resvg } from "@resvg/resvg-js";
import satori from "satori";
// フォントファイルをバイナリで取り込む (Astroのバンドルで対応できる場合)
import BoldFont from "../assets/NotoSansJP-Bold.ttf";
/**
* elementをSVGに変換し、さらにPNG化する。
*/
const generateOgpImage = async (element: React.ReactNode) => {
// satoriでHTML要素(Reactノード)をSVGテキストに変換する
const svg = await satori(element, {
width: 1200,
height: 630,
fonts: [
{
name: "Noto Sans JP",
data: Buffer.from(BoldFont),
style: "normal",
weight: 600,
},
],
});
// resvgでSVG → PNGに変換する
const resvg = new Resvg(svg, {
fitTo: {
mode: "width",
value: 1200,
},
});
// バイナリデータとしてPNGを返す
const image = resvg.render();
return image.asPng();
};
/**
* ブログ記事用のOGP画像を生成する
*/
export const generateBlogPostOgpImage = (title: string) => {
// ここでは satori に渡すコンポーネント全体を定義する
const element = (
<div
style={{
display: "flex",
width: "1200px",
height: "630px",
position: "relative",
backgroundColor: "#fff",
padding: "2rem",
boxSizing: "border-box",
border: "40px solid #e0e8b2",
}}
>
<h1
style={{
fontSize: "4rem",
margin: "24px",
textAlign: "left",
color: "#333",
lineHeight: "1.5",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
maxWidth: "1100px",
}}
>
{title}
</h1>
<div
style={{
position: "absolute",
bottom: "3rem",
right: "3rem",
color: "#333",
fontSize: "2.5rem",
display: "flex",
alignItems: "center",
}}
>
{/* satori公式ドキュメントでは<img>タグに width/height を設定することが推奨されています */}
<img
src="https://avatars.githubusercontent.com/u/124267041?v=4"
width={80} // widthを指定
height={80} // heightを指定
style={{ borderRadius: 9999, marginRight: 24 }}
/>
rie03p
</div>
</div>
);
return generateOgpImage(element);
};

画像の扱い方について

satoriの公式リポジトリのREADMEには、次のような注意点があります。

  1. <img> には width / height 属性を指定するとよい。
  2. background-image を使う場合は、サイズ指定をしないと背景が要素全体に伸縮される。
  3. src に外部URLを指定する場合、SVG生成時にそのURLへリクエストが発生する ため、バンドル環境やネットワーク状況によっては問題が発生することがある。
  4. 追加のI/Oを避けるために、base64形式 (data:image /png;base64,~) や ArrayBuffer を src に指定する方がパフォーマンスが良い。

Base64エンコードの利用例

await satori(
<img
src="data:image/png;base64,..."
width={200}
height={300}
/>,
options
)

静的ファイルエンドポイントの作成

Astro には「エンドポイント」機能があり、ページとしてレンダリングするのではなく、任意のデータを返却できます。
以下の例では、 /posts/ogp/[…slug].png.ts のようなパスで PNGデータを直接返す エンドポイントを作成します。

src/pages/ogp/[...slug].png.ts
import { generateBlogPostOgpImage } from "@/utils/ogp";
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
export const GET: APIRoute = async ({ params }) => {
const { slug } = params;
const posts = await getCollection("post");
const post = posts.find((p) => p.slug === slug);
// タイトルをもとにOG画像を生成
const title = post?.data.title;
if (!title) {
return new Response("Not Found", { status: 404 });
}
const image = await generateBlogPostOgpImage(title);
return new Response(image, {
headers: {
"Content-Type": "image/png",
},
});
};
export async function getStaticPaths() {
const posts = await getCollection("post");
return posts.map((post) => {
return {
params: {
slug: post.slug,
},
};
});
}

メタタグにOG画像を設定

AstroでOG画像を設定するには、ページやレイアウトコンポーネントから og:image メタタグを出力します。
以下は src/pages/posts/[…slug].astro 内で、生成したOG画像URLを指定する例です。

const ogImageUrl = `/posts/ogp/${post.slug}.png`;
---
<Layout meta={{ title, articleDate, ogImage: ogImageUrl }}>

終わりに

この記事では、記事タイトルなどの情報からOG画像を自動生成し、記事単位でユニークなSNSシェア用サムネイルを用意する ための手順を解説しました。
OG画像のデザインやフォント、配置などは自由にカスタマイズできるので、運営しているブログやサイトの雰囲気に合わせて調整してみてください。

参考リンク

  1. satori (vercel/satori)
  2. resvg (linebender/resvg)
  3. Astro Endpoints
  4. 公式Playground (OG Image Generation)
  5. Astroでフォントデータをローカルファイルから読み込めるようにする