記事一覧に戻る
ブログ投稿のOG画像を自動生成する
ブログ投稿のOG画像を自動生成する Photo by   Andrej Lišakov   on   Unsplash
目次

Twitter cardの仕様変更

2023年10月の初めに、Twitter (別名X)の外部リンクの表示内容が変更されました。

具体的には、twitter:cardsummary_large_imageを使用している場合、
以前は以下の画像のようにOGイメージ+descriptionがカードに表示されていました

image

しかし仕様変更後は、OGイメージが大きく表示され、以下のように画像の右下にドメインが小さくされるようになりました。

image

Twitterカードにタイトルとdescriptionが表示されなくなり、 投稿したURLの詳細な情報がわからなくなってしまいました。

最近では、この使用を利用して、OGイメージを画像に見せかけてクリックをさせ、悪質なサイトに飛ばすという例もちらほら見かけます。

ブログ投稿などをOGPの内容が動的に変化するコンテンツを共有する場合には
OGイメージを自動生成し、記事タイトルをOGイメージに含めたほうが、ユーザーにとって遷移する先のサイト情報がわかりやすくなります。

OG画像を自動生成する

そこで当ブログも投稿記事をTwitterに共有する際に記事タイトルを含めたOGイメージを動的に生成するようにしました。

概要としては、 vercel/satori @resvg/resvg-js を使用して JSX → SVG → PNG の流れでPNGを生成しています

実装

OGイメージのエンドポイントを作成する

まず、src/pages/og[slug].png.tsを作り、画像を返すエンドポイント1を作成します。

getStaticPaths()ですべてのブログエントリのslugを取得し、パスを生成します。

次に以下の手順で、GET関数GET methodに対するレスポンスを定義していきます。

  1. getStaticPaths()で生成されたパスからslugを取得し、対応するエントリすべてをgetEntryBySlug()で取得
  2. 後述するOgImageコンポーネントでエントリのタイトルをもとにOGイメージを生成する
  3. 生成されたOGイメージをbodyとするレスポンスを定義する
// [slug].png.ts
import type { APIContext } from "astro";
import { getCollection, getEntryBySlug } from "astro:content";
// satoriを使用したOGイメージのコンポーネント
import { OgImage } from "../components/OgImage";

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

export async function GET({ params }: APIContext) {
  const entry = await getEntryBySlug("blog", params.slug as string);
  const body = await OgImage(entry!.data.title); // Bufferを受け取る

  // 生成されたOGイメージを含むレスポンスを返す
  return new Response(body, {
    headers: {
      "Content-Type": "image/png",
    },
  })
}

各エントリのパス生成と各エンドポイント(eg: /og/about-me.png)のレスポンス生成はビルド時に行われます

satoriでJSXをSVGに変換

次にsatoriを使用してOGイメージをJSXで定義し、SVGに変換します。

satori自体の使い方はとても簡単なのでここで詳しく説明しませんが、
基本的にはsatori(element, options)の第一引数にJSXでOGイメージを定義し、第二引数でフォントや大きさなどを指定してあげればOKです。

背景に使用する画像を読み込む

今回はOGイメージの背景に、自作した画像をastro:assetsを使用して背景画像を読み込んで使用しています

import ogBackground from "../assets/og_background.png";

satoriでは背景画像に画像を使用し、更にSVGやPNGなどにレンダリングする場合はbase64エンコードされたData URLBufferを使用すると余計な計算がなくなるらしいので、src/assets/においた背景画像をData URLとして読み込みます。

このとき、ogBackground.srcが指すパスが、開発モードとproductionモードで異なる。(developではそのままsrc/assets以下になるが、productionではdist/_astro以下になる)

そのため、import.meta.env.DEVを利用して、モードによってreadFileSyncで指定するパスを変えてあげる

(ここらへんもっと良いいいやり方がありそうなので、知っている方いれば教えて頂きたいです。)

const ogBackgroundImage = readFileSync(
    import.meta.env.DEV
      ? new URL(`../assets/og_background.png`, import.meta.url)
      : `./dist/${ogBackground.src}`,
    { encoding: "base64" }
  );

// Data URLにする
const ogBackgroundImageUrl = `data:image/png;base64,${ogBackgroundImage}`;

フォントの取得

satoriで使用する日本語フォントをfetchします。

最初はAstroのドキュメントの通りに、public/fontsにfontファイルを置いて利用する方法で行こうとしたのですが、
satoriが内部で使用しているOpenTypeのエラーが出てしまいどうしても解決できなかったのでコード内でfetchするようにしました。

Goolge Fonts APIではURLパラメータのtext=に渡した文字列に最適化(サブセット化)したフォントファイルを返してくれます。

async function getSubsetFontData(text: string, font: string, fontWeight: number) {
  const GOOGLE_FONT_ENDPOINT = `https://fonts.googleapis.com/css2?family=${font}:wght@${fontWeight}&text=${encodeURIComponent(text)}`;

  const css = await fetch(GOOGLE_FONT_ENDPOINT).then((res) => res.text());

  // fontのurlを正規表現で取得する
  const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)?.[1];

  if (!resource) {
    throw new Error('font url is not found');
  }

  const arrayBuffer = await fetch(resource).then((res) => res.arrayBuffer());

  return arrayBuffer;
}

satori playgroundのソースコード2でも、同じようにGoolge Fonts APIからフォントをfetchしていますが、
.ttfファイルを確実に取得するために以下のようにuser agentをすり替えています。
これはあまりよろしくないな~っていうのと、こんな事しなくても普通にfetchできたので僕の場合はそのままfetchしています。

// https://github.com/vercel/satori/blob/main/playground/pages/api/font.ts#L86
await fetch(API, {
  headers: {
    // Make sure it returns TTF.
    'User-Agent':
      'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
  },
})

完成

これで、以下のようにブログの各エントリに対してタイトルを挿入したOGイメージを自動生成することができるようになりました

image

参考

  1. vercel /satori
  2. t28.dev | @vercel/og を使って Astro 製ブログのビルド時に OGP 画像を出力する
  3. Marginalia | satoriを使ったAstroのOGP画像生成メモ

Footnotes

  1. Astro endpoint
  2. satori-playgroundの font fetch 実装