開発blog-platform

何が壊れたか

CIで動かしている verify-blog-payload.mjs が、ある日から急に「件数 0 件」を返すようになった。

このスクリプトは Cloudflare Pages にデプロイする直前のローカルビルド後に走らせていて、dist/blog/_payload.json を fetch してブログインデックスに記事が一定件数以上載っているかを確認する役割を持っていた。CI/ローカルどちらでも BLOG_MIN_ARTICLES を下回ると exit 1 で止まる、最後のセーフティネットになっていた。

それが急に止まらなくなった。正確には「件数 0 件で止まる」ようになった。_payload.json 自体は HTTP 200 で返ってくるが、中の collection 配列が空になっている。

原因をたどる

ローカルでビルド後の dist/blog/_payload.json を覗いて、ようやく状況が掴めた。

@nuxt/content 3.11 系にバージョンが上がってから、SSG 経路で collection の取得結果が _payload.json にシリアライズされなくなっていた。HTML 側にはちゃんとインラインされて記事リストが描画されているのに、payload は空。同じ collection を参照しているのに、配信形式によって中身が違う、という挙動だった。

_payload.json を信頼する前提が成立しなくなったので、検証ロジックを根本から作り直す必要が出た。

どう作り直すかを決める

方針は1分で決めた。_payload.json から取れないなら、ユーザーに実際に届く dist/blog/index.html を直接読めばいい。HTML には記事リンクが描画されているのだから、そこから記事 URL を抜き出して数えれば、CDN 経由でユーザーが見る画面と同じ基準で検証できる。

具体的には:

  • 検証元を dist/blog/_payload.json から dist/blog/index.html に変更
  • frontmatter の path 形式に合わせて /YYYY-MM-DD... リンクを正規表現で抽出
  • 同じ記事が複数箇所からリンクされても二重カウントしないようにユニーク化
  • BLOG_MIN_ARTICLES のデフォルトを 100 → 20 に引き下げ(HTML 上に実際に表示される件数の基準に合わせる)

_payload.json 経由のときは内部状態の全件を見ていたので 100 件で問題なかったが、HTML 走査だとページネーション後の表示件数を見ることになるので、現実的な閾値に下げた。

方針だけ口頭で出して、スクリプトの書き直しは Claude Code に任せた。出てきた差分は apps/web/scripts/verify-blog-payload.mjs36 insertions / 70 deletions で、payload 用の JSON パース・型チェックがごっそり消えて、HTML の match() ベースの軽い実装に置き換わった。コードが短くなる修正は気持ちがいい。

デバッグ残骸の発見

修正そのものとは別に、リポジトリ直下で apps/web/blog-index-sample.html というファイルが置きっぱなしになっているのに画面上で気づいた。記憶にないファイル名だった。

何で作ったのか覚えていなかったので Claude Code に経緯を辿らせたところ、HTML 走査方針を決めた前日(2026-05-19)に「dist/blog/index.html の構造を一度ローカルで眺めるためにコピーして置いた」サンプルだと判明した。memo/2026-05-19/blog-index-sample.html にも同じものが残っていた。

要は、HTML パースのロジックを書くために手元で正規表現を試した残骸で、スクリプトが完成した時点で消し忘れていただけ。Claude Code に「デバッグ残骸なら削除しといて、ついでにコミットしておいて」と頼んで、サンプル HTML の削除まで含めて 1 つの fix コミットに収めてもらった。

リポジトリ直下に意味不明な HTML が転がっているのは精神衛生に悪い。次にこのディレクトリを開いた他人(未来の自分を含む)が「これ何?消していいの?」で 5 分溶かす未来を、ここで潰しておけた。

コミットを2つに分けた理由

このセッションの作業中、ステージ済みのまま放置されていた content 系の変更(記事の追加・更新と、それに連動するリダイレクト再生成)が手元に残っていた。

fix(blog-verify) のコミットにこの content 変更を同居させると、後から git log を追う人間が「なぜブログ記事の追加コミットに verify スクリプトの全面書き換えが混ざっているのか」を理解できなくなる。バグった時に切り戻したい単位がそもそも違う。

なので順序を分けて、

  1. fix(blog-verify): payload 廃止対応の HTML 走査切替 + 残骸 HTML の削除
  2. content(2026-05-19): 日記・読書記録など5本追加 + リダイレクト再生成

の 2 コミットに割ってコミットした。fix と content は混ぜない、というのを今後も守りたい。

学び

  • 外部から取得するデータ構造の前提(_payload.json の中身)はライブラリのマイナーバージョン更新で平気で変わる。SSG ビルド後に「ユーザーが実際に受け取る HTML」を直接見に行く検証の方が、結局壊れにくい
  • デバッグ用に置いた一時ファイルは、その日のうちに消すか、消せないなら memo/{日付}/ に隔離する。リポジトリ直下に置きっぱなしになったら最後、忘れる
  • ステージ済みの変更が複数機能にまたがっていたら、コミットを分ける。あとで切り戻せる単位を意識する

関連ファイル

  • apps/web/scripts/verify-blog-payload.mjs — _payload.json から HTML 走査ベースに書き直したスクリプト
  • 削除: apps/web/blog-index-sample.html — デバッグ用に置いたまま忘れていたサンプル HTML
  • 削除: memo/2026-05-19/blog-index-sample.html — 同じ用途で memo 配下にも残っていた残骸
  • コミット: 70b5a1de fix(blog-verify): _payload.json 廃止に対応して HTML 走査ベースの検証に切替
  • コミット: beb31a80 content(2026-05-19): 日記・読書記録など5本追加 + リダイレクト再生成