
Next.js で記事の OGP 画像を自動生成する
最近よく見るようになった、記事タイトルを含めたサイトの og:image
ですが、Next.js の場合、簡単に作れることがわかったのでやってみました。
'next/og'
Next.js にはまさに OGP のためにあるみたいな名前の next/og
というものがあり、これを使うことで実現できるみたいです。
ということで、
app/blog/[slug]/og.png/route.tsx
にファイルを作ります。
import fs from 'node:fs/promises'
import path from 'node:path'
import matter from 'gray-matter'
import { ImageResponse } from 'next/og'
export const dynamic = 'force-dynamic'
export const dynamicParams = false
type Props = {
params: Promise<{ slug: string }>
}
// 静的パスを生成する
export async function generateStaticParams() {
// content/blog ディレクトリの .md ファイル一覧を取得
const dir = path.join(process.cwd(), 'content/blog')
const files = await fs.readdir(dir)
return files
.filter((f) => f.endsWith('.md'))
.map((f) => ({ slug: f.replace(/\.md$/, '') }))
}
export async function GET(_: Request, { params }: Props) {
const { slug } = await params
const filePath = path.join(process.cwd(), 'content/blog', `${slug}.md`)
let fileContent: string
const FontData = {
IBMPlexSansJP: await fs.readFile(
path.join(
process.cwd(),
'public',
'assets',
'fonts',
'IBM_Plex_Sans_JP',
'IBMPlexSansJP-Light.ttf'
)
),
}
try {
fileContent = await fs.readFile(filePath, 'utf-8')
} catch {
return new Response('Not Found', { status: 404 })
}
try {
const backGroundImage = await fs.readFile(
path.join(process.cwd(), 'public', 'assets', 'img', 'ogp-base.png')
)
// Base64エンコード
const base64 = Buffer.from(backGroundImage).toString('base64')
const bgSrc = `data:image/png;base64,${base64}`
const { data } = matter(fileContent)
return new ImageResponse(
<div
style={{
display: 'flex',
width: '100%',
height: '100%',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
src={bgSrc}
alt="title"
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
position: 'absolute',
top: 0,
left: 0,
zIndex: -1,
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
width: '1000px',
height: '525px',
}}
>
<div
style={{
fontSize: 80,
flex: 3,
}}
>
{data.emoji || '📝'}
</div>
<div
style={{
fontSize: 60,
fontWeight: 'bold',
padding: '0 20px',
textShadow: '2px 2px 4px rgba(0, 0, 0, 0.5)',
flex: 4,
}}
>
{data.title}
</div>
<div
style={{
flex: 2,
fontSize: 32,
}}
>
{data.created}
</div>
</div>
</div>,
{
width: 1200,
height: 630,
// フォントの読み込み
fonts: [
{
name: 'IBMPlexSansJP',
data: FontData.IBMPlexSansJP,
style: 'normal',
},
],
emoji: 'twemoji',
}
)
} catch (e) {
console.error(e)
return new Response('Internal Server Error', { status: 500 })
}
}
これで、記事URL + /og.png
でOGP用の画像が表示されるようになります。
基本的には、ビルド時に全記事分つくるので、記事詳細と似てますね。
ただ、JSX を返す代わりにこちらは ImageResponse を返しています。
実際これって画像がかえってくるんですが、中身は普通に JSX ですよね。
これは、内部的に JSX を SVG 化して、それを更に PNG に変換する、みたいなことをしているらしいです。
なので、普通にページを作るのと同じ感覚でレイアウトをつくれるのは楽でいいですね。
ただし、画像としてかえってくるので当然 DevTools でのデバッグはできないです 💦
とはいえ、 Canvas でやろうとすると勝手が全然違って大変だった記憶がありますし、ソレに比べたら全然楽です。
Note
一応、 options.debug
を true
にして生成すると、要素の境界線が描画されるようになるので、多少やりやすくなるかもですが、ある程度別の場所でレイアウトを整えてしまったほうが楽なような気もします。
フォントの指定
画像生成にあたり、フォントを読み込んで指定してあげないと正しく表示ができないらしいです。
また、ここでのフォント指定には next/font/google
は使えないらしいので、
普通にWeb版を読み込むか、プロジェクト内に当該フォントを含めてしまうしかないようです。
絵文字の種類
絵文字データ( 🎉 とか 🐈 とか)は、いくつかのスタイルが用意されていて、
デフォルトだと X で使われてる Twemoji
になりますが、ほかにも noto color emoji
や blobmoji
、openmoji
が使えます
emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' = 'twemoji'
ここでは明示的に twemoji
を指定してますが、デフォルト値が twemoji
なので、 Twemoji を使いたい場合は省略しても大丈夫そうです。
Twemoji
Twemoji は、X (Twitter) で使われてる絵文字です。
Noto Color Emoji
Noto Color Emojiは、名前からもわかる通り、Google の Noto シリーズの1つで、 Android や Chrome OS で表示される絵文字もこれっぽいです。
Google Fonts にあるので、ウェブフォントとしてサイトにセットアップすればサイトの絵文字をこれにすることも可能みたいです。
Notoシリーズだけど共同開発してた Adobe 側の
源ノ
シリーズには流石にないっぽいかな?
Blobmoji
blobmoji は、かつて Google が Android 端末で使ってた絵文字です。
独特の形の愛嬌のある絵文字なんですが(私はこれをプリンと呼んでましたw)いつのまにか Noto Color Emoji に変わったみたいですね。
今は、Google のソフトウェアキーボードである、Gboard から選べるステッカーに、残ってるようです。
OpenMoji
OpenMoji はその名の通りのオープンソースな絵文字です。
独特の雰囲気のある絵文字ですが、個人的には結構好きです
メタデータの設定
最後に、ページをビルドする際のメタデータに og:image を含めるように generateMetadata
を調整します
OGP絡みは、
openGraph.title
タイトルopenGraph.description
概要openGraph.url
URLopenGraph.type
タイプ。投稿ならarticle
かなopenGraph.images[]
url
今作った画像のパス。記事のパーマリンク +/og.png
width
画像の幅height
画像の高さalt
画像の alt
images はオブジェクトの配列なので、複数サイズ登録できるっぽいのかな。
まぁとりあえず1200 x 630 1枚あれば最低限なんとかなる気はしますが。
Important
og:image はこの横長のがメジャーなサイズですが、最近流行りの埋め込みカードだと、中央で正方形にクロップされることが多いみたいなので、
絶対に見せたい要素は画像中央の 630 x 630 px
の範囲内に収めると良いらしいです。
テストする
ローカルでビルドしてから開いてみて、もんだなく画像が取れてそうだったら、実際にデプロイしてみてから、
Facebook のシェアデバッガー に URL を突っ込んでチェックしましょう。
参考
https://www.riku-mono.me/posts/nextjs-app-ogp
https://nextjs.org/docs/app/api-reference/functions/image-response