[{"data":1,"prerenderedAt":279},["ShallowReactive",2],{"content-/blog-verify-html-walker":3,"all-pages-for-dir":277,"og-image-/blog-verify-html-walker":278},{"id":4,"title":5,"body":6,"category":259,"description":260,"extension":261,"meta":262,"navigation":263,"ogImage":264,"path":265,"project_name":266,"published":267,"publishedAt":268,"seo":269,"stem":270,"tags":271,"todo":264,"unpublished":267,"updatedAt":264,"__hash__":276},"pages/2026-05/2026-05-20/blog-verify-html-walker.md","blog-verifyを_payload.json廃止に対応してHTML走査ベースに切り替えた",{"type":7,"value":8,"toc":248},"minimark",[9,13,22,37,44,47,53,59,64,67,73,76,113,118,133,136,143,153,156,159,162,165,175,178,192,195,198,216,219],[10,11,12],"h2",{"id":12},"何が壊れたか",[14,15,16,17,21],"p",{},"CIで動かしている ",[18,19,20],"code",{},"verify-blog-payload.mjs"," が、ある日から急に「件数 0 件」を返すようになった。",[14,23,24,25,28,29,32,33,36],{},"このスクリプトは Cloudflare Pages にデプロイする直前のローカルビルド後に走らせていて、",[18,26,27],{},"dist/blog/_payload.json"," を fetch してブログインデックスに記事が一定件数以上載っているかを確認する役割を持っていた。CI/ローカルどちらでも ",[18,30,31],{},"BLOG_MIN_ARTICLES"," を下回ると ",[18,34,35],{},"exit 1"," で止まる、最後のセーフティネットになっていた。",[14,38,39,40,43],{},"それが急に止まらなくなった。正確には「件数 0 件で止まる」ようになった。",[18,41,42],{},"_payload.json"," 自体は HTTP 200 で返ってくるが、中の collection 配列が空になっている。",[10,45,46],{"id":46},"原因をたどる",[14,48,49,50,52],{},"ローカルでビルド後の ",[18,51,27],{}," を覗いて、ようやく状況が掴めた。",[14,54,55,56,58],{},"@nuxt/content 3.11 系にバージョンが上がってから、SSG 経路で collection の取得結果が ",[18,57,42],{}," にシリアライズされなくなっていた。HTML 側にはちゃんとインラインされて記事リストが描画されているのに、payload は空。同じ collection を参照しているのに、配信形式によって中身が違う、という挙動だった。",[14,60,61,63],{},[18,62,42],{}," を信頼する前提が成立しなくなったので、検証ロジックを根本から作り直す必要が出た。",[10,65,66],{"id":66},"どう作り直すかを決める",[14,68,69,70,72],{},"方針は1分で決めた。",[18,71,42],{}," から取れないなら、ユーザーに実際に届く dist/blog/index.html を直接読めばいい。HTML には記事リンクが描画されているのだから、そこから記事 URL を抜き出して数えれば、CDN 経由でユーザーが見る画面と同じ基準で検証できる。",[14,74,75],{},"具体的には:",[77,78,79,90,101,104],"ul",{},[80,81,82,83,85,86,89],"li",{},"検証元を ",[18,84,27],{}," から ",[18,87,88],{},"dist/blog/index.html"," に変更",[80,91,92,93,96,97,100],{},"frontmatter の ",[18,94,95],{},"path"," 形式に合わせて ",[18,98,99],{},"/YYYY-MM-DD..."," リンクを正規表現で抽出",[80,102,103],{},"同じ記事が複数箇所からリンクされても二重カウントしないようにユニーク化",[80,105,106,108,109,112],{},[18,107,31],{}," のデフォルトを ",[18,110,111],{},"100 → 20"," に引き下げ（HTML 上に実際に表示される件数の基準に合わせる）",[14,114,115,117],{},[18,116,42],{}," 経由のときは内部状態の全件を見ていたので 100 件で問題なかったが、HTML 走査だとページネーション後の表示件数を見ることになるので、現実的な閾値に下げた。",[14,119,120,121,124,125,128,129,132],{},"方針だけ口頭で出して、スクリプトの書き直しは Claude Code に任せた。出てきた差分は ",[18,122,123],{},"apps/web/scripts/verify-blog-payload.mjs"," の ",[18,126,127],{},"36 insertions / 70 deletions"," で、payload 用の JSON パース・型チェックがごっそり消えて、HTML の ",[18,130,131],{},"match()"," ベースの軽い実装に置き換わった。コードが短くなる修正は気持ちがいい。",[10,134,135],{"id":135},"デバッグ残骸の発見",[14,137,138,139,142],{},"修正そのものとは別に、リポジトリ直下で ",[18,140,141],{},"apps/web/blog-index-sample.html"," というファイルが置きっぱなしになっているのに画面上で気づいた。記憶にないファイル名だった。",[14,144,145,146,148,149,152],{},"何で作ったのか覚えていなかったので Claude Code に経緯を辿らせたところ、HTML 走査方針を決めた前日（2026-05-19）に「",[18,147,88],{}," の構造を一度ローカルで眺めるためにコピーして置いた」サンプルだと判明した。",[18,150,151],{},"memo/2026-05-19/blog-index-sample.html"," にも同じものが残っていた。",[14,154,155],{},"要は、HTML パースのロジックを書くために手元で正規表現を試した残骸で、スクリプトが完成した時点で消し忘れていただけ。Claude Code に「デバッグ残骸なら削除しといて、ついでにコミットしておいて」と頼んで、サンプル HTML の削除まで含めて 1 つの fix コミットに収めてもらった。",[14,157,158],{},"リポジトリ直下に意味不明な HTML が転がっているのは精神衛生に悪い。次にこのディレクトリを開いた他人（未来の自分を含む）が「これ何？消していいの？」で 5 分溶かす未来を、ここで潰しておけた。",[10,160,161],{"id":161},"コミットを2つに分けた理由",[14,163,164],{},"このセッションの作業中、ステージ済みのまま放置されていた content 系の変更（記事の追加・更新と、それに連動するリダイレクト再生成）が手元に残っていた。",[14,166,167,170,171,174],{},[18,168,169],{},"fix(blog-verify)"," のコミットにこの content 変更を同居させると、後から ",[18,172,173],{},"git log"," を追う人間が「なぜブログ記事の追加コミットに verify スクリプトの全面書き換えが混ざっているのか」を理解できなくなる。バグった時に切り戻したい単位がそもそも違う。",[14,176,177],{},"なので順序を分けて、",[179,180,181,186],"ol",{},[80,182,183,185],{},[18,184,169],{},": payload 廃止対応の HTML 走査切替 + 残骸 HTML の削除",[80,187,188,191],{},[18,189,190],{},"content(2026-05-19)",": 日記・読書記録など5本追加 + リダイレクト再生成",[14,193,194],{},"の 2 コミットに割ってコミットした。fix と content は混ぜない、というのを今後も守りたい。",[10,196,197],{"id":197},"学び",[77,199,200,206,213],{},[80,201,202,203,205],{},"外部から取得するデータ構造の前提（",[18,204,42],{}," の中身）はライブラリのマイナーバージョン更新で平気で変わる。SSG ビルド後に「ユーザーが実際に受け取る HTML」を直接見に行く検証の方が、結局壊れにくい",[80,207,208,209,212],{},"デバッグ用に置いた一時ファイルは、その日のうちに消すか、消せないなら ",[18,210,211],{},"memo/{日付}/"," に隔離する。リポジトリ直下に置きっぱなしになったら最後、忘れる",[80,214,215],{},"ステージ済みの変更が複数機能にまたがっていたら、コミットを分ける。あとで切り戻せる単位を意識する",[10,217,218],{"id":218},"関連ファイル",[77,220,221,226,232,237,243],{},[80,222,223,225],{},[18,224,123],{}," — _payload.json から HTML 走査ベースに書き直したスクリプト",[80,227,228,229,231],{},"削除: ",[18,230,141],{}," — デバッグ用に置いたまま忘れていたサンプル HTML",[80,233,228,234,236],{},[18,235,151],{}," — 同じ用途で memo 配下にも残っていた残骸",[80,238,239,240],{},"コミット: ",[18,241,242],{},"70b5a1de fix(blog-verify): _payload.json 廃止に対応して HTML 走査ベースの検証に切替",[80,244,239,245],{},[18,246,247],{},"beb31a80 content(2026-05-19): 日記・読書記録など5本追加 + リダイレクト再生成",{"title":249,"searchDepth":250,"depth":250,"links":251},"",2,[252,253,254,255,256,257,258],{"id":12,"depth":250,"text":12},{"id":46,"depth":250,"text":46},{"id":66,"depth":250,"text":66},{"id":135,"depth":250,"text":135},{"id":161,"depth":250,"text":161},{"id":197,"depth":250,"text":197},{"id":218,"depth":250,"text":218},"dev","Nuxt Content 3.11で _payload.json から collection 結果が消えた。検証スクリプトを dist/blog/index.html の直接走査に作り直し、デバッグ残骸の blog-index-sample.html も削除して2コミットに分けてコミットした記録。","md",{},true,null,"/blog-verify-html-walker","blog-platform",false,"2026-05-20T00:00:00.000Z",{"title":5,"description":260},"2026-05/2026-05-20/blog-verify-html-walker",[272,273,274,275],"Nuxt Content","blog-verify","リファクタリング","Cloudflare Pages","9pcTRajkT814VRJ0O9nFfmuYdeup-habzxCJJdBFigc",[],"https://log.eurekapu.com/og/blog/blog-verify-html-walker.png?v=2026-05-20T00%3A00%3A00.000Z&title=blog-verify%E3%82%92_payload.json%E5%BB%83%E6%AD%A2%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%A6HTML%E8%B5%B0%E6%9F%BB%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AB%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88%E3%81%9F&author=Kei%20Komatsu&sig=38b1c06f47784436",1782528839718]