Closed12

satoriでブログのOGP画像を動的に生成する

ikuma-tikuma-t

現状は適当に幾何学模様を散りばめた固定画像を利用している。

ikuma-tikuma-t

文字を動的に変更するにあたって、図形と色数がだいぶごちゃつきそうだったので、台紙を書き直した。

ikuma-tikuma-t

AstroのIntegrationとして、ビルド時にsatoriを使ってOGPを作るようにしてみたけど、複数個同時に生成しようとすると失敗する。原因を調査する。

ikuma-tikuma-t

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)
ikuma-tikuma-t

もしやと思って、Promise.allで実行している部分を順次実行にしてみたところ問題なく出力できるようになった。並列でやってはいけなかったのか...

ikuma-tikuma-t

なお、現状はこんな感じ。非常に力技である。

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);
        }
      },
    },
  };
};
このスクラップは5ヶ月前にクローズされました