kk-web

Next.jsでOGP画像を動的に生成する方法

2024-07-12

調べてみると色々と情報が錯綜していたので、備忘録がてら。

今回は以下の仕様となっています。

  • Next.js
  • Vercel

適当に router.tsx ファイルを作成し、以下のように書いてあげたら動きました。

import { promises as fs } from "fs";
import path from "path";
import { ImageResponse } from "@vercel/og";
import { NextRequest } from "next/server";
import parseMD from "parse-md";

function getUniqueChars(text: string): string {
  const charSet = new Set<string>();

  for (const char of text) {
    charSet.add(char);
  }

  return Array.from(charSet).join("");
}

type GetArticleParams = {
  slug: string;
};

type GetArticleData = {
  title: string;
};

async function getArticle({ slug }: GetArticleParams): Promise<GetArticleData> {
  const markdownPath = path.join(
    process.cwd(),
    "/src/markdown-pages",
    `${slug}.md`,
  );
  const fileContents = await fs.readFile(markdownPath, "utf8");
  const { metadata } = parseMD(fileContents);
  const { title } = metadata as {
    title: string;
  };

  return { title };
}

type Context = {
  params: { slug: string };
};

export async function GET(
  _: NextRequest,
  { params: { slug } }: Context,
): Promise<ImageResponse> {
  const { title } = await getArticle({ slug });
  const fontData = await fetch(
    `https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700&text=${encodeURIComponent(
      getUniqueChars(title),
    )}`,
  ).then((res) => res.text());
  const fontUrl = fontData.match(/url\((.*?)\)/)?.[1];

  if (!fontUrl) {
    throw new Error("Failed to load font");
  }

  const font = await fetch(fontUrl).then((res) => res.arrayBuffer());
  const imageIndex = (parseInt(slug, 10) % 2) + 1;

  return new ImageResponse(
    (
      <div
        style={{
          background: `url('https://hogefuga/piyomoge.png')`,
          display: "flex",
          height: "100%",
          padding: "30px 36px",
          width: "100%",
        }}
      >
        <div
          style={{
            alignItems: "center",
            color: "#fff",
            display: "flex",
            fontFamily: '"Noto Sans JP", sans-serif',
            fontSize: "42px",
            fontWeight: "bold",
            height: "44%",
            justifyContent: "center",
            padding: "0 36px",
            textAlign: "center",
            width: "100%",
          }}
        >
          {title.split("\n").map((line, i) => (
            <div key={i}>{line}</div>
          ))}
        </div>
      </div>
    ),
    {
      debug: false,
      fonts: [
        {
          data: font,
          name: "Noto Sans JP",
          style: "normal",
          weight: 700,
        },
      ],
      height: 630,
      width: 1200,
    },
  );
}

今回は静的に配置されたマークダウンファイルから OGP 画像を動的に生成しているのでちょっと大げさですが、大まかな実装感は変わらないかなと。

Google Fonts を噛ませて日本語フォントに対応しましたが、サブセット化はきっちり行いましょう。

© 2018 kk-web