...
Next.jsで個人ブログサイトを作ってるときに、OGP(Open Graph Protocol)の画像を動的にしたかった。せっかくNext.js使ってるから、Nextの機能でAPI作って返す方法探してみたら ImageResponse なるものを見つけたので、試してみた。
どうやら普段ページを作るのと同じく、jsxを描くだけで画像が作れるらしい。
以下環境
今更だけど、Nextではページとは別にAPIを生やすことができる。このAPIは実行場所をedgeかnodeサーバーか選ぶことができ、サイト制作のちょっとした機能を作るのに使える。
今回は各ページの内容から動的にOGPの画像を生成したいので、「タイトル」と「サムネ画像」をクエリパラメータとして画像を生成して返すAPIを作り、ページのog:image
タグにこのURLを指定することにした。
ディレクトリ構成はこんな感じ。App Routerではどこにファイルを配置するかによってURLのパスが変わるんだけど、今回は/api/og
に置くことにした。
(root)
└ app/
└ api/
└ og/
└ route.tsx
ここにハンドラ関数を作って、next/ogのImageResponseを使って結果を返すようにする。
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
return new ImageResponse(/* ここに色々渡す */);
}
ページ側からは、このAPIのURLを叩くようにする。
このとき、描画に必要な情報だけをクエリパラメータで渡すようにした。そうじゃないと、ページ生成側とAPI側でデータ取得が重複してしまう。
export const generateMetadata = async ({ params, searchParams }: Props) => {
const data = await getDetail(params.slug, { draftKey: searchParams.dk }); // データを取ってくる外部関数
return {
openGraph: {
url: siteUrl,
title: `${data.title} - ${siteName}`,
siteName: siteName,
type: "article",
images: {
// 作ったAPIのURLを指定
url: `${siteUrl}/api/og?title=${data.title}${data.thumbnail?.url !== undefined ? `&thumbnail=${data.thumbnail.url}` : ""}`,
width: 1200,
height: 630
},
},
/* その他色々 */
};
};
ImageResponseにJSXを渡すことで画像を描画できるので、タイトルとサムネイルを渡してデザインを組んでいく。
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
return new ImageResponse(
<OgImage
title={searchParams.get("title") ?? ""}
thumbnail={searchParams.get("thumbnail") ?? undefined}
/>
);
}
type OgImageProps = {
title: string;
thumbnail: string | undefined;
};
const OgImage = ({ title, thumbnail }: OgImageProps) => (
<div>
{/* 中身のデザイン */}
</div>
);
next/ogでは内部的にsatoriを使用しているため、いくつか制約がある点に注意。
display: none
またはdisplay: flex
のみ指定可能。長くなるので一部だけだけど、こんな感じで実装した。
const OgImage = ({ title, thumbnail }: OgImageProps) => (
<div
style={{
display: "flex",
position: "relative",
background: "linear-gradient(to bottom, #8791a3 0%, #6c717a 100%)",
width: 1200,
height: 630,
color: "#dddddd",
border: "solid 24px rgba(221, 221, 221, 0.7)",
}}
>
<img
style={{
position: "absolute",
width: 416,
height: 234,
left: 368,
top: 70,
border: "solid 4px rgba(221, 221, 221, 0.7)",
borderRadius: 4
}}
src={thumbnail ?? `${siteUrl}/default_thumbnail.png`}
alt={title}
/>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: 42,
position: "absolute",
width: 800,
height: 100,
top: 330,
left: 176
}}
>
<p
style={{
margin: 0,
textAlign: "center"
}}
>
{title}
</p>
</div>
{/* その他諸々 */}
</div>
);
Advanced Features: デバッグ | Next.js
nextのserverless APIは、デバッグ時にdevサーバーを以下のオプションで起動する必要がある。
NODE_OPTIONS='--inspect' next dev
これを、package.json
のscriptsに指定して上げれば良い。
ちなみにWindowsの場合は、npmからcross-env
をインストールして以下のようにする必要がある。
cross-env NODE_OPTIONS='--inspect' next dev
成功すると、無事にdevサーバーで画像生成のAPIを実行し、レンダリング結果をブラウザから確認することができる。
当たり前だが、APIで画像を生成しているので、動的プレビューやDevToolsを利用することはできない。
フォントを使う場合、ローカルからバイトデータを読み込んでくる必要がある。
Open Graph (OG) Image Examples
ここを参考に、フォントの読み込みを実装した。
ただし、後述する理由でedge runtimeでの実行を断念してnodeで動かしているため、fetch APIではなくfsを使用している。
How can I use files in Serverless Functions on Vercel?
ちなみに、next/ogの前身は@vercel/ogなので、こちらの名前で検索すると情報が出てきやすい。
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const font = await fs.readFile(path.join(process.cwd(), "assets", "mplus-2c-medium.ttf"));
return new ImageResponse(
<OgImage title={searchParams.get("title") ?? ""} thumbnail={searchParams.get("thumbnail") ?? undefined} />,
{
fonts: [
{
name: "m-plus-2c",
data: font,
weight: 500,
style: "normal"
}
]
}
);
}
フォントファイルは以下のように配置している。ライセンス表記も同じ階層に配置。
(root)
└ assets/
┝ OFL.txt
└ mplus-2c-medium.ttf
これで、あとはnameに指定したフォントをfontFamily
に指定すればOK。成功すると、無事に指定したフォントが使用されるのを確認できる。
const OgImage = ({ title, thumbnail }: OgImageProps) => (
<div
style={{
fontFamily: '"m-plus-2c"',
fontWeight: 500,
fontStyle: "normal",
// その他諸々
}}
>
// 以下省略
</div>
);
画像生成のAPIが出来たので、ここからはデプロイ先のVercelで動かしてみる。Vercelに乗せる際はいつもと変わらず、GitHubのリポジトリを指定してあげれば勝手にデプロイされる。
edge runtimeで実行しようとしたけど、サイズ制限に引っかかってデプロイできなかった。Vercelのedgeでの容量制限は1MB以内で、これはedgeで実行するroute.tsx
が参照しているすべてのモジュールやファイルを集めてgz圧縮した状態での容量となっている。
使用したカスタムフォントが大きいのかなと思い、subset化やwoff形式による圧縮でなんとか頑張ったけど、1MB以内に収めることはできなかった。
カスタムフォントを完全にのぞいてミニマルな状態でデプロイしても制限に引っかかったので、next/ogの中身を見てみたら1MB越えのwasmファイルを発見。てっきりフォールバックフォントが重いのかと思ってたから意外だった。
ミニマルな状態でもedgeに乗らないということで、仕方なくnodeで動かすことにした。にしてもサンプルではedge runtimeで動かしているんだけどな......
聞いた話によると、next/ogは最近実装が変わってサイズが増えたとかなんとか。ほんとに聞いた話だけど。
最後に、無事にOGPの画像を指定できているかをOGPチェッカーで確認。
OGP確認:facebook、twitter、LINE、はてなのシェア時の画像・文章を表示 | ラッコツールズ🔧
無事に生成出来てそう。discordでも問題なし。
というわけで、無事に当サイトのOGPの画像を動的に生成することができた。めでたしめでたし~
どうでもいいけど、クエリパラメータを手動で指定すれば存在しない記事を捏造することができる。悪用はやめてね