Astroブログでsatoriを使って記事ごとのOGP画像を自動生成する

はじめに

少し前にGatsbyからAstroにブログを移行したこのブログだが、satoriを使って記事ごとにGOP画像を自動生成できるようにした。

今回はその方法を残しておく。

画像生成に必要な準備

今回使用するsatoriはVercelが公開しているライブラリで、JSX構文で書かれた要素をSVGに変換してくれる。JSXを処理するのでreactパッケージが必要となる。一方、DOMの差分処理などはする必要がないのでreact-domは不要だ。

npm install satori react @types/react

また、satoriが生成するのはSVG画像であるため、そのままではOGP画像には使用できない。今回はsatoriが作成したSVG画像をPNGに変換するためresvgを使用する。

npm install @resvg/resvg-js

最後に、satoriで生成する画像の中で文字を扱うにはフォントデータが必要となる。今回はGoogleFontsからNoto Sans Japaneseをダウンロードして使用する。

satoriで画像生成

まずは画像を作成するための関数をsrc/utils/ogp.tsx作成する。

import type { CollectionEntry } from 'astro:content';
import React from 'react'
import satori from 'satori';
import { Resvg } from '@resvg/resvg-js';
import RegularFont from "../assets/fonts/NotoSansJP-Regular.ttf";
import BoldFont from "../assets/fonts/NotoSansJP-Bold.ttf";

const generateOgpImage = async (element: React.ReactNode) => {
  const svg = await satori(
    element,
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "Noto Sans JP",
          data: Buffer.from(RegularFont),
          style: "normal",
          weight: 400,
        },
        {
          name: "Noto Sans JP",
          data: Buffer.from(BoldFont),
          style: "normal",
          weight: 600,
        },
      ],
    }
  );

  const resvg = new Resvg(svg, {
    fitTo: {
      mode: "width",
      value: 1200,
    },
  });
  const image = resvg.render();

  return image.asPng();
}


export const generateBlogPostOgpImage = (post: CollectionEntry<"post">) => {
  const { title, createdAt, tags } = post.data;

  return generateOgpImage((
    <div>blog title is {title}</div>
  ));
}

export const generateSiteOgpImage = () => {
  return generateOgpImage((
    <div>site ogp</div>
  ));
};

generateBlogPostOgpImageはブログ記事ページ用に、generateSiteOgpImageはそれ以外のページに使用する。それ以外のページはsatoriではなく生のOGP画像をpublicディレクトリにでも配置して良いが、好きなときにスタイルを変更したりしたいのでこのようにしている。

satoriのオプションについては公式のドキュメントを参照してほしい。また、生成された画像がどのようになっているかは公式のPlayGroundで確認するのが便利だ。

また、フォントファイルの読み込みにはfs.readFileSyncを使いたかったが、Astroではno such file or directoryとエラーが出てできなかった。Astroでフォントデータを読み込む方法については以下に詳しく書いたので参照してみてほしい。

OGP画像用のエンドポイントを作成する

これでOGP画像は作成できるようになったので、それを参照するためのエンドポイントを作成する。今回は以下のようなエンドポイントでOGP画像を参照できるようにする。

それぞれ、src/pages/posts/ogp/[slug].png.tssrc/pages/ogp.png.tsというファイルを作成する。Astroにおける静的なファイルを返すエンドポイントを作成するには、filename.[返したいファイルの拡張子].[js|ts]というファイルを作成する。詳しくは以下を参照してほしい。

// src/pages/posts/ogp/[slug].png.ts
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
import { getPostSlug } from "../../../utils/posts";
import { generateBlogPostOgpImage } from "../../../utils/ogp";

export const GET: APIRoute = async ({ params }) => {
  const slug = params.slug;
  const posts = await getCollection("post");
  const post = posts.find((p) => p.slug === slug);
  const png = await generateBlogPostOgpImage(post!);

  return new Response(png, {
    headers: {
      'Content-Type': 'image/png',
    },
  });
};

export async function getStaticPaths() {
  const posts = await getCollection("post");
  return posts.map((post) => {
    return {
      params: { slug: post.slug },
    }
  })
}

// src/pages/ogp.png.ts
import type { APIRoute } from "astro";
import { generateSiteOgpImage } from "../utils/ogp";

export const GET: APIRoute = async () => {
  const png = await generateSiteOgpImage();

  return new Response(png, {
    headers: {
      'Content-Type': 'image/png',
    },
  });
};

これで各エンドポイントからOGP画像が返ってくるようになった。

メタタグにOGP画像を設定する

最後に、各ページのメタタグにOGP画像を設定する。自分は全てのページでBaseLayoutというレイアウトを使用しているので、その中でメタタグに値を渡して設定した。

---
import { siteConfig } from '../config'

interface Props {
  title: string;
  description: string;
  ogUrl?: string;
}

const {
  title,
  description,
  ogUrl = `${siteConfig.url}/ogp.png`,
} = Astro.props
---

<!doctype html>
<html lang="en">
  <head>
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={ogUrl} />
    <meta property="og:type" content="website" />
  </head>

記事ページで使用する際は以下のようにする。それ以外のページではogUrlを渡さなければ、デフォルトでOGP画像が設定される。

---
const { title, description } = post.data;
---

<BaseLayout
  title={`${title} | ${siteConfig.title}`}
  description={description}
  ogUrl={`${siteConfig.url}/posts/ogp/${post.slug}.png`}
>
  <Content />
</BaseLayout>

これで記事ごとのページと、それ以外のページにもOGP画像を設定することができた。

おわり

satoriはよく使っているJSX構文でSVGが作成できる便利なライブラリだ。ぜひ試してみてほしい。

関連記事