Nuxtだと、Nuxt SEO Kitのnuxt-og-imageを使えば、
vueコンポーネントとかHMLTをOG画像にできるけど、
unjs/nitroでもできないかなと思い、
いろいろ調べてみたときの備忘録(*´ω`*)
nuxt-og-imageの中をのぞいてみたら、
satoriとsharpを使ってたので、それを参考に実装してみた
satori+sharpを使った画像生成
この3つのライブラリを使うと、HTMLをPNGに変換できるっぽい
- vercel/satori ... ReactNode(VNode)をSVGに変換
- lovell/sharp ... SVGをPNGに変換
- natemoo-re/satori-html ... HTMLをVNodeに変換
satoriはVercel社が出しているライブラリで、JSX形式で利用するっぽい。
なので、HTMLといってもReactNodeを受け取る形になっている。
jsx形式なら、satoriとsharpでこんな感じに画像を生成できる。
// api.jsx import satori from 'satori' // satoriでReactNodeをSVGに変換 const svg = await satori( <div style={{ color: 'black' }}>hello, world</div>, { width: 600, height: 400, fonts: [] }, ); // sharpでSVGからPNGに変換 const png = await sharp(Buffer.from(svg)).png().toBuffer();
そのままだと使えないので、natemoo-re/satori-htmlを使う。
HTMLな文字列をVNodeに変換し、satriのインプットにできるように変換してくれるライブラリ。
import satori from "satori"; import { html } from "satori-html"; import sharp from "sharp"; // satori-htmlで文字列をVNodeに変換 const vnode = html(`<div style="color: black;">hello, world</div>`); // satoriでVNodeをSVGに変換 const svg = await satori( vnode, { width: 600, height: 400, fonts: [] }, ); // sharpでSVGからPNGに変換 const png = await sharp(Buffer.from(svg)).png().toBuffer();
これでHTML文字列からPNGを生成できちゃう。すごい(*´ω`*)
satoriもすべてのCSSを扱えるわけではないため、
サポート状況や制限事項などはドキュメントを参照する
satoriで画像を扱う
画像については、base64形式に変換して、埋め込めばOK。
URLであれば、src="https://...."
とかでもいける。
import { html } from "satori-html"; import fs from "fs"; // 画像ファイルの読み込み const imageBuffer = await fs.readFileSync("<image_path>"); // 画像ファイルをbase64形式にエンコード const imageBase64 = Buffer.from(imageBuffer).toString("base64"); const imageData = `data:image/png;base64,${imageBase64}`; const vnode = html(` <div> <img src="${imageData}" width="1200" height="630"/> </div> `);
satoriでfontを扱う
フォントを変更したい場合は、satoriのオプションに、
フォントファイルのデータを渡せばOK
import satori from "satori"; import { html } from "satori-html"; import fs from "fs"; // fontデータの読み取り const fontData = await fs.readFileSync("./NotoSansJP-Bold.ttf"); // satori-htmlで文字列をVNodeに変換 const vnode = html(`<div style="color: black;">hello, world</div>`); // satoriでVNodeをSVGに変換 const svg = await satori( vnode, { width: 1200, height: 630, // フォントの設定 fonts: [ { name: 'Noto Sans JP', data: fontData, style: 'normal', }, ], // 埋め込みフォントの有効化(デフォルトはfalse) embedFont: true, }, );
nitroで画像生成できるようにする
ディレクトリ構成はこんな感じ。
server/ assets/ NotoSansJP-Bold.ttf.b64 og_template.png.b64 routes/ index.ts nitro.config.ts package.json
実際にOGP画像を生成する
routes/index.ts
はこんな感じ。
// routes/index.ts import satori from "satori"; import { html } from "satori-html"; import sharp from "sharp"; export default defineEventHandler(async (event) => { const { text } = getQuery(event); // fontデータの読み取り const fontDataB64 = await useStorage().getItem<String>("assets/server/NotoSansJP-Bold.ttf.b64"); const fontData = Buffer.from(fontDataB64, "base64"); // 背景画像の読み込み const imageBase64 = await useStorage().getItem<String>("assets/server/og_template.png.b64"); // 画像ファイルをbase64形式にエンコード const imageData = `data:image/png;base64,${imageBase64}`; // satori-htmlで文字列をVNodeに変換 const vnode = html(` <div style="width: 1200px; height: 630px; position: relative; display: flex; justify-content: center; align-items: center;"> <img style="position: absolute; z-index: -1;" src="${imageData}"></img> <div style="font-size: 68px; text-align: center;">${ text || "Hello World"}</div> </div> `); // satoriでVNodeをSVGに変換 const svg = await satori( vnode, { width: 1200, height: 630, // フォントの設定 fonts: [ { name: 'Noto Sans JP', data: fontData, style: 'normal', }, ], // 埋め込みフォントの有効化(デフォルトはfalse) embedFont: true, }, ); // sharpでSVGからPNGに変換 const png = await sharp(Buffer.from(svg)).png().toBuffer(); // ヘッダーの設定 setHeader(event, "Content-Type", `image/png`); setHeader(event, "Cache-Control", "public, max-age=604800"); return ogImage; });
unstorage
の扱いがちょっとムズイ
基本は今までのを組み合わせた形で、
fsのかわりにunjs/unstorageをつかう。
unstorageをちゃんと理解できていない部分もあるけど、
const fontData = await useStorage().getItemRaw<ArrayBuffer>("assets/server/NotoSansJP-Bold.ttf");
のように、getItemRaw()
を使うと、
npm run dev
のときは問題ないが、npm run build
したあとに、うまく動かなかった。。
nitroはbuild時に.output/
にすべてを配置するようになっているため、
assetsなどのファイルも.output/
配下にまとめられるが、
output/server/chunks/raw/og_template.mjs
を見てみると、
// .output/server/chunks/raw/og_template.mjs // ROLLUP_NO_REPLACE const og_template = "�PNG\r\n\u001a\n\u0000\u0000\u0000\rIHD
という感じの文字列になっていて、うまくArrayBufferとして読み取ることができなかった。。
なので、あらかじめ、Base64形式にエンコードした.b64
ファイルを用意しておき、
それを利用するようにしている。
Base64化は、コマンドで実行した。
$ base64 -i og_template.png -o og_template.png.b64
getItems()
の指定がちょっとムズイ
また、getItems()
で指定するassets/server/
もすこしわかりずらく、
Issueがあがっているほど。
assets/
ディレクトリ配下のファイルにserver側の処理でアクセスする場合は、
getItems(assets/server/<filename>)
という感じになる。
このガイドのあたりに書いてあるとおり、
nitro.config.ts
にアセットディレクトリを追加できるが、
// nitro.config.ts export default defineNitroConfig({ serverAssets: [{ baseName: 'templates', dir: './templates' // Relative to `srcDir` (`server/` for nuxt) }] })
その場合は、こんな感じになるよう。
// routes/success.ts export default defineEventHandler(async (event) => { return await useStorage().getItem(`assets/templates/success.html`) // or return await useStorage('assets:templates').getItem(`success.html`) })
以上!! いろいろハマったけど、nitroでもOGPを生成できるように(*´ω`*)
こう見てみると、いろいろよしなにやってくれるnuxt-og-imageは偉大だ。。(*´ω`*)
参考にしたサイトさま
- vercel/satori: Enlightened library to convert HTML and CSS to SVG
- natemoo-re/satori-html: An HTML adapter for Vercel's Satori
- lovell/sharp: High performance Node.js image processing, the fastest module to resize JPEG, PNG, WebP, AVIF and TIFF images. Uses the libvips library.
- Satori + SvelteKit で OGP 画像を自動生成する
- Base64 エンコード・デコードを行う方法 - Node.js を用いた開発 - Node.js 入門
- Install Nuxt OG Image | Nuxt SEO
- Server Assets default path confuse · Issue #914 · unjs/nitro
- base64 コマンド | コマンドの使い方(Linux) | hydroculのメモ