この記事では、satori と resvg を使用して、ブログ記事単位でOG画像を自動生成する方法を解説します。
最終的には、以下のような画像が生成されるようになります。
はじめに
satori は HTML要素をSVGに変換する ライブラリで、文字やレイアウトを自由にカスタマイズできる点が魅力です。
resvg は SVGからPNGへ変換 するライブラリです。
Astroで動的に生成したSVGをPNGに変換し、それをOG画像として利用する流れになります。
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のバンドル対象から外すことでエラーを回避します。
処理の流れ
- OG画像用のHTMLを作成
- satoriでHTMLをSVGに変換
- resvgでSVGをPNGに変換
- 出来上がった画像をAstroの静的ファイルエンドポイントで配信
実装
OG画像の生成コンポーネント
まずは OG 画像を生成するための処理をまとめたファイルを作成します。Astro では React などのフレームワークを併用できるため、ここでは tsxファイル として実装します。
- ライブラリのインストール
yarn add reactyarn add -D satori @resvg/resvg-js @types/react
- フォントファイルの準備
satoriで文字を正しくレンダリングするため、使用したいフォントデータを読み込みます。
フォントファイル(例: NotoSansJP-Bold.ttf)を src/assets/
などに配置しておきます。
フォントファイルの読み込みについては、こちらの記事が参考になりました。
- OG画像を生成する関数を作る
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には、次のような注意点があります。
<img>
には width / height 属性を指定するとよい。- background-image を使う場合は、サイズ指定をしないと背景が要素全体に伸縮される。
- src に外部URLを指定する場合、SVG生成時にそのURLへリクエストが発生する ため、バンドル環境やネットワーク状況によっては問題が発生することがある。
- 追加の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データを直接返す エンドポイントを作成します。
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, }, }; });}
- GET関数
- URLに含まれる slug を元に記事を探し、記事のタイトルをOG画像生成に渡します。
- 生成されたPNGバイナリを Response として返します。URLに含まれる
- getStaticPaths 関数
- ビルド時に全記事のOG画像(slug.png)をあらかじめ生成するために必要です。
メタタグに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画像のデザインやフォント、配置などは自由にカスタマイズできるので、運営しているブログやサイトの雰囲気に合わせて調整してみてください。