戦略
フロー
Vercelが提供するライブラリである satori はHTMLをSVGへ変換することができます。そうして得られたSVGを @resvg/resvg-js でPNG画像に変換することで、OG画像とします。
$ yarn add satori @resvg/resvg-js
Reactのインストール
ベースとなるHTMLはReact JSXで作成するため、Reactインテグレーションを導入します。
$ yarn astro add react
ファイルを用意する
エンドポイント
Astroでは、/[slug].png.ts
というファイルを用意することで、静的生成の度にルートパラメータ slug
に対応するOG画像 [slug].png
を吐き出すエンドポイントをつくることができます。
import { getArticles, getArticleBySlug } from "src/lib/client";
import { getOgImageResponse } from "src/components/OgImage";
import type { APIContext } from "astro";
export const getStaticPaths = async () => {
const articles = await getArticles();
if (!articles || articles.length <= 0) return;
return articles.map((article) => ({
params: { slug: article.slug },
}));
};
export const GET = async ({ params }: APIContext) => {
const article = await getArticleBySlug(params.slug);
if (!article) return;
const ogImageResponse = await getOgImageResponse(article.title);
return ogImageResponse;
};
ここでは、getOgImageResponse
という関数を OgImage
コンポーネントに切り出しています。
以降の処理を処理をセクションごとにまとめます。
画像を生成するコンポーネント
import fs from "fs";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
export const getOgImageResponse = async (text: string): Promise<Response> => {
const fontBold = fs.readFileSync("src/fonts/NotoSansJP-Bold.ttf");
const width = 1200;
const height = 630;
const svg = await satori(
<main style={{
height: "100%",
width: "100%",
background: "#fff",
position: "relative",
}}>
{text}
</main>,
{
width,
height,
fonts: [
{
name: "Noto Sans JP",
data: fontRegular,
weight: 700,
style: "normal",
},
],
}
);
const resvg = new Resvg(svg, {
font: {
loadSystemFonts: false,
},
fitTo: {
mode: "width",
value: width,
},
});
const image = resvg.render();
return new Response(image.asPng(), {
headers: {
"Content-Type": "image/png",
},
});
};
satori
はDOMからSVGを生成します。これを Resvg
に渡すことでPNG画像のインスタンスを生成し、これをHTTPレスポンスの形で返却します。
画像のURLを渡す
metaタグの設定があるコンポーネント(記事ページのレイアウト)に画像のURLを渡します。
課題
画像生成がボトルネックになってかなり時間がかかることがありました。
ローカルサーバ起動時のエラー
@resvg/resvg-js
をインストールしてから、$ astro dev
がエラーを吐いてローカルサーバが立ち上がらなくなる現象が発生したため、このIssue にある内容をもとにastro.config.mjs
に以下を追記しました。vite: { ssr: { external: ['@resvg/resvg-js'] }, optimizeDeps: { exclude: ["@resvg/resvg-js"] }, }