AstroでのOGImage自動作成


OGImage とは

こういうやつですね。

Twitter などにリンクを貼り付けた時に特定の metadata に設定されている URL を表示してくれるやつです。

OGImage 用の png 画像を作成する

個人のブログで毎回画像を手で作るのはしんどいので、テンプレ的な画像内の文字だけ変えるようなものを作成します。 Astro Integration API を使って build 完了時に画像生成処理を呼び出すと言う方法で実現しました。 画像は satori と sharp を使って jsx から png 画像を生成します。

docs.astro.build
docs.astro.build favicon https://docs.astro.build/en/reference/integrations-reference/
GitHub - vercel/satori: Enlightened library to convert HTML and CSS to SVG
Enlightened library to convert HTML and CSS to SVG - vercel/satori
GitHub - vercel/satori: Enlightened library to convert HTML and CSS to SVG favicon https://github.com/vercel/satori
GitHub - vercel/satori: Enlightened library to convert HTML and CSS to SVG
GitHub - lovell/sharp: High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.
High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library. - lovell/sharp
GitHub - lovell/sharp: High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library. favicon https://github.com/lovell/sharp
GitHub - lovell/sharp: High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.

下記に実際のコードのせておきます。

src/integrations/ogimage.tsx

import type { AstroIntegration } from "astro";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import satori from "satori";
import sharp from "sharp";

# satoriとsharpを使ってpng画像を作る
const generate = async (title: string) => {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);
  const font = fs.readFileSync(
    path.resolve(__dirname, "NotoSansJP-SemiBold.ttf")
  );
  const svg = await satori(
    <div
      style={{
        height: "100%",
        width: "100%",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "center",
        backgroundColor: "#fff",
        fontSize: 32,
        fontWeight: 600,
      }}
    >
      <svg
        width="75"
        viewBox="0 0 75 65"
        fill="#000"
        style={{ margin: "0 75px" }}
      >
        <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
      </svg>
      <div style={{ marginTop: 40 }}>Hello, World</div>
    </div>,
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Noto Sans JP",
          data: font,
          style: "normal",
        },
      ],
    }
  );
  return await sharp(Buffer.from(svg)).png().toBuffer();
};

export const createOGImage = ({
  config,
}: {
  config: { path: string };
}): AstroIntegration => {
  return {
    name: "og-image",
    hooks: {
      "astro:build:done": async ({ dir, pages }) => {
        const blogPages = pages.filter((page) =>
          page.pathname.includes(config.path)
        );

        await Promise.all(
          blogPages.map(async (page) => {
            const indexHtmlPath = path.join(
              dir.pathname,
              page.pathname,
              "index.html"
            );

            const indexHtml = fs.readFileSync(indexHtmlPath, "utf8") as any;
            const title = await indexHtml.match(
              /<title[^>]*>([^<]+)<\/title>/
            )[1];
            const buffer = await generate(title);
            const filename = path.join(dir.pathname, page.pathname, "ogp.png");
            // 非同期のfs.writeFileだと生成がうまく以下あない場合があるので同期的に書き込む
            fs.writeFileSync(filename, buffer);
          })
        );
      },
    },
  };
};

astro.config.mjs

export default defineConfig({
  integrations: [
    ...createOGImage({
      config: {
        path: "blog/",
      },
    }),
  ],
});

await satori()の第 1 引数に渡している jsx が最終的なレイアウトを決める感じです。 jsx で書くために react も導入が必要なので入れておきます。

@astrojs/react
Learn how to use the @astrojs/react framework integration to extend component support in your Astro project.
@astrojs/react favicon https://docs.astro.build/en/guides/integrations-guide/react/
@astrojs/react

Playground を使ってレイアウトを作成する

公式の Playground 環境があるのでそこで style を調整するとレイアウトは比較的ラクに作れます。

Vercel OG Image Playground
Generate Open Graph images with Vercel’s Edge Function.
Vercel OG Image Playground favicon https://og-playground.vercel.app/
Vercel OG Image Playground

OG Image Playground

SSG するか SSR するか

今回の自分の方法は SSG ということになるとおもいます。OGImage 自体は build 時に作成して dist 配下に保存してあります。SSR で作成する方法もあり、それはこの記事の最初にはった GitHub の画像のリポジトリで行われている方法で、Astro の Endpoints 機能を使って実現されています。

feat: generate dynamic og image for blog posts by satnaing · Pull Request #15 · satnaing/astro-paper
Dynamic OG Image Generation dynamic OG images will be generated at build time using Satori. the OG image format will be <blog post title>.svg. draft posts and posts with frontmatter.ogImage specif...
feat: generate dynamic og image for blog posts by satnaing · Pull Request #15 · satnaing/astro-paper favicon https://github.com/satnaing/astro-paper/pull/15
feat: generate dynamic og image for blog posts by satnaing · Pull Request #15 · satnaing/astro-paper
Endpoints
Learn how to create endpoints that serve any kind of data
Endpoints favicon https://docs.astro.build/en/core-concepts/endpoints/
Endpoints