0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Astroブログで記事ごとのOGP画像を自動生成する方法

0
Posted at

📝 この記事は forge.workstyle.tech に掲載した記事の転載です。

この記事では、Astroブログで記事ごとのOGP画像を作り、公開のたびに自動生成する仕組みを組める。記事画像が無い場合の共通画像と、ビルド後の検査も加える。

前提は、Astroでブログを運用していること、記事のslugを取得できること、記事を公開するスクリプトがあること。例では、記事Markdownのfrontmatterにstatus、slug、title、createdがあり、生成画像をサイトのpublic/og/へ同期する。

私は記事画像を生成するスクリプトを作っていた。けれど、公開の流れにはつないでいなかった。2026-10-01にXへ投稿した記事のカードを見て、初めてその抜けに気づいた。

手順1:記事ごとの画像を生成する

まず、OGP画像を作るスクリプトを用意する。例では記事タイトルと日付をHTMLテンプレートへ渡し、Chromeのヘッドレスモードで1200×630のPNGにする。デザインは紺の背景にタイトルを載せる。

次は、指定したogp-build.mjsからの抜粋だ。Vault内の記事を読み、公開済みの記事だけを生成対象にする。既定では、すでに画像がある記事を飛ばす。

const args = process.argv.slice(2);
const all = args.includes("--all");
const only = args.filter((a) => !a.startsWith("--"));

for (const f of readdirSync(BLOG)) {
  if (!f.endsWith(".md") || f.startsWith("_")) continue;

  const t = readFileSync(join(BLOG, f), "utf8");
  const meta = fm(t);
  if (meta.status !== "published") continue;

  const slug = meta.slug || f.replace(/\.md$/, "");
  if (only.length && !only.includes(slug)) continue;
  if (!all && !only.length && existsSync(join(OUT, `${slug}.png`))) continue;

  targets.push({
    slug,
    title: meta.title || slug,
    date: (meta.published_at || meta.created || "").slice(0, 10),
  });
}

画像の生成には、記事タイトルと日付をクエリとしてテンプレートに渡す。ogp-template.htmlでは、タイトルの長さに応じて文字サイズを変え、日付も画像に載せている。テンプレートの寸法は1200×630だ。

このスクリプトでは、画像の元データを記事Markdownと同じVault側に置く。出力先はGenerated/Blog/og/<slug>.png。サイトのpublic/og/へ写す処理は、画像生成とは分けてある。

実行例は次のとおり。パスは自分のリポジトリに合わせて置き換える。

node /path/to/vault/_system/ogp-build.mjs

通常実行では未生成の画像だけが対象になる。全記事を作り直す場合は--all、指定記事だけならslugを渡す。

node /path/to/vault/_system/ogp-build.mjs --all
node /path/to/vault/_system/ogp-build.mjs my-article-slug

生成スクリプトは対象記事をfrontmatterから拾う。したがって記事の公開状態やslugの持ち方が異なる場合は、ここを自分の構成に合わせる。例のスクリプトではstatusがpublishedの記事だけが対象だ。

ここで詰まりやすい点: 画像生成スクリプトを手で一度動かしても、その後に増える記事は自動では対象にならない。私の環境では、画像生成自体はできていたが公開手順に入っていなかった。公開済み93本のうち画像があったのは73本で、9月下旬以降の20本は記事ごとの画像が無かった。

手順2:公開フローの最初に画像生成を入れる

画像が作れるようになったら、記事をサイトへ同期する前に生成スクリプトを実行する。指定のpublish.shでは、公開状態への更新のあと、サイト同期より前に次の処理を入れている。

echo "② OGP 画像を作る(未生成の記事だけ)"
node "$VAULT/_system/ogp-build.mjs" >/dev/null

echo "③ サイトへ同期"
cd "$SITE"
npm run --silent sync

順序が大切だ。記事を公開状態にしてから画像を生成し、その後にサイトへ同期する。これで公開対象の記事が生成処理から見える。同期によって、Vaultの画像がAstroサイトのpublic/og/へコピーされる。

自分の公開処理が記事の公開化、同期、コミットなどを別の順序で行う場合も、記事が公開対象になった後、Astroのビルドより前に画像生成と同期を置く。生成だけして同期を忘れると、ビルド時点でサイトのpublic/og/に画像がない。

ここで詰まりやすい点: 画像が無くてもAstroのページはビルドできる。私のブログでは、記事画像が見つからないとテンプレート付属のblog-placeholder-1.jpgへ進んでいた。そのため公開処理は失敗せず、カードに「Build the web you want」と出るまで抜けが見えなかった。

手順3:記事画像が無い場合の共通画像を決める

記事固有の画像がないとき、Astroの既定画像をそのまま表示すると、未生成に気づきにくい。サイト側で共通画像を用意し、フォールバック先を決めておく。

指定のBaseHead.astroでは、記事のslugからpublic/og/<slug>.pngを探す。ファイルがあれば記事画像を使い、無ければpublic/og/_site.pngを参照する部分の抜粋がこちらだ。

const m = Astro.url.pathname.match(/^\/blog\/([^/]+)\/?$/);
const ogSlug = m ? m[1] : null;
const ogFile = ogSlug
  ? join(process.cwd(), "public", "og", `${ogSlug}.png`)
  : null;

