[Astro] カバーSVGからOGP画像(PNG)を生成して og:image に使う
Tech記事サムネイルを自作SVGで量産したのですが、SVG は OGP画像(og:image)には使えない(X や Facebook がレンダリングしない)という問題があります。
そこで、カバーSVG をビルド用にPNG化して og:image に使う仕組みを入れました。デザインはそのまま流用できます。
方針:SVGをラスタライズして使い回す
- サムネがカバーSVGの記事 → PNG に変換して
og:imageに - サムネが**写真(JPG)**の記事 → そのまま
og:imageに(既にラスタなのでOK)
カバーは共通化していて種類が少ない(このブログでは16枚)ので、その16枚をPNG化するだけで済みます。
ラスタライザは resvg-js(フォントを明示できる)
カバーSVGは文字主体(英字+一部日本語)。ラスタライズはフォントの有無で結果が変わるので、フォントを明示的に扱える @resvg/resvg-js を使います。
npm i -D @resvg/resvg-js
生成スクリプト。public/images/blog/covers/*.svg を読み、public/images/blog/og/*.png に書き出します。
// scripts/generate-og.mjs
import { Resvg } from '@resvg/resvg-js';
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
const SRC = 'public/images/blog/covers';
const OUT = 'public/images/blog/og';
mkdirSync(OUT, { recursive: true });
for (const file of readdirSync(SRC).filter((f) => f.endsWith('.svg'))) {
const svg = readFileSync(join(SRC, file), 'utf-8');
const resvg = new Resvg(svg, {
fitTo: { mode: 'width', value: 1200 }, // viewBox 1200x630 の比率で描画
font: { loadSystemFonts: true }, // ローカルのフォントを使う
});
const png = resvg.render().asPng();
writeFileSync(join(OUT, file.replace(/\.svg$/, '.png')), png);
}
package.json にスクリプトを登録して npm run og で回せるようにしておきます。
{ "scripts": { "og": "node scripts/generate-og.mjs" } }
ハマりどころ:ビルド時に変換しない(フォント問題)
最初は「ビルドのたびに変換」を考えましたが、ホスティングのビルド環境(Linux)には日本語フォントが無いことが多く、その状態でラスタライズすると日本語が豆腐(□)になります。
そこで、フォントの揃ったローカルで生成 → PNGをコミットする方式にしました。
- 確実(環境のフォント事情に左右されない)
- カバーを追加・変更したときだけ
npm run ogで再生成すればよい
ビルドパイプラインに組み込みたい場合は、CJKフォント(Noto Sans JP など)をリポジトリに同梱して resvg に明示ロードさせる必要があります。今回は枚数も少ないので、ローカル生成+コミットが手軽でした。
og:image を配線する
記事レイアウトで、サムネのパスから OGP 画像を決めます。カバーSVGなら og 用PNGに差し替え、写真ならそのまま。og:image は絶対URLにします。
---
// src/layouts/BlogPostLayout.astro
import { SITE } from '@/data/site';
const { thumbnail } = Astro.props.post.data;
const ogPath = thumbnail
?.replace('/images/blog/covers/', '/images/blog/og/')
.replace(/\.svg$/, '.png');
const ogImage = ogPath ? new URL(ogPath, SITE.url).href : undefined;
---
<BaseLayout ogImage={ogImage}>...</BaseLayout>
共通レイアウト側でメタタグを出力。画像があるときは Twitter のカードを大きい表示にします。
{ogImage && <meta property="og:image" content={ogImage} />}
{ogImage && <meta name="twitter:image" content={ogImage} />}
<meta name="twitter:card" content={ogImage ? 'summary_large_image' : 'summary'} />
確認
npm run og # OG画像を生成
npm run build
grep og:image dist/blog/<記事>/index.html
og:image が絶対URLのPNGになっていればOK。仕上がりは各SNSのカードバリデータ(X の Card Validator や Facebook の Sharing Debugger)で確認できます。
まとめ
- SVGは
og:imageに使えない → PNGにラスタライズして使う - フォント問題があるので、ローカル生成+コミットが確実(
npm run og) - カバーSVG→PNG、写真→そのまま、で
og:imageを配線。絶対URLにするのを忘れずに
サムネのデザインをそのまま OGP に流用できるので、記事ごとに OGP を用意する手間がほぼゼロになりました。
Astro, SVG, SEO