vercelのog image generationを使用して、Next.jsで動的にOG画像を自動生成する
この記事は ミライトデザイン Advent Calendar 2022 の 23 日目の記事です。
前日の kakiuchi の記事に続いて書いていきます。
はじめに
2022/10/10 に vercel から OG Image を自動生成するライブラリの発表がありました。
このライブラリを使って Next.js で作ったページの OG 画像を動的に生成する方法を紹介します。
前回の記事を例にこんな感じで表示させることができます。
環境
- react: 18.2
- next: 12.3.1
- vercel/og: 0.0.20
OG画像を生成するライブラリ
vercel が発表した @vercel/og
というライブラリは、Vercel Edge Functions で HTML/CSS(JSX)でマークアップしたものを動に OG 画像として生成できるライブラリです。
Vercel Edge Functions とは、エッジで関数を実行できる環境となるので、@vercel/og
は runtime: 'edge'
という設定をして Edge Runtime で使用する必要があります。
また、Next.js は v12.2.3 以降を使用する必要があります。
コアエンジンでは Satori と Resvg という 2 つのライブラリを使用して、最終的に PNG に変換しています。
具体的には、Satori というライブラリが HTML/CSS(JSX)から SVG を生成し、Resvg が生成された SVG を PNG に変換しているような流れで、これを Edge 環境で行っています。(JSX → SVG → PNG)
使用方法
基本的には公式の通り進めてけば問題ないです。
サンプルリポジトリを作成しているので参考に見てみてください。
画像生成の設定
まずはライブラリのインストールをします。
yarn add @vercel/og
ライブラリをインストールしたら pages/api
配下に og.tsx
を作成して、og 画像を生成するための API エンドポイントを作成します。
config に runtime: 'edge'
を指定して ImageResponse
を return するだけです。
今回は公式の「Hello world!」の画像で設定をしています。
import { ImageResponse } from '@vercel/og';
export const config = {
runtime: 'edge',
};
export default function handler() {
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,
},
);
}
作成したら yarn dev
でサーバーを立ち上げ http://localhost:3000/api/og を確認すると画像が生成されていることが確認できます。
画像の生成はこれだけです。
OGPの設定
ただ、このままだと画像が生成されただけなので、OG 画像として表示されるように、page の <Head>
に <meta>
タグを追加します。
ここでは、試しにトップページで確認できるように pages/index.tsx
で設定します。
実際に確認ができるように vercel にデプロイして絶対パスを指定しておきましょう。
<meta
property="og:image"
content="<https://vercel-og-sample.vercel.app/api/og>"
/>
デプロイできたら OGP を確認できるツールで確認してみましょう。
先程確認した画像で OG 画像が設定されていることが確認できます。
ページごとで動的に画像を生成する
画像を生成して OG 画像として設定できたことが確認できましたが、このままでは全ページ「Hello world!」の画像となってしまいます。
そのため、次はページごとに動的に画像が変わるように設定していきます。
ページごとに OG 画像を生成するためには、query パラメーターを使用して動的にページタイトルやその他要素を画像に挿入できます。
https://vercel-og-sample.vercel.app/api/og?title=hogehoge&publishedAt=2020-12-23
まずは、先程作成した og.tsx
を下記のように修正します。
import { ImageResponse } from "@vercel/og";
import { NextRequest } from "next/server";
export const config = {
runtime: "edge",
};
export default function (req: NextRequest) {
const { searchParams } = new URL(req.url);
const hasTitle = searchParams.has("title");
const title = hasTitle
? searchParams.get("title")?.slice(0, 100)
: "My default title";
return new ImageResponse(
(
<div
style={{
fontSize: 128,
background: "white",
width: "100%",
height: "100%",
display: "flex",
textAlign: "center",
alignItems: "center",
justifyContent: "center",
}}
>
{title}
</div>
),
{
width: 1200,
height: 600,
}
);
}
最初と違う点としては、 searchParams
でパラメーターから title
のデータを取得して、画像のテキストに挿入しています。
試しに先程トップページで指定した url に title
のパラメーターを付与して確認してみます。(画像の確認なので、localhost で問題なし)
開発環境で、api のパスにアクセスしてみます。
http://localhost:3000/api/og?title=OG画像を動的に生成
するとパラメーターで渡した title が画像に表示されていることを確認できました。
サンプルブログを作成し、ページごとにパラメーターを変更してOG画像を設定する
あとは、トップページではなくページごとの <meta>
タグにクエリーパラメーターを設定するようにします。
サンプルとして、ブログのように一覧ページと詳細ページを作成して各ページで動的に OG 画像が生成されるようにします。
APIの作成
まずはブログのダミーデータを取得できるように API を作成します。
Post の型定義。
export type Post = {
id: string;
title: string;
};
ブログのダミーデータ。
import { Post } from "../types/post";
export const postsData: Post[] = [
{
id: "4d20c87f-3497-125b-209d-6aa0c44333ac",
title: "OG画像を動的に生成する",
},
{
id: "46ab05ad-f790-574e-5367-a88a21bf6916",
title: "Next.jsでJamstackな個人サイトを作った",
},
];
post 一覧取得 API。
import type { NextApiRequest, NextApiResponse } from "next";
import { postsData } from "../../../mock/posts";
import { Post } from "../../../types/post";
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Post[]>
) {
res.status(200).json(postsData);
}
post 詳細 API。
import type { NextApiRequest, NextApiResponse } from "next";
import { postsData } from "../../../mock/posts";
import { Post } from "../../../types/post";
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Post>
) {
const posts = postsData;
const { id } = req.query;
const post = posts.filter((post) => post.id === id)[0];
res.status(200).json(post);
}
これで、ブログのデータを取得できる API ができました。
試しに確認してみます。
http://localhost:3000/api/posts
http://localhost:3000/api/posts/4d20c87f-3497-125b-209d-6aa0c44333ac
これで、一覧及び詳細データが取得できました。
API ができたら、この時点でデプロイをしておきます。
この後ページを作成しますが、API からダミーデータを取得して事前ビルドをする必要があるので、本番環境で API が無いと事前ビルドでデータが取得できずにエラーとなってしまいます。
ページの作成
あとは一覧と詳細のページファイルを作成しますが、開発環境と本番環境で url が変わるように env の設定をしておきます。
NEXT_PUBLIC_APP_URL=http://localhost:3000
本番環境では、自身がデプロイした環境で設定してください。
vercel の場合だと、ダッシュボードの Settings > Environment Variables
で設定ができます。
では、ダミーデータを API から取得して、一覧画面に表示させるためのページファイルを作成します。
import { GetStaticProps, NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { Post } from "../../types/post";
type DataType = {
contents: Post[];
};
export default function Posts({ contents }: DataType) {
return (
<>
<Head>
<title>posts</title>
<meta name="description" content="記事一覧" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<main
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
padding: "6rem",
}}>
<ul>
{contents.map((post) => (
<Link key={post.id} href={`/posts/${post.id}`}>
<li>{post.title}</li>
</Link>
))}
</ul>
</main>
</>
);
}
export const getStaticProps: GetStaticProps<DataType> = async () => {
const data = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/posts`).then(
(data) => data.json()
);
return {
props: {
contents: data,
},
};
};
一覧ページを確認すると、ダミーデータのリストが表示されたことを確認できます。
次に詳細画面のページファイルを作成します。
import {
GetStaticPaths,
GetStaticPathsResult,
GetStaticProps,
InferGetStaticPropsType,
NextPage,
} from "next";
import Head from "next/head";
import { Post } from "../../types/post";
import { ParsedUrlQuery } from "querystring";
type Params = {
id: string;
} & ParsedUrlQuery;
type DataType = { post: Post };
type Props = InferGetStaticPropsType<typeof getStaticProps>;
export default function Posts({ post }: Props) {
return (
<>
<Head>
<title>{post.title}</title>
<meta name="description" content={post.title} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
property="og:image"
content={`${process.env.NEXT_PUBLIC_APP_URL}/api/og?title=${post.title}`}
/>
</Head>
<main
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
padding: "6rem",
}}>
<p>id: {post.id}</p>
<h2>{post.title}</h2>
</main>
</>
);
}
export const getStaticPaths: GetStaticPaths = async (): Promise<
GetStaticPathsResult<Params>
> => {
const data = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/posts`).then(
(data) => data.json()
);
const paths = data.map((post: Post) => {
return {
params: {
id: post.id,
},
};
});
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps<DataType, Params> = async ({
params,
}) => {
if (!params?.id) {
throw new Error("Error: ID not found");
}
const post = await fetch(
`${process.env.NEXT_PUBLIC_APP_URL}/api/posts/${params.id}`
).then((data) => data.json());
return {
props: { post },
};
};
一覧からブログを選択すると詳細画面に遷移され、id と title が表示されることが確認できました。
http://localhost:3000/posts/46ab05ad-f790-574e-5367-a88a21bf6916
また、詳細ページの meta タグを確認してみてください。
<meta
property="og:image"
content={`${process.env.APP_URL}/api/og?title=${post.title}`}
/>;
上記のように記述していますが、ビルドされたあとは API から取得したデータのタイトルが表示されていることが確認できます。
これで、ページごとに OG 画像が生成されるようになったので、最後にデプロイして OGP 確認ツールで見てみます。
ページのタイトルを取得して OG 画像が設定されていることが確認できました。
その他カスタマイズ
OG 画像の設定はできるようになったので、あとはスタイルをカスタマイズして完成させます。
公式の方でサンプルを用意していたり、play ground で確認できたりするので、後は自分好みにカスタマイズしてスタイルを整えていきましょう。
まとめ
もともと OG 画像は自分で作成して設定していたりしたので、自動で生成してくれると楽ですし、デザインも統一されるのでとてもいいなと思いました。
Vercel が用意してくれているライブラリで簡単に OG 画像が生成できるので、よかったら試してみてください。
次は「NFT と仮想通貨とブロックチェーン関連」について書いてくれる FrozenVoice さんの記事です。
Discussion