const siteOg = join(process.cwd(), "public", "og", "_site.png");
const ogImage = ogFile && existsSync(ogFile)
  ? new URL(withBase(`/og/${ogSlug}.png`), Astro.site ?? Astro.url)
  : existsSync(siteOg)
    ? new URL(withBase("/og/_site.png"), Astro.site ?? Astro.url)
    : new URL(image.src, Astro.url);

この例では、記事固有の画像が優先される。見つからない場合はサイト共通の画像、共通画像も無ければコンポーネントに渡された画像へ進む。Astroコンポーネントのog:imageとtwitter:imageの両方に、決めたogImageを設定する。

<meta property="og:image" content={ogImage} />
<meta name="twitter:image" content={ogImage} />

共通画像もpublic/og/_site.pngに配置する。私の環境では、記事画像と同じデザインで、サイトの記録を示すタイトルを入れた画像にした。

ここで詰まりやすい点: 共通画像をフォールバックにすると、記事固有画像が無いページもそれらしいカードになる。私の環境でも、Astro既定画像が出ていた20本はページとして公開され、生成漏れ自体はエラーにならなかった。共通画像を用意しただけで完了にせず、次のビルド後検査も加える。

手順4:ビルド後に共通画像へのフォールバックを探す

公開前の生成と同期を済ませても、ビルド結果に記事固有画像が設定されたか確認する。publish.shでは、ビルド後のdist/blog/*/index.htmlを検索する。

missing=$(grep -L '/og/[^"]*\.png' dist/blog/*/index.html 2>/dev/null; grep -l '/og/_site\.png' dist/blog/*/index.html 2>/dev/null)

if [ -n "$missing" ]; then
  node "$VAULT/_system/ogp-build.mjs" >/dev/null
  npm run --silent sync >/dev/null
  npx astro build >/dev/null

  still=$(grep -l '/og/_site\.png' dist/blog/*/index.html 2>/dev/null)
  [ -n "$still" ] && osascript -e \
    'display notification "記事のOGP画像が作れず、サイト共通の画像で公開しました" with title "ブログ"'
fi

このコードは指定ファイルからの抜粋だ。実ファイルではビルド後に記事ページを検索し、記事画像の指定がないページや共通画像を参照するページがあれば、画像生成、同期、再ビルドを行う。それでも共通画像へのフォールバックが残った場合はMacに通知する。

記事一覧ページや記事以外のページを検査対象に含めないよう、検索対象はdist/blog/*/index.htmlに絞っている。記事以外にも同じ画像検査が必要なら、対象パスを自分のサイト構成に合わせて追加する。

シェルの検索は、HTMLに出力されたURLの形に依存する。withBaseの設定やデプロイ先のサブパスによってURLが変わる場合は、検索文字列を生成結果に合わせる。検査が何を見ているか分かるように、実際のビルド後HTMLにあるog:imageを見てから条件を決める。

ここで詰まりやすい点: ビルド後のHTMLが共通画像を指していれば、画像生成または同期が間に合っていない可能性がある。私のブログでは、共通画像が選ばれたページを見つけたら生成と同期をやり直し、再ビルドするようにした。それでも残る場合は通知する。

手順5:公開フロー全体を確認する

設定をつないだら、記事を1本用意して、公開フローを通す。確認するのは、画像ファイルだけではない。公開状態の記事が生成対象になり、同期され、ビルド後のHTMLが記事固有の画像URLを指すところまで見る。

確認の順は次のとおり。

  1. 公開対象の記事にslugとタイトルがあることを確認する。
  2. 生成スクリプトを実行し、Generated/Blog/og/<slug>.pngができることを確認する。
  3. 同期後に、サイトのpublic/og/<slug>.pngが存在することを確認する。
  4. Astroをビルドし、dist/blog/<slug>/index.html内のog:imageとtwitter:imageが記事画像を指すことを確認する。
  5. 記事画像のファイルが無い状態では、public/og/_site.pngへのフォールバックが出ることを確認する。
  6. ビルド後検査が共通画像を見つけたとき、再生成と再ビルドを行い、それでも残れば通知することを確認する。

投稿済みのXカードを使って表示だけを確かめると、サイト側の修正が反映されるまで待つことがある。Xはカード情報をしばらくキャッシュし、手動で更新させる公式の手段としてCard Validatorを使える状態ではない。確認では、まずビルド後HTMLと画像URLを見る。私の環境ではデプロイ直後に画像URLが一瞬404を返し、数秒後に200になった。

画像URLが200になっても、すでに投稿済みのカードがすぐ変わるとは限らない。サイトのビルド結果と、X上のキャッシュ表示は別々に確認する。

まとめ

記事ごとのOGP画像を作るだけでは、新しい記事に自動で適用されるとは限らない。生成、同期、ビルドの順で公開フローにつなぐ。

今回の構成では、記事画像があればそれを使い、無ければサイト共通画像へ進む。ビルド後に共通画像を指す記事ページが残っていないか検査し、見つけた場合は作り直して再ビルドする。

確認するときは、生成されたPNG、サイト側へ同期されたファイル、ビルド後のog:imageを順に見る。手動で一度動いた処理が、新しく増える記事にも実行されているか。その点まで公開フローで確かめられるようにしておく。


元記事: https://forge.workstyle.tech/blog/ogp-fell-back-to-astro-default/?utm_source=qiita&utm_medium=crosspost&utm_campaign=ogp-fell-back-to-astro-default

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?