【Next.js+Vercel】OGP画像を動的生成したい。
Next.jsで作ったこのブログ(CraftTime)のOGP画像を作成したい。
本ブログはデプロイ先が「Vercel」なので、@vercel/og
というライブラリを使ってOGP画像の動的生成をしていきます。
今回は、その作業工程をまとめてみました。初回は簡単なOGP画像を生成して
目次
手順①:ライブラリをインストールする
$ npm i @vercel/og
手順②:「og.tsx」を作成する
pages/api
ディレクトリ内にog.tsx
を作成します。
そして「OGP画像を生成するための処理」を記載していきます。このnew ImageResponse()
という関数に、htmlを渡すことでそのhtmlを画像に変換してくれます。
import {ImageResponse} from '@vercel/og';
export const config = {
runtime: 'edge',
};
export default function () {
return new ImageResponse(
(
// ==========================================
// OGP画像へ描画される内容
// ==========================================
<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,
},
);
}
出力される画像はローカル環境でもhttp://localhost:3000/api/og
で確認できます。
![](https://craft-time.jp/wp-content/uploads/2023/01/og-1-1024x512.png)
手順③:metaデータを設定する
ページごとにOGP画像を設定するには、metaタグで画像指定する必要があります。
import React from "react";
import Head from "next/head";
export default function Index() {
return (
<Head>
{/* metaタグを追加 */}
<meta property="og:image" content="https://example.com/api/og" />
</Head>
)
}
これで「OGP画像の生成」と「metaタグへの設定」ができました。
あとは表示させたいデザインに合わせてカスタマイズしていきます。
手順④:画像をカスタマイズする
- ページタイトルに合わせてテキストを動的に変える
- 背景画像を設定する
- テキストのフォントを変更する
タイトルに合わせて動的生成する
import {ImageResponse} from '@vercel/og';
import {NextRequest} from 'next/server';
export const config = {
runtime: 'edge',
};
export default function handler(req: NextRequest) {
try {
const {searchParams} = new URL(req.url); // URLパラメータを取得する
const hasTitle = searchParams.has('title'); // 'title'パラメータが存在するか判定
const title = hasTitle // 'title'パラメータの有無に合わせて出力
? searchParams.get('title')?.slice(0, 50)
: 'Craft Title';
return new ImageResponse(
(
// ==========================================
// OGP画像へ描画される内容
// ==========================================
<div style={{
fontSize: 128,
background: 'white',
width: '100%',
height: '100%',
display: 'flex',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
}}>
{title}
</div>
// ==========================================
),
{
width: 1200,
height: 630,
},
);
} catch (e: any) {
console.log(`${e.message}`);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
}
import React from "react";
import Head from "next/head";
export default function PostId({post}) {
return (
<Head>
{/* metaタグを追加 */}
<meta property="og:image" content={`https://example.com/api/og?title=${post.title}`} />
</Head>
)
}
背景画像を指定する
ベースとなる背景画像(/images/og-base.png
)を追加します。
![](https://craft-time.jp/wp-content/uploads/2023/01/og-base-1024x538.png)
追加した背景画像をbackgroundImage:
で指定します。
import React from "react";
import {ImageResponse} from '@vercel/og';
import {NextRequest} from 'next/server';
export const config = {
runtime: 'edge',
};
export default function handler(req: NextRequest) {
try {
const {searchParams} = new URL(req.url); // URLパラメータを取得する
const hasTitle = searchParams.has('title'); // 'title'パラメータが存在するか判定
const title = hasTitle // 'title'お粗メータの有無に合わせて出力
? searchParams.get('title')?.slice(0, 50)
: 'Craft Title';
return new ImageResponse(
(
<div
style={{
backgroundColor: "#fff",
backgroundSize: "100% 100%",
// ベース画像を追加
backgroundImage: `url(https://example.com/images/og-base.png)`, // ベース画像
height: '100%',
width: '100%',
display: 'flex',
textAlign: "left",
alignItems: "flex-start",
justifyContent: "center",
flexDirection: "column",
flexWrap: "nowrap",
}}>
{/* テキスト部分 */}
<div
style={{
width: '85%',
margin: 'auto',
marginTop: 90,
fontSize: 70,
fontStyle: 'bold',
letterSpacing: '-0.025em',
color: '#111',
lineHeight: 1.3,
whiteSpace: 'pre-wrap',
}}>
{title}
</div>
</div>
),
{
width: 1200,
height: 630,
},
);
} catch (e: any) {
console.log(`${e.message}`);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
}
![](https://craft-time.jp/wp-content/uploads/2023/01/og-2-1024x538.png)
こんな感じで出力されました。
フォントを指定する
今回は「Noto Sans Japanese(NotoSansJP-Bold.otf)」を使用したいと思います。
- Google Fontsのフォントページで「Noto Sans Japanese」をダウンロード
- ダウンロードしたデータ(
NotoSansJP-Bold.otf
)を軽量化する(参考ページ) - プロジェクトの
/assets
フォルダに軽量化したフォントファイルに追加する
import React from "react";
import {ImageResponse} from '@vercel/og';
import {NextRequest} from 'next/server';
export const config = {
runtime: 'edge',
};
// フォントを取得する
const font = fetch(new URL('../../assets/NotoSansJP-Bold.otf', import.meta.url)).then(
(res) => res.arrayBuffer(),
);
export default async function handler(req: NextRequest) {
try {
const fontData = await font;
const {searchParams} = new URL(req.url); // URLパラメータを取得する
const hasTitle = searchParams.has('title'); // 'title'パラメータが存在するか判定
const title = hasTitle // 'title'お粗メータの有無に合わせて出力
? searchParams.get('title')?.slice(0, 50)
: 'Craft Title';
return new ImageResponse(
(
<div
style={{
backgroundColor: "#fff",
backgroundSize: "100% 100%",
backgroundImage: `url(${process.env.NEXT_PUBLIC_SITE_URL}/images/og-base.png)`, // ベース画像
height: '100%',
width: '100%',
display: 'flex',
textAlign: "left",
alignItems: "flex-start",
justifyContent: "center",
flexDirection: "column",
flexWrap: "nowrap",
}}
>
{/* テキスト部分 */}
<div
style={{
width: '85%',
margin: 'auto',
marginTop: 90,
fontSize: 70,
fontStyle: 'bold',
fontFamily: 'NotoSansJP', // フォントを指定
letterSpacing: '-0.025em',
color: '#111',
lineHeight: 1.3,
whiteSpace: 'pre-wrap',
}}
>
{title}
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'NotoSansJP',
data: fontData,
style: 'normal',
},
],
},
);
} catch (e: any) {
console.log(`${e.message}`);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
}
![](https://craft-time.jp/wp-content/uploads/2023/01/og-3-1024x538.png)
おまけ:OGP画像デザインのテスト用ページを作った
画像生成のデザイン(レイアウト)を調整する時に、Chromeデベロッパーツール(検証モード)などで確認しながら描画範囲を確認したかったので、一時的にテストページを作って確認していました。
(og.tsx
では画像化された状態で出力され、marginなどがどの範囲に適用されているのか確認しづらかった)
import React from "react";
import {NextRequest} from "next/server";
export default function OGP() {
const title = "OGP画像デザインのテスト用ページを作る"
return(
<div className="w-screen h-screen bg-slate-200 flex items-center mx-auto">
<div style={{display: 'flex', alignItems: 'center', textAlign: 'center', margin: 'auto', width: '1200px', height: '630px'}}>
{/* ======================================== */}
<div
style={{
backgroundColor: "#fff",
backgroundSize: "100% 100%",
backgroundImage: `url(${process.env.NEXT_PUBLIC_SITE_URL}/images/og-base.png)`,
height: '100%',
width: '100%',
display: 'flex',
textAlign: "left",
alignItems: "flex-start",
justifyContent: "center",
flexDirection: "column",
flexWrap: "nowrap",
}}>
{/* テキスト部分 */}
<div
style={{
width: '85%',
margin: 'auto',
marginTop: 100,
fontSize: 70,
fontStyle: 'bold',
letterSpacing: '-0.025em',
color: '#111',
lineHeight: 1.4,
whiteSpace: 'pre-wrap',
}}>
{title}
</div>
</div>
{/* ======================================== */}
</div>
</div>
)
}
まとめ
今回初めてVercelの「OG Image Generation」を利用して、思っていた以上に簡単に実装することができました。
ブログなどOGPを生成するシーンがそこまで多くない場合は、この方法で十分そうですね。「metaタグのパスに拡張子がないけど大丈夫かな?」という点が気になるのでもう少し調べたいと思います。
最近の記事
![](/_next/image?url=%2Fimages%2Fnoimage.png&w=3840&q=75)
![](/_next/image?url=%2Fimages%2Fnoimage.png&w=3840&q=75)
![](/_next/image?url=%2Fimages%2Fnoimage.png&w=3840&q=75)
![](/_next/image?url=%2Fimages%2Fnoimage.png&w=3840&q=75)
![](/_next/image?url=%2Fimages%2Fnoimage.png&w=3840&q=75)
![](/_next/image?url=%2Fimages%2Fnoimage.png&w=3840&q=75)