サムネイル

Next.jsで動的OGPを作った時のエピソードを書いてみました

このブログサイトで動的OGPを実装しようとしたお話をします。Next.jsを使用しているので@vercel/ogで簡単に実装できます。ただし、Next.js 12.2.3以上でないとこのモジュールが動作しません。

$ npm install @vercel/og

※インストールした@vercel/ogのバージョンは0.5.6です。

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

また、Next.js 13.3.0から@vercel/ogをインストールしなくても、Next.js APIのnext/serverからImageResponseをインポートできるようになり動的OGPを生成できるようになりました。

import { ImageResponse } from "next/server";

画像を生成してみる

// pages/api/og.tsx
import { ImageResponse } from '@vercel/og' // Next.js 12.2.3以上13.3.0未満の場合
import { ImageResponse } from 'next/sever' // Next.js 13.3.0以上の場合

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

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

http://localhost:3000/api/ogにアクセスする。

あとは任意のページにmetaタグを挿入すればOGPが出来上がります。

import Head from 'next/head'

<Head>
  <title>タイトルです</title>
  <meta
    property="og:image"
    content="https//hogehoge.com/api/og"
  />
</Head>

動的にタイトルを挿入する

それでは、次に動的にタイトルを挿入し、動的なOGPっぽくしたいと思います。
結論としては、https://hogehoge.com/api/og?title=タイトルのようにクエリパラメータを使用して動的にタイトルを挿入します。

// pages/api/og.tsx
import { NextApiHandler } from "next";
import { ImageResponse } from "@vercel/og";

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

