この記事の概要
Astro と satori を組み合わせて動的な OGP 画像を生成しようとしていました。
用意しておいた画像と組み合わせて OGP を生成しようとしたら苦戦したので、起きたことや解決策を備忘録として記事にします。
前提
環境
主要なものだけを記載します。
依存関係 | バージョン |
---|---|
astro | 4.9.2 |
@astrojs/react | 3.4.0 |
react | 18.3.1 |
satori | 0.10.13 |
sharp | 0.33.4 |
やろうとしていたこと
内容自体は簡単で、次の通りです。
- 背景に画像を敷く
- S3 などにアップされている画像ではなく、リポジトリ内に存在する画像
- background-image か、img 要素かなどは問わない
- その上に記事タイトルを配置する
- ビルド時に書き出して配信する
参考にしていた記事
1 番最初はこの記事を参考にして、文字を描画する段階までは進めました。
ただ、ローカル画像を使おうとしたら上手くいかなくなってしまいました。
解決策
最終的には次のようにして解消しました。
- OGP に使う元となる画像を
public
ディレクトリに保存する - 画像を
base64
形式で読み込む -
satori
内のimg
のsrc
にbase64
形式で読み込んだ画像を指定する
コードの全体像はこのようになっています。
(この記事に関係ある部分だけを抜き出しています)
import { readFileSync } from "node:fs";
import satori from "satori";
import sharp from "sharp";
async function getFontData(text: string) {
// フォントデータ取得のための処理
}
const imgBase64 = readFileSync(
new URL("../../public/og-image-background.png", import.meta.url),
{ encoding: "base64" },
);
const imgDataUrl = `data:image/png;base64,${imgBase64}`;
export async function getOgImage(text: string) {
const fontData = (await getFontData(text)) as ArrayBuffer;
const svg = await satori(
<div style={{ ... }}>
<img style={{ ... }} src={imgDataUrl} />
<div style={{ ... }}>{text}</div>
</div>,
{
width: 1200,
height: 630,
fonts: [{ ... }],
},
);
return await sharp(Buffer.from(svg)).png().toBuffer();
}
ダメだったこと
通常のimport
でimg
のsrc
を指定する
最初はこのように書いてみました。
import imgSrc from "../assets/og-image-background.png";
...
export async function getOgImage(text: string) {
const fontData = (await getFontData(text)) as ArrayBuffer;
const svg = await satori(
<div style={{ ... }}>
<img style={{ ... }} src={imgSrc} />
<div style={{ ... }}>{text}</div>
</div>,
// 中略
);
return await sharp(Buffer.from(svg)).png().toBuffer();
}
すると、次のようなエラーメッセージが出ました。
Unsupported image type: unknown
.astro
以外のファイルで画像を指定するときは、書き方を変える必要がありました。1
import imgSrc from "../assets/og-image-background.png";
...
export async function getOgImage(text: string) {
const fontData = (await getFontData(text)) as ArrayBuffer;
const svg = await satori(
<div style={{ ... }}>
- <img style={{ ... }} src={imgSrc} />
+ <img style={{ ... }} src={imgSrc.src} />
<div style={{ ... }}>{text}</div>
</div>,
// 中略
);
return await sharp(Buffer.from(svg)).png().toBuffer();
}
しかしこれでもエラーが出ます。
Image source must be an absolute URL: ~~~
相対パス、絶対パスの両方を試しましたがダメでした。
楽に解決しようと外部にアップした画像の URL を指定することなのですが、色々と自分のやりたいこととの兼ね合いで、ローカル画像でも上手くいく方法を模索します。
base64
で画像を読み込む
satori のドキュメントに「生成されたSVGをPNGなどの他の画像フォーマットにレンダリングしたい場合は、props.srcとして直接base64エンコードされた画像データ(またはバッファ)を使用した方が、Satoriでの余分なI/Oが不要になります。」といった記載があるのを見つけました。2
余分とかでなくそもそも生成できないのですが、base64 でのアプローチに価値はありそうなので試してみました。
- import imgSrc from "../assets/og-image-background.png";
...
+ const imgBase64 = readFileSync(
+ new URL("../assets/og-image-background.png", import.meta.url),
+ { encoding: "base64" },
+ );
+ const imgDataUrl = `data:image/png;base64,${imgBase64}`;
export async function getOgImage(text: string) {
const fontData = (await getFontData(text)) as ArrayBuffer;
const svg = await satori(
<div style={{ ... }}>
- <img style={{ ... }} src={imgSrc.src} />
+ <img style={{ ... }} src={imgDataUrl} />
<div style={{ ... }}>{text}</div>
</div>,
// 中略
);
return await sharp(Buffer.from(svg)).png().toBuffer();
}
これにより、開発環境での問題はクリアしました。
記事ごとにちゃんとテキストの違う画像が生成できました。
ただし、ビルドするとエラーが出ました。
ENOENT: no such file or directory, open '/PATH/TO/REPOSITORY/dist/assets/og-image-background.png'
dist/
はビルド生成物が格納されるディレクトリです。
そして、Astro はデフォルトでは画像ファイルをdist/_astro
に格納します。3
そのためdist/assets
などというディレクトリは存在せず、エラーになってしまいました。
ここでastro.config.mjs
の設定を変えてみました。
この設定によりdist/_astro
はdist/assets
という名前になります。34
export default defineConfig({
...
+ build: {
+ assets: "assets",
+ },
});
ところが、これでもファイルは見つかりません。
画像が書き出される際、ファイル名にハッシュ値がついてog-image-background.ハッシュ値.png
という名前になり、見つけられなかったのです。
public/
に画像を置く
実現したいのは、ビルド時にディレクトリ名やファイル名が変わらない状態です。
となるとsrc/
以下ではなくpublic/
におけば良いのでは?と思い試してみました。
...
const imgBase64 = readFileSync(
- new URL("../assets/og-image-background.png", import.meta.url),
+ new URL("../../public/og-image-background.png", import.meta.url),
{ encoding: "base64" },
);
const imgDataUrl = `data:image/png;base64,${imgBase64}`;
export async function getOgImage(text: string) {
const fontData = (await getFontData(text)) as ArrayBuffer;
const svg = await satori(
<div style={{ ... }}>
<img style={{ ... }} src={imgDataUrl} />
<div style={{ ... }}>{text}</div>
</div>,
// 中略
);
return await sharp(Buffer.from(svg)).png().toBuffer();
}
こちらで解消し、冒頭に載せたコードと同じになりました。
まとめ
実現したいことの割に悩みましたが、無事解消して良かったです。
それなりにやりたい人が多そうな内容だと思っているのですが、X でも Stack Overflow でも個人ブログでも、実例を全然見つけられませんでした。
ややニッチですが、どなたかのお役に立てれば幸いです。
-
https://github.com/vercel/satori?tab=readme-ov-file#images ↩
-
https://docs.astro.build/en/reference/configuration-reference/#buildassets ↩ ↩2
-
自動で生成されるディレクトリの名前を不用意に(しかも、何かしらと衝突しそうな
assets
なんて名前に)変えるなんてやめた方が良いとは思いますが、一旦試さずにはいられませんでした。 ↩