Closed12
satoriでブログのOGP画像を動的に生成する
N番煎じではあるけど、ブログのOGPを動的に設定したかったので、やっていく。
現状は適当に幾何学模様を散りばめた固定画像を利用している。
文字を動的に変更するにあたって、図形と色数がだいぶごちゃつきそうだったので、台紙を書き直した。
とりあえず入れる
yarn add -D satori @resvg/resvg-js
とりあえず一つ自動生成できた。
AstroのIntegrationとして、ビルド時にsatoriを使ってOGPを作るようにしてみたけど、複数個同時に生成しようとすると失敗する。原因を調査する。
1件ずつだと成功する2件を、2件まとめて処理しようとすると、以下のエラーで落ちる。
Stacktrace:
BindingError: Expected null or instance of Node, got an instance of Node
at Error.<anonymous> (/node_modules/yoga-wasm-web/dist/asm.js:1:131684)
satoriが内部でyogaを使用している。実際に使っているのがこれのwasm版。なので、yoga-wasm-webでエラーが出ている。
もしやと思って、Promise.allで実行している部分を順次実行にしてみたところ問題なく出力できるようになった。並列でやってはいけなかったのか...
とりあえず生成できるようになったので、一旦OK。次はリファクタリングしていく。
なお、現状はこんな感じ。非常に力技である。
import fs from "fs/promises";
import path from "path";
import type { AstroIntegration } from "astro";
import fm from "front-matter";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = path.dirname(new URL(import.meta.url).pathname);
const getStaticPaths = (): string[] => {
const posts = Object.keys(import.meta.glob("/src/content/blog/**/*.md"));
return posts.map((filename) => filename.replace(/^.*\/(.*)\.md$/, "$1"));
};
const generate = async (
title: string,
{
background,
font,
}: {
background: string;
font: Buffer;
}
): Promise<Buffer> => {
const svg = await satori(
{
type: "div",
props: {
style: {
display: "flex",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
width: 1200,
height: 630,
backgroundImage: `url(${background})`,
backgroundSize: "1200px 630px",
},
children: {
type: "div",
props: {
style: {
display: "flex",
justifyContent: "center",
alignItems: "center",
width: 1040,
height: 390,
fontSize: "60px",
fontWeight: "bold",
color: "#2E2E2E",
textOverflow: "ellipsis",
},
children: title,
},
},
},
} as any,
{
width: 1200,
height: 630,
fonts: [
{
name: "NotoSansJP",
data: font,
weight: 900,
style: "normal",
},
],
}
);
const resvg = new Resvg(svg);
return resvg.render().asPng();
};
export interface CreateAstroOgimageOptions {
dist: string;
}
export const createAstroOgimage = ({
dist,
}: CreateAstroOgimageOptions): AstroIntegration => {
return {
name: "astro-ogimage",
hooks: {
"astro:build:done": async () => {
const pages = getStaticPaths();
const background = await fs.readFile(
path.resolve(__dirname, "assets/base.png"),
"base64"
);
const font = await fs.readFile(
path.resolve(__dirname, "assets/NotoSansJP-Bold.otf")
);
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
console.log(page);
const markdown = await fs.readFile(
path.resolve(__dirname, `../src/content/blog/${page}.md`),
"utf8"
);
const { attributes } = fm<{ title: string; image?: string }>(
markdown
);
const buffer = await generate(attributes.title, {
background: `data:image/png;base64,${background}`,
font,
});
const filename = path.join(dist, "blog", page, "ogp.png");
await fs.writeFile(filename, buffer);
}
},
},
};
};
このスクラップは2024/02/11にクローズされました