const handler: NextApiHandler = async (req) => {
  if (!req.url) throw Error("not supported.");
  const { searchParams } = new URL(req.url);
  
  const title = searchParams.get("title");
  const postDate = searchParams.get("postDate");
  return new ImageResponse(
    (
      <div
        style={{
          borderWidth: "36px",
          borderColor: "#5bbee5",
          backgroundColor: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          textAlign: "center",
          alignItems: "center",
          justifyContent: "center",
          color: "black",
          position: "relative",
        }}
      >
        <h2
          style={{
            color: "black",
            fontSize: 64,
          }}
        >
          {title}
        </h2>
        <div
          style={{
            display: "flex",
            position: "absolute",
            width: "100%",
            bottom: 0,
            paddingRight: 32,
            justifyContent: "flex-end",
          }}
        >
          <h2
            style={{
              color: "black",
              fontSize: 40,
            }}
          >
            {postDate}
          </h2>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  );
};

export default handler;

これでhttp://localhost:3000/api/og?title=Next.jsでSVGを表示する&postDate=2022年02月02日投稿にアクセスすると画像が表示されるはずですがされません。???????

@vercel/ogではデフォルトで日本語での表示をサポートしているのですが、文字列に漢字が含まれているとsatori内でパースできずにエラーになってしまいます。

error - uncaughtException: Error [ERR_UNHANDLED_ERROR]: Unhandled error. (Error: Unsupported OpenType signature <htm
    at Object.parseBuffer [as parse] (webpack-internal:///(middleware)/./node_modules/@shuding/opentype.js/dist/opentype.module.js:11455:15)
    at vt.addFonts (webpack-internal:///(middleware)/./node_modules/satori/dist/index.wasm.js:18:19200)
    at gu (webpack-internal:///(middleware)/./node_modules/satori/dist/index.wasm.js:19:29617)
    at async Object.start (webpack-internal:///(middleware)/./node_modules/@vercel/og/dist/index.js:10:2912))

この問題は日本語フォントを適用することで解決できます。

カスタムフォントを適応させる

Google Fonsあたりから好きなフォントをダウンロードして下さい。
ダウンロードしたフォントファイルをpublic/に格納(※今回はpublic/font配下に格納しました。)し、以下のように書き換えて下さい。

// pages/api/og.tsx
import { NextApiHandler } from "next";
import { ImageResponse } from "@vercel/og";

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

const notoSansJP = fetch(
  new URL("../../public/font/NotoSansJP-Bold.woff", import.meta.url).toString()
).then((res) => res.arrayBuffer());

const handler: NextApiHandler = async (req) => {
  if (!req.url) throw Error("not supported.");
  const { searchParams } = new URL(req.url);
  const fontNoto = await notoSansJP;

  const title = searchParams.get("title");
  const postDate = searchParams.get("postDate");
  return new ImageResponse(
    (
      <div
        style={{
          borderWidth: "36px",
          borderColor: "#5bbee5",
          backgroundColor: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          textAlign: "center",
          alignItems: "center",
          justifyContent: "center",
          color: "black",
          position: "relative",
        }}
      >
        <h2
          style={{
            color: "black",
            fontSize: 64,
            fontFamily: '"NotoSansJP"',
          }}
        >
          {title}
        </h2>
        <div
          style={{
            display: "flex",
            position: "absolute",
            width: "100%",
            bottom: 0,
            paddingRight: 32,
            justifyContent: "flex-end",
          }}
        >
          <h2
            style={{
              color: "black",
              fontSize: 40,
              fontFamily: '"NotoSansJP"',
            }}
          >
            {postDate}
          </h2>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 600,
      fonts: [
        {
          name: "NotoSansJP",
          data: fontNoto,
          style: "normal",
        },
      ],
    }
  );
};

export default handler;

これで、http://localhost:3000/api/og?title=Next.jsでSVGを表示する&postDate=2022年02月02日投稿
にアクセスすると上手く表示されるはずです。


ですが、vercelにデプロイする際エラーになりました。
vercelのhobbyプランではEdge関数は1MB以下ではならなければいけません。日本語フォントは重い・・・
フォントファイルはサブセット化し4.7MBから527KBに落としたのですがEdge関数の大きさは1.12MBでした。(筆者調べ)
他の日本語フォントを試したりしたのですがEdge関数の大きさは1MB以下になりませんでした。hobbyプラン以上のプラン変更も検討しましたがこのためだけにプランを変更するのは流石に厳しい・・・
だから諦めました。

@vercel/ogのバージョンによって漢字が含まれていてもエラーが出ないバージョンがありました。全て確認したわけではありませんですがバージョン0.0.19では漢字が含まれていてもエラーがでませんでした。文字列中に全角スペースがあったり、のような全角の記号があるとsatori内でパースできずエラーになってしまうようです。ちなみに、なぜかは大丈夫でした。
ですが、この方法ではvercelにはデプロイできましたが、vercelでは上手く動いてくれませんでした。

Next.js 13.3.0以上だと関係なかった

結論としてはこちらの方法を使って実装しました。Next.js13.3.0以上からnext/serverでできるようになりましたと冒頭で述べさせていただきました。

// pages/api/og.tsx
import { ImageResponse } from "next/server";
import { NextApiHandler } from "next";

export const config = {
  runtime: "edge",
};

const handler: NextApiHandler = async (req) => {
  if (!req.url) throw Error("not supported.");

  const { searchParams } = new URL(req.url);
  const title = searchParams.get("title");
  const postDate = searchParams.get("postDate");

  return new ImageResponse(
    (
      <div
        lang="ja-JP"
        style={{
          borderWidth: "36px",
          borderColor: "#5bbee5",
          backgroundColor: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          textAlign: "center",
          alignItems: "center",
          justifyContent: "center",
          color: "black",
          position: "relative",
        }}
      >
        <h2
          style={{
            color: "black",
            fontSize: 64,
          }}
        >
          {title}
        </h2>
        <div
          style={{
            display: "flex",
            position: "absolute",
            width: "100%",
            bottom: 0,
            paddingRight: 32,
            justifyContent: "flex-end",
          }}
        >
          <h2
            style={{
              color: "black",
              fontSize: 40,
            }}
          >
            {postDate}
          </h2>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  );
};

export default handler;

Next.js 13.3.0以上だとフォントファイルとか関係なく1MB以下に収めることができ無事vercelにデプロイすることができました。

元々このブログサイトはNext.js13では動いてなかったのでNext.js13にバージョンを上げたりとか、結構苦労しましたがその苦労に関してはまた別の機会にw
今思えば他のやり方があったのでは?と思う気持ちもあるのですが今回はこの辺りで失礼します。それでは良いNext.jsライフを〜!!

参照