dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Vercel OG(@vercel/og)でOG画像を動的生成する

こちらの記事を読んで。

以前から https://reading-list.zaki-yama.dev/ という自分のサイトに OG 画像を設定したいな〜と思っていたところに @vercel/og なるライブラリが登場したので試してみた。

※ なお、今までずっと「OGP 画像」だと思ってたんだけど、記事中では OG image と表記されていたので、ここでも「OG 画像」と表記する。

@vercel/og の特徴

冒頭の記事を読む限り、今までは vercel/og-image というサービスが提供されていたが、これには以下のような問題があった。

  • 難しい: Serverless Function 内で Chromium を起動し、 Puppeteer でスクリーンショットを撮る、ということをやっていた。これらのツールのセットアップは実装が難しくしばしばエラーを引き起こしていた
  • 遅い:Chromium を Serverless Function 内で起動するため圧縮し、コールドブート中に解凍される必要があるため、遅い(平均4秒)。結果、ソーシャルカードの生成が遅くなったり壊れたりした

今回発表された @vercel/og はこれらの問題を解決したもので、具体的には以下の特徴がある。

  • 簡単 (Easy):ヘッドレスブラウザ不要。HTML と CSS から OG 画像を生成できる
  • 高速 (Fast): vercel.com/docs で試した結果 vercel/og-image より 5倍程度速かったとのこと

加えて @vercel/og を利用したコードは Next.js アプリケーションと共存でき、 vercel/og-image のように別のところにデプロイする必要がない というのもうれしい。

@vercel/og が使える条件

https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#requirements を読む限り

  • Next.js のバージョンが v12.2.3 以降
  • runtime: 'experimental-edge' を有効にして Edge Runtime を使用する

を満たす必要がある。後者はデプロイ先が Vercel であれば問題にならないが、他だとどうなんだろ。やったことないのでわからない。

試してみる

というわけで自分で運用している Next.js アプリケーションの https://reading-list.zaki-yama.dev に @vercel/og を導入してみる。

ソースコードhttps://github.com/zaki-yama/reading-list

公式ドキュメントとしては以下を読めばよさそう。

簡単な OG 画像を表示する

まずは、ライブラリを導入して静的な OG 画像を表示する。
インストールは以下のコマンドで行う。

$ npm i @vercel/og

続いて、OG 画像生成用の API エンドポイントを用意する。
/pages/api/og.tsx というファイルを作る。

// /pages/api/og.tsx

import { ImageResponse } from '@vercel/og';

export const config = {
  runtime: 'experimental-edge',
};

export default function () {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    },
  );
}

(コードは https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation#getting-started を引用)

これで npm run dev して http://localhost:3000/api/og にアクセスすると Hello world! という画像が表示される。

最後に、これが OG 画像として表示されるようにする。それには <head> に以下のように meta タグを埋め込めばよい。
(自分は Layout コンポーネント内に定義した)

<meta
  property="og:image"
  content="https://reading-list.zaki-yama.dev/api/og"
/>

テキストを動的に設定する

基本的な使い方がわかったので、次は画像内のテキストを動的に設定してみる。
具体的には、現在開いているページのタイトルを OG 画像に埋め込むようにしたい。

OG Image Examples > Dynamic text generated as image にちゃんとサンプルが用意してある。

まず、 /pages/api/og.tsx 側は以下のように、クエリパラメータでタイトルを受け取るようにする。

// /pages/api/og.tsx

import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';

export const config = {
  runtime: 'experimental-edge',
};

export default function handler(req: NextRequest) {
  try {
    const { searchParams } = new URL(req.url);

    // ?title=<title>
    const hasTitle = searchParams.has('title');
    const title = hasTitle
      ? searchParams.get('title')?.slice(0, 100)
      : 'My default title';

    return new ImageResponse(
      (
        <div>
            // ...略...
            {title}
        </div>
      ),
      {
        width: 1200,
        height: 630,
      },
    );
  } catch (e: any) {
    console.log(`${e.message}`);
    return new Response(`Failed to generate the image`, {
      status: 500,
    });
  }
}

(コードは https://vercel.com/docs/concepts/functions/edge-functions/og-image-examples#dynamic-text-generated-as-image から抜粋)

そして、使う側は

<meta
  property="og:image"
  content="https://reading-list.zaki-yama.dev/api/og?title=${postData.title}"
/>

というようにクエリパラメータをページごとに変えて設定してやる。

1点サンプルには載ってなかったこととして、自分は以下のように URLSearchParams をかませるようにした。

// /pages/posts/[id].tsx
export default function Post({
  postData,
}: {
  postData: {
    title: string;
    date: string;
    contentHtml: string;
  };
}) {
  const title = `${siteTitle} ${postData.title}`;
  const searchParams = new URLSearchParams(`title=${title}`);
  return (
    <Layout>
      <Head>
        <title>{title}</title>
        <meta
          property="og:image"
          content={`https://reading-list.zaki-yama.dev/api/og?${searchParams.toString()}`}
          key="og-image"
        />
        <meta property="og:title" content={postData.title} key="og-title" />
      </Head>
      ...

https://github.com/zaki-yama/reading-list/blob/main/pages/posts/%5Bid%5D.tsx#L17-L27

おそらくサイトや記事のタイトルに ' (スペース)が含まれていたせいで Open Graph Debugger で見たときにうまく表示されなかったため、これらの文字は URL エンコードするようにした。

結果

https://reading-list.zaki-yama.dev/posts/2022-09-13 という URL を Twitter や Slack に貼ったときに OG 画像が表示されるようになった。

デザイン性皆無なのはしょうがないが、一旦満足。