🥰

@vercel/og を使って Next.js で OGP 画像を生成する仕組みを作ったお話

2023/09/18に公開

こんにちは! FUSSY で業務委託として参加しているエンジニアの小野寺と申します。
この記事では、我々が運営している FUSSY というサービスでの OGP 生成機能について解説をしていきたいと思います 🚀

これはどんな話?

  • @vercel/og を活用して Next.js で 動的な OGP 画像を生成する仕組みを開発したお話
  • React 風にレイアウトを構築できるので簡単に OGP 自動生成機能を実現可能
  • 実装の工夫点も記載しているので役に立つかも!

FUSSY とは

我々が運営している FUSSY は「みんなで編集するデータベース」です。
Web 上には多くのデータベースがありますが、その多くは一人の管理者が編集しています。
一人で編集をし続けることは、自身のこだわりを反映できる一方で、編集を続ける体力が求められたり、譲渡が難しいといった課題があります。
サービスを通じて、これらの課題を解決する機能を FUSSY は提供しています。

FUSSY では Google フォームのような方法でデータの入力ができる UI を提供しています。

より多くの方が、データを使ってファン活動・推し活を楽しめるサービスの提供を目指し、日々の開発を進めております。

@vercel/og とは

https://vercel.com/docs/functions/edge-functions/og-image-generation

@vercel/og は、Vercel が 2022 年の 10 月に公開した、Edge Function 上で OGP 画像を生成するためのライブラリです。
Satori と呼ばれる、JSX で PNG 画像を生成することができるライブラリが使われております。
React に慣れていれば、すぐに使い始められます。

今回のユースケース

FUSSY では、「データベース」や「クエスト」といった機能を提供しています。

データベース画面のスクリーンショット

クエスト画面のスクリーンショット

データベースは、自分の好きな漫画、アニメ、アーティスト、アイドルなどの情報をまとめて、保存するための機能です。
また、クエストは様々な推し活を大小様々なタスクとして完了して、ポイントを貯めていく機能です。
特にデータベースは複数人での利用をおすすめしており、みんなで楽しみながら新たなデータベースを作り上げることを想定しています。

これら機能は、SNS での共有を通じて他のユーザーの投稿を見て「わかる!」と共感したり、まだ知らない魅力を発見したりして楽しむことができます。
SNS で共有した際、魅力的な OGP 画像があることでユーザの皆さまに楽しんでいただけると考えました。

データベースやクエストの詳細な内容は、以下に記載しておりますのでご気軽にご覧ください!

実際に実装してみる

FUSSY は Next.js を使用してフロントエンドが構成されております。
(なお、Pages Router でのルーティングを採用しております。)

yarn add @vercel/og

インストールはこれだけです。

続いて、OGP 画像を生成するコンポーネントを作成します。
Open Graph (OG) Image Generation | Vercel Docs を参考して、/pages/api/{任意のエンドポイント名} の下に index.tsx を追加します。

(例) /pages/api/{任意のエンドポイント名}/index.tsx
import { ImageResponse } from "@vercel/og";

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

export default async function handler(req: NextRequest) {
    // NOTE: テキストをパラメータで指定することで動的な文章の表示が行える
    // const { searchParams } = new URL(req.url);
    // const text = searchParams.get("text");

    return new ImageResponse(
        (
            <div
                style={{
                    fontSize: 40,
                    color: "black",
                    background: "white",
                    width: "100%",
                    height: "100%",
                    padding: "50px 200px",
                    textAlign: "center",
                    justifyContent: "center",
                    alignItems: "center",
                }}
            >
                👋 Hello नमस्ते こんにちは สวัสดีค่ะ 안녕 добрий день Hallá
            </div>
        ),
        {
            width: 1200,
            height: 630,
        }
    );
}

これで @vercel/og を使った OGP 画像の生成は完了です。

https://vercel.com/docs/functions/edge-functions/og-image-generation でのチュートリアルに記載されている OGP 画像

あとは、View の部分を別コンポーネントに分けることで実装は完了です!

(例) /pages/api/{任意のエンドポイント名}/index.tsx
import { ImageResponse } from "@vercel/og";

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

export default async function handler(req: NextRequest) {
    return new ImageResponse(
        (
            <OgpExampleComponent>
                👋 Hello नमस्ते こんにちは สวัสดีค่ะ 안녕 добрий день Hallá
            </OgpExampleComponent>
        ),
        {
            width: 1200,
            height: 630,
        }
    );
}
(例) /components/ogp/OgpExampleComponent/index.tsx
import { FC, ReactNode } from "react";

