【JS】OGPの画像生成をCloudinaryからSatoriにする
こんにちは!SaaS離れの年頃のMizutani(@sirycity)です。なんか自分で作りたくなってきた。
というわけで今回はまさに今までSaaSのCloudinaryに頼っていたOGPの画像をSatoriを使って自力での実装に変更した話です。
結論
ビルドが重いのとレイアウトにちょっと制限がある以外は大満足!
はじめに
ブログがSNSでシェアされる時、画像もいっしょに表示されると嬉しいですよね?その設定をOGPって言います。OGPは文章だったり作者名だったり色々ありますが、今回はそのうち画像についてを扱います。以後OGPはOGPの画像のことを指します。
OGPについて
技術的な部分は割愛しますが要するにこんな形式で画像のURLを示すだけです。こんだけなら簡単。
<meta property="og:image" content="" />
動的OGPについて
でも実際のところOGPを場合によって変えたい時とかありますよね。ブログタイトルとか。そんな時に毎回画像作ってたら大変です。なのでパラメータを元に画像を自動で作る機能が欲しいわけです。これが動的OGP。
Cloudinaryについて
Cloudinaryは画像処理のSaaSです。まあGoogleフォトの豪華版みたいなもんです。S3の豪華版って言った方がいいかも。
んで、このCloudinaryを使うとURLパラメータに基づいてOGPを自動生成できるんです。まあ便利。設定は割愛します。というか設定したの3年前くらいで忘れた。ごめん…
Satoriについて
このパラメータを元に画像を自動で作る機能を叶えるjsのライブラリがSatoriです。しかもレイアウトをjsxで書けちゃう。すごいね。なおSatoriは厳密にはパラメータをsvgに変換するライブラリなので今回はそれをさらにpngに変換します。OGPはsvg使えんからね。
実際にやってみる
というわけで実際にやってみましょう。今回は最小構成をAstroでやってみます。なお、この記事を参考にしました。
パッケージ入れる
本体とsvg→png変換用。
npm i satori @resvg/resvg-js
コンポーネント作る
jsxファイル。超簡単に。最小構成だからな。
export const OgImage = ({ text }) => <div>{text}</div>
ロジック部分を作る
jsファイル。上の記事ほぼコピペ。
import { Resvg } from '@resvg/resvg-js'
import satori from 'satori'
// import { OgImage } from コンポーネント置いた場所
export const getOgImage = async text => {
const fontData = await getFontData()
const svg = await satori(OgImage({ text }), {
width: 1200,
height: 630,
fonts: [{ name: 'Noto Serif JP', data: fontData, style: 'normal' }],
})
const resvg = new Resvg(svg, {
font: { loadSystemFonts: false },
fitTo: { mode: 'width', value: 1200 },
})
const image = resvg.render()
const png = image.asPng()
return png
}
const getFontData = async () => {
const API = `https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@700`
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) return
return await fetch(resource[1]).then(res => res.arrayBuffer())
}
適当に記事一覧つくる
tsファイル。適当にこんなかんじに。
export const posts = [
{ slug: '2023-01-01', title: 'あけおめ' },
{ slug: '2023-12-25', title: 'メリークリスマス' },
]
動的ページの読み込み部分作る
Astroの場合はこう。 src > pages > 動的ページ みたいなかんじ。
touch src/pages/\[slug\].png.ts
中身はこう。
// import { posts } from 記事一覧置いた場所
// import { getOgImage } from ロジック置いた場所
export const getStaticPaths = async () =>
posts.map(({ title, slug }) => ({ params: { slug }, props: { title } }))
export const GET = async ({ props: { title } }) => {
const ogImage = await getOgImage(title)
return new Response(ogImage, { headers: { 'Content-Type': 'image/png' } })
}
以上。実際にメタタグでOGP指定する所はまあええな。
Satoriの問題点
CSSに制限がかかる
例えばdisplayがflexしか使えんとか文字の中央寄せができんとか。この2つが特に気になった。細かい所だとグラデーションできんとか。まあ言い出したらきりがない。
フォントもだいぶ制約される
フォントデータはどっかのCDNから持ってこなきゃいかん。Webみたいに汎用フォントへのフォールバックができないです。
重い
重い。仕方ないけどね。ブログで使うとビルド時間が記事に比例して伸びていく。
さいごに
課題はそこそこあれど中途半端に使っていたSaaSから卒業できたのはとても嬉しい。こうやって成長していくんやな(自意識過剰)以上。