[Astro] カバーSVGからOGP画像(PNG)を生成して og:image に使う

Tech
[Astro] カバーSVGからOGP画像(PNG)を生成して og:image に使う

記事サムネイルを自作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