開発mdx-playground

起きたこと

https://log.eurekapu.com/blog/ を開くと、記事一覧が一瞬だけ映って空っぽになった。記事リンクが見える時間は 100ms あるかないかで、目を凝らさないと気づかない。

「前にも同じことがあった」と直感した。2026-06-04 に踏んだ payload null 化と同じ症状だ。あのときは useBlogArticles.tsqueryCollection().all() の戻り値をそのまま返していて、Cloudflare Pages にデプロイされた _payload.json の該当 slot が null に化けていた。プロジェクトの .claude/rules/nuxt-content-payload-null.md に対策と検出方法を書き残してあるはずだ。

直感どおりだった。今回も _payload.jsonnull を吐いていた。

切り分け

まず SSR と CSR を別々に観察した。Claude Code に curl で HTML を引いてもらい、a.article-link の出現数と current-month のラベルを数える。

  • SSR HTML: 記事リンク 208 件、当月ラベル「2026年6月」。正しい。
  • _payload.json: blog-public-articlesog-blog-index の両方の slot が null。完全に化けている。

SSR は当月分の記事を吐けているのに、CSR で hydration するときに参照する payload は空っぽだった。だから一瞬だけ正しい DOM が見えてから、Vue が CSR で「データが空だから」と表示を消しにいく。2026-06-04 と教科書どおり同じ流れだ。

ここで useBlogArticles.ts を読み直してもらった。対策の plain POJO 化は確かに入っていた。.map で必要フィールドだけ詰め替える D 案がそのまま残っている。コードレベルでは対策済みのはずなのに、本番だけ null 化している。意味がわからない。

ローカル再現を試みる

「今のコードでも pnpm generate し直したら同じ payload null が出るのでは」と疑った。CLAUDE.md に「pnpm generate は10分かかるから検証目的で勝手に走らせるな」と書いてあるが、今回はまさに本番検証なので走らせる判断をした。

pnpm generate をバックグラウンドで投げ、待ち時間に本番の他のページを観察する。10分後、dist/blog/_payload.json を確認した。

  • ローカル build の _payload.json: 757KB、blog-public-articles 配列に中身あり
  • 本番 (Cloudflare Pages) の _payload.json: 116 byte、null

ローカルでは再現しない。今のコードは正しく POJO 配列を書き出している。本番にデプロイされているビルド成果物だけが壊れているという結論に落ち着いた。

対処:再デプロイ

ローカルの dist/ は既に正常な payload を持っている。これをそのまま Cloudflare に投げ直せば直るはずだ。wrangler pages deploy dist で押し込んでもらった。

デプロイ後、もう一度本番の _payload.json を fetch して中身を確認したら、配列が返ってきた。ブラウザで /blog/ を開き直すと、一覧が消えなくなった。直った。

原因は特定できなかった

Claude Code に「原因は何だった?」と聞いても、特定できないという返事だった。手元にある状況証拠はこれだけ:

  • 今朝の measure-deploy.ps1 経由のビルド+デプロイは exit 0 で正常終了している
  • そのとき push された dist/blog/_payload.json だけが null に化けていた
  • 同じソースで今もう一度 pnpm generate し直した結果は正常な配列を吐く
  • ローカルでは何度試しても再現しない

仮説としては「朝のビルド時に何らかのタイミング問題で _payload.json だけがシリアライズに失敗した」あたりだが、ログが残っていないので確証は得られない。measure-deploy.ps1 のビルドログを残す運用にしておけば、次は追えるかもしれない。

反省:対策ルールが「効いている」と過信していた

.claude/rules/nuxt-content-payload-null.md に「plain POJO 化すれば直る」と書き残してあったので、もう同じ問題は踏まないと思い込んでいた。今回踏んだのはコードレベルの問題ではなくビルドアーティファクトの問題で、ルールの対策範囲を超えていた。

ルールに欠けていた観点はこのあたりだと思う。

  • デプロイ後の検出スニペットは書いてあるが、毎回デプロイ後に流す運用にしていなかった。今朝の measure-deploy.ps1 直後に流していれば、その場で気づけた
  • ローカル dist/_payload.json と本番 _payload.json の差分チェックがルールに入っていなかった。コード対策が入っていてもビルド成果物がズレることがある、という観点が抜けていた
  • 「対策を入れたから安心」と思考停止していた。POJO 化は必要条件であって十分条件ではなかった

残タスク

  • measure-deploy.ps1 にデプロイ完了後の payload 健全性チェックを組み込む(_payload.json のサイズが閾値以下なら警告)
  • .claude/rules/nuxt-content-payload-null.md に「ローカル dist と本番 dist を fetch して比べる」観点を追記
  • ビルドログを memo/{日付}/build-log.txt に残す運用を試す

ここまで書き残しておけば、次に同じことが起きたとき自分が早く気づける。