OGPの画像をいちいち作るのが面倒だったので、Next.jsの機能を使って生成できるようにしました。
OGPって何?
OGPとは、Open Graph Protocolの略で、ウェブサイトの内容をソーシャルメディアでシェアしたときに表示できるデータです。XやFacebookなどのSNSでウェブサイトをシェアしたときに、タイトル、説明、画像などが表示されているのを見たことがありますよね。
OGPの目的は、ウェブサイトがソーシャルメディア上でシェアされた際に、適切な形式で表示されるようにすることです。URLだけではどのようなウェブサイトかわかりにくいですが、タイトルや画像などが表示できていれば、どのようなウェブサイトかわかります。
OGPを設定する
OGPはHTMLの<head>内に<meta>タグとして配置されます。代表的なOGPタグには、以下のようなものがあります。
<meta property="og:title" content="ページのタイトル">: シェアされたページのタイトルを指定します。<meta property="og:description" content="ページの説明">: シェアされたページの説明を指定します。<meta property="og:image" content="画像のURL">: シェアされた際に表示される画像のURLを指定します。
この記事では、Next.jsの機能を使ってOGPを設定していきます。
Next.jsのOGP
基本は、Next.js公式のApp Routerのチュートリアルを参考にしました。
OGPの設定
まずは、静的なデータをlayout.tsxに記述して設定します。
import { Metadata } from 'next'
const siteName = 'サイトのタイトルです'
const description = 'サイトの説明です'
export const metadata: Metadata = {
title: {
default: siteName,
template: `%s | ${siteName}`,
},
description,
openGraph: {
title: siteName,
description,
siteName,
locale: 'ja_JP',
type: 'website',
images: {
url: `image/ogp.png`,
},
},
}ページごとに異なる動的なデータは、page.tsxに記述します。generateMetadataを使うと、動的に設定できます。
import { Metadata } from 'next'
export const generateMetadata = async (slug): Promise<Metadata> => {
const post = await getPostBySlug(slug) // ブログの記事データの取得
if (post) {
return {
title: post.title, // ページのタイトル
openGraph: {
title: post.title, // ページのタイトル
description: post.description, // ページの説明
images: {
url: `/api/og?title=${post.title}`, // ページの画像URL
},
},
}
}
return {}
}これによって、HTMLの<head>内の<meta>タグにデータが入ります。
<title>サイトのタイトルです</title>
<meta name="description" content="サイトの説明です" />
<meta property="og:title" content="サイトのタイトルです" />
<meta property="og:description" content="サイトの説明です" />
<meta property="og:image" content="https://~" />これで、OGPを設定できるようになります。
画像については、APIを作成して、記事のタイトルから動的に作成できるようにします。
OGP画像の作成
画像を作成するためのAPIを作成します。/api/og?title=記事のタイトルにアクセスしたときに、記事のタイトルの入った画像を表示できるようにします。api/og/og.tsxを作り、APIを記述していきます。
App RouterのAPIについては、公式を参考にしました。
og:imageに入る画像を生成するためには、ImageResponseを使います。ImageResponseにJSXを渡すことで画像を描画できます。
一部省略していますが、画像を生成するソースはこんな感じです。ファイルは、api/og/route.tsxです。
import { ImageResponse, NextRequest } from 'next/server' // v14.0.0以降ではnext/ogにしましょう。
import { loadGoogleFont } from '@lib/fonts'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const title = searchParams.get('title') ?? '' // ブログ記事のタイトル
try {
// Google Fontの読み込み
const googleFontArrayBuffer = await loadGoogleFont({
family: 'Mochiy Pop One', // 好きなフォントを設定する。
weight: 400,
text: title,
})
return new ImageResponse(
(
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
textAlign: 'center',
width: '100%',
height: '100%',
backgroundImage: 'url(背景画像のURL。省略)',
backgroundSize: '1200px 630px',
backgroundRepeat: 'no-repeat',
}}
>
<div>{title}</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'MochiyPopOne',
data: googleFontArrayBuffer,
style: 'normal',
weight: 400,
},
],
}
)
} catch (error) {
console.error('Error while generating OGP image:', error)
return new Response(`Failed to generate the image`, {
status: 500,
})
}
}satori を使っているようですので、使えるCSSにはいくつか制約があります。
フォントの読み込み
OGPの画像にGoogle Fontsを使います。日本語で JIS第三、四水準漢字対応、かつかわいいフォントを探しました。
lib/font.tsを作って、GoogleFontsを読み込む処理を記述します。APIのファイルに書くとビルド時にエラーが出たのでファイルを分けました。
export async function loadGoogleFont({
family,
weight,
text,
}: {
family: string
weight?: number
text?: string
}) {
const params = new URLSearchParams({
family: `${family}${weight ? `:wght@${weight}` : ''}`,
})
if (text) {
params.append('text', text)
} else {
params.append('subset', 'latin')
}
const url = `https://fonts.googleapis.com/css2?${params.toString()}`
const css = await fetch(url).then((res) => res.text())
const fontUrl = css.match(
/src: url\((.+)\) format\('(opentype|truetype)'\)/,
)?.[1]
if (!fontUrl) {
throw new Error('Font file not found in CSS fetched from Google Fonts')
}
return fetch(fontUrl).then((res) => res.arrayBuffer())
}これで、記事のタイトルに応じて画像を作成できました! Localhostでは、http://localhost:3000/api/og?title=記事のタイトルにアクセスすると、画像を表示できます。
表示が遅い場合は、フォントファイルをローカルに置いて読み込むと早くなるかもしれません。
お役に立てれば幸いです💕
参考サイト