type Props = {
    children: ReactNode
}

const OgpExampleComponent: FC<Props> = ({ children }) => {
    return (
        <div
            style={{
                fontSize: 40,
                color: "black",
                background: "white",
                width: "100%",
                height: "100%",
                padding: "50px 200px",
                textAlign: "center",
                justifyContent: "center",
                alignItems: "center",
            }}
        >
            {children}
        </div>
    );
}

export default OgpExampleComponent;

@vercel/og のための工夫点や注意点

Satori 内での CSS 対応状況について

Satori は、内部の挙動として React Native などで利用されている Yoga Layout の WASM 版を利用しています。
そのため、すべての CSS の機能をサポートしているわけではありません。
Satori の公式リポジトリに使用可能な CSS プロパティが記載されているので、詳細な対応状況はリンク先からご確認いただけると嬉しいです 🙏

https://github.com/vercel/satori#css

デプロイ制限を回避しながら Google Fonts を読み込む

Satori では、 OpenType.js がサポートしている TTF/OTF/WOFF を利用したカスタムフォントの適用が可能です。
しかしながら、Vercel Edge Functions を使用している場合、Hobby プランだとデプロイサイズが 1MB までに制限されています。
一般的な日本語フォントは、ファイルサイズが 5-15MB あたりが多いため、サブセット化などを行わない限り現実的な利用は厳しいです。
また、サブセット化を行っても表示可能な文字の範囲が狭くなってしまい、満足にフォントの描画が行えないことも多いです。

ここでは、Google Fonts を活用したカスタムフォントの適用方法を解説します。
Satori Playground の実装 に動的にフォントデータを取得できる実装が存在しており、参考にします。

export async function fetchFont(text: string): Promise<ArrayBuffer | null> {
    // NOTE: Noto Sans JP を使用する場合は以下の URL になる。(他のフォントを使用する場合は別のフォント名にする)
    // text パラメータを付与することで、使用する文字のみをサブセット化を行い通信容量を削減しながら通信が可能となる。
    const googleFontsUrl = `https://fonts.googleapis.com/css2?family=Noto+Sans+JP&text=${encodeURIComponent(
        text
    )}`;

    const css = await (
        await fetch(googleFontsUrl, {
            headers: {
                "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",
            },
        })
    ).text();

    const resource = css.match(
        /src: url\((.+)\) format\('(opentype|truetype)'\)/
    );

    if (!resource) return null;
    const res = await fetch(resource[1]);
    return res.arrayBuffer();
}

これによって、Google Fonts を活用したカスタムフォントでの OGP 画像の提供を実現しています。
Vercel Edge Functions でもデプロイ制限に引っかからずにカスタムフォントの適用が可能になります。

Vercel Preview を活用して開発を加速させる

Vercel Preview を組み合わせて、実装後のレビューを加速させましょう。
Vercel Preview では、ブランチにコミットが push されると自動でプレビュー環境が作成される機能が備わっております。

PR 作成時に発行される Vercel Preview のコメント

これによって、PR 作成時に OGP 生成 の URL をコメントとして貼っておくだけで、コードと生成画像の確認が同時に行えます。
動作検証は URL を貼るだけだったので、レビューが円滑に行えてとても便利でした。

実装レビューで OGP を URL を貼って動作確認をしている図

また、Vercel Preview には OGP の Preview を確認できる機能 も搭載されています。
これによって、見た目だけではなく、クロール側でも問題なく情報が読み取られているのかを確認することができます。

FUSSY で導入している OGP について

現在、FUSSY では以下のような OGP 画像を使用しています!

[例1] データベース共有で表示される OGP [例2] クエスト共有で表示される OGP
FUSSY で使用しているデータベースの OGP 画像 FUSSY で使用しているクエストの OGP 画像

おわりに

@vercel/og を活用した FUSSY での動的 OGP 画像の生成について解説させていただきました!
Next.js の延長線で簡単に OGP 画像が生成できるのはとても魅力的だと思います。
今回のケースでは OGP への活用でしたが、Satori の活用は様々なケースへの発展も見込めそうです。(背景画像の自動生成など)
皆さんも好きなレイアウトで画像を生成する仕組みを作ってみてはいかがでしょうか。
最後まで読んでいただき、ありがとうございました!

Discussion