og画像を生成できるようにした
毎回og画像を設定するのが面倒だったので特別にog画像の設定をしていない記事には、zennやはてなブログのようにタイトルのog画像を生成するようにした
astroのogp生成では @vercel/og 使っていたり、pupetterなどで生成してる記事があったが 今回は@vercel/ogの内部でも使われているsatoriを直接使うことにする
ざっくりやること
Reactで画面を作る容量でog画像っぽいUIを作り、satoriと@resvg/resvg-jsを使用して作ったogっぽいUIをsvgに変換 その後og画像はsvg対応していないのでsharpを使ってそのsvgをpngに変換する
インストール
必要なものをインストールする。このブログのパッケージ管理はpnpmを使っているのでpnpmで書く
pnpm add -D satori sharp @resvg/resvg-js
# reactで作るのでreactも入れる
pnpm astro add react
作っていく
コンポーネントを作る。とりあえずはタイトルとブログタイトルが出るところを目指す。なので完成形はこんな感じ。
ogimageを出すためのコンポーネント
// OgImage.tsx
import satori from 'satori'
import sharp from 'sharp'
import { SITE_TITLE } from '../consts'
const fontFamily = 'Noto Sans JP'
const convertFontFamily = fontFamily.replaceAll(' ', '+')
export async function getOgImage(text: string) {
const fontData = await getFontData(SITE_TITLE, 400)
const boldFontData = await getFontData(text, 700)
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
backgroundColor: '#fff',
padding: '24px',
overflow: 'hidden',
}}
>
<div
style={{
display: 'flex',
flexGrow: '1',
fontWeight: 700,
fontSize: '48px',
width: '100%',
overflow: 'hidden',
}}
>
{text}
</div>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
fontSize: '24px',
}}
>
blog.yuzu441.com
</div>
</div>,
{
width: 800,
height: 400,
fonts: [
{
name: fontFamily,
data: fontData,
weight: 400,
style: 'normal',
},
{
name: fontFamily,
data: boldFontData,
weight: 700,
style: 'normal',
},
],
},
)
return await sharp(Buffer.from(svg)).png().toBuffer()
}
async function getFontData(text: string, wght: number) {
const API = `https://fonts.googleapis.com/css2?family=${convertFontFamily}:wght@${wght}&text=${encodeURIComponent(
text,
)}`
const css = await (
await fetch(API, {
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',
},
})
).text()
const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
if (!resource) {
throw new Error('Failed to fetch font')
}
const res = await fetch(resource[1])
return res.arrayBuffer()
}
ogを返すEndpointを作る。画像が別で設定されている記事は生成404を返すことで生成しないようにしている。
// src/pages/og/[slug].png.ts
import type { APIContext, GetStaticPathsResult } from 'astro'
import { getCollection } from 'astro:content'
import { getOgImage } from '../../components/OgImage'
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
const posts = await getCollection('posts')
return posts.map((entry) => ({
params: { slug: entry.slug },
props: { title: entry.data.title, isGenerate: entry.data.image?.url === undefined },
}))
}
export async function GET({ props: { title, isGenerate } }: APIContext) {
if (!isGenerate) {
return new Response(undefined, {
status: 404,
})
}
const body = await getOgImage(title)
return new Response(body, {
headers: {
'content-type': 'image/png',
},
})
}
これでastro build
するとが画像が生成される
これ作っててAstroのエンドポイントに対してgetStaticPaths
効くの知った
tips
ogのUIを作っていく時に毎回ビルドしたり、ブラウザで開いて作るの面倒だなと思っていたらvercelが Vercel OG Image Playground というプレビュー見ながら作れるのでこれで作ってコンポーネントファイルにコピペして作ると効率が良さそう
作っている時に記事のタイトルなのでそんなに長くなることは無いだろうけどtext-overflow: elipsis
で長かったら省略して欲しかったんだけど、うまく動かなくて断念したのでできた人はXか何かで教えてほしい