[{"data":1,"prerenderedAt":721},["ShallowReactive",2],{"content-/excel-data-separation-cloudflare-ssg-oom":3,"all-pages-for-dir":719,"og-image-/excel-data-separation-cloudflare-ssg-oom":720},{"id":4,"title":5,"body":6,"category":699,"description":700,"extension":701,"meta":702,"navigation":644,"ogImage":703,"path":704,"project_name":705,"published":706,"publishedAt":707,"seo":708,"stem":709,"tags":710,"todo":703,"unpublished":706,"updatedAt":703,"__hash__":718},"pages/2026-05/2026-05-10/excel-data-separation-cloudflare-ssg-oom.md","Cloudflare Pages SSG の OOM を Excel 教材データ分離で根本対応した話 — Codex 再帰レビューと SSR fetch 404 の突破",{"type":7,"value":8,"toc":686},"minimark",[9,14,18,26,29,33,41,44,57,60,64,71,74,81,104,114,128,131,135,142,172,175,179,182,236,242,246,249,256,262,489,496,499,506,510,517,524,531,534,603,607,610,624,627,630,633,662,665,682],[10,11,13],"h2",{"id":12},"_57-から連続で落ち続けた-ssg-ビルド","5/7 から連続で落ち続けた SSG ビルド",[15,16,17],"p",{},"eurekapu-nuxt4 の Cloudflare Pages デプロイが、5/7 以降ずっと exit 134 で死んでいた。Node のヒープ上限 2GB に到達して、SSG プロセスが力尽きる。3日連続で赤いバッジを見ていた。",[15,19,20,21,25],{},"最初は PR #18（cockpit モーダルズーム関連）のレビュー中に発覚した。PR の中身を読むと UI 改修だけで、ビルドが落ちる要素は一切ない。差分を main と比べて、ビルド失敗は main 側の既知 OOM だと切り分けた。",[22,23,24],"strong",{},"PR #18 は技術的に独立してグリーン","、ビルド失敗は別件。",[15,27,28],{},"「これ実質的にグリーンにする必要なくてもうスクワッシュマージしちゃいませんか」と判断して、PR #18 は素通りで取り込んだ。CI の赤が PR の問題でないなら、PR を待たせる理由がない。",[10,30,32],{"id":31},"codex-に調査指示書を書かせる","Codex に調査指示書を書かせる",[15,34,35,36,40],{},"OOM の真因を掴むため、Codex に「5/6 成功と 5/7 失敗の間のコミット差分を読んで原因仮説を出して」と依頼した。出力は ",[37,38,39],"code",{},"memo/2026-05-10/codex-build-oom-investigation.md"," に保存。",[15,42,43],{},"Codex が返した仮説はこうだった。",[45,46,47],"blockquote",{},[15,48,49,52,53,56],{},[37,50,51],{},"allMillerChapters.ts"," 等の TS ファイルから Excel 教材の全文を直接 import している。Vite/Nuxt がこの 490KB 級のデータをメモリに展開した状態で SSG を回すと、ページ数 × データサイズで爆発する。",[22,54,55],{},"目次・メタデータ（toc, slideCounts, manifest）と本文を分離し、本文は build 時に public/ へ JSON として吐き出して fetch で取りに行く構造に変える","べき。",[15,58,59],{},"提案は腑に落ちた。Excel 教材のページは10コースあり、各コースの全文を Vue の SFC が import で抱えていた。SSG はページごとに Vue コンポーネントを評価するから、メモリは積み上がる。",[10,61,63],{"id":62},"試行錯誤1-codex-cli-の-windows-サンドボックスエラーを根本解決した","試行錯誤1: Codex CLI の Windows サンドボックスエラーを根本解決した",[15,65,66,67,70],{},"計画書を書いて Codex レビューにかけようとしたら、",[22,68,69],{},"毎回 Codex CLI がサンドボックスエラーで落ちた","。",[15,72,73],{},"最初は「リトライで通るかも」と数回叩いたが、毎回同じところで止まる。一旦回避策を考えかけたが、自分が「毎回エラーするので、根本的な解決策を考えてほしい」と Claude Code に投げた。",[15,75,76,77,80],{},"原因を追ったら ",[37,78,79],{},"~/.codex/config.toml"," の設定に行き着いた。",[82,83,88],"pre",{"className":84,"code":85,"language":86,"meta":87,"style":87},"language-toml shiki shiki-themes vitesse-light vitesse-light","[windows]\nsandbox = \"elevated\"\n","toml","",[37,89,90,98],{"__ignoreMap":87},[91,92,95],"span",{"class":93,"line":94},"line",1,[91,96,97],{},"[windows]\n",[91,99,101],{"class":93,"line":100},2,[91,102,103],{},"sandbox = \"elevated\"\n",[15,105,106,109,110,113],{},[37,107,108],{},"elevated"," モードは Windows Sandbox/Hyper-V を使う特権分離方式で、ホストの仮想化機能が整っていないと起動できない。Claude Code が代わりに ",[37,111,112],{},"unelevated"," に書き換えた。",[82,115,117],{"className":84,"code":116,"language":86,"meta":87,"style":87},"[windows]\nsandbox = \"unelevated\"\n",[37,118,119,123],{"__ignoreMap":87},[91,120,121],{"class":93,"line":94},[91,122,97],{},[91,124,125],{"class":93,"line":100},[91,126,127],{},"sandbox = \"unelevated\"\n",[15,129,130],{},"バックアップを取得 → 設定変更 → テスト実行で、Codex CLI が一発で通った。回避策で済ませず、設定ファイルの根本に手を入れたから、以降のレビュー全部が走るようになった。",[10,132,134],{"id":133},"試行錯誤2-codex-再帰レビューで計画を磨く","試行錯誤2: Codex 再帰レビューで計画を磨く",[15,136,137,138,141],{},"計画書 ",[37,139,140],{},"memo/2026-05-10/excel-data-separation-plan.md"," を Codex に3回読ませた。",[143,144,145,152,166],"ul",{},[146,147,148,151],"li",{},[22,149,150],{},"1回目",": 致命的指摘3点。本文分離の単位、Loader の責務、lint ルールの粒度。全部反映。",[146,153,154,157,158,161,162,165],{},[22,155,156],{},"2回目",": 新たな1点。「MillerViewer のナビ設計が",[22,159,160],{},"全章 chapters を持っている前提","で書かれているため、章別 JSON に分割すると次章ナビが壊れる」。これは見落としていた。",[37,163,164],{},"chapters"," の代わりに manifest だけ持って、ナビは章 ID で順序を解決する設計に書き直した。",[146,167,168,171],{},[22,169,170],{},"3回目",": 致命的な点なし。計画確定。",[15,173,174],{},"「人間が判断する係、Codex が指摘する係」の構図がはまった。自分は方針判断と取捨選択だけして、計画書の整合性チェックは Codex に回した。",[10,176,178],{"id":177},"phase-17-の実装","Phase 1〜7 の実装",[15,180,181],{},"計画が固まったら、Claude Code に Phase 1〜7 を一気に実装させた。",[143,183,184,194,207,217,226],{},[146,185,186,189,190,193],{},[22,187,188],{},"Phase 1",": ",[37,191,192],{},"excelBasicFunctionsNarration.ts"," を生成スクリプトで JSON 化。巨大データを TS から分離した。",[146,195,196,189,199,202,203,206],{},[22,197,198],{},"Phase 3",[37,200,201],{},"top.vue"," の import を軽量 manifest 形式に変更。",[22,204,205],{},"490KB が 18KB に減った（3.7%）","。SSG 時のメモリ展開がここで一段沈む。",[146,208,209,212,213,216],{},[22,210,211],{},"Phase 4",": ESLint の ",[37,214,215],{},"no-restricted-imports"," で、MillerViewer から教材全文 import を機械的に禁止した。再発防止の網。",[146,218,219,189,222,225],{},[22,220,221],{},"Phase 5",[37,223,224],{},"MillerViewerLoader.vue"," を新規作成。既存 MillerViewer はそのまま温存して、Loader が JSON を fetch して props で渡すラッパー方式にした。リスク最小の差し込み。",[146,227,228,231,232,235],{},[22,229,230],{},"Phase 7",": 10コース全部を JSON 化。",[22,233,234],{},"616KB が public/ 配信に逃げた","。全10ページを Loader 経由に書き換え。",[15,237,238,239,241],{},"数字は手応えがあった。",[37,240,201],{}," の manifest が 490KB から 18KB に痩せたとき、Vite の HMR が体感で速くなった。",[10,243,245],{"id":244},"試行錯誤3-ssr-で-fetch-が-404-を返した","試行錯誤3: SSR で fetch が 404 を返した",[15,247,248],{},"dev server で動作確認したら、CSR では動くが SSR で 404 が返ってきた。",[15,250,251,252,255],{},"原因を Codex に投げたら即答。「",[22,253,254],{},"SSR からの fetch が server middleware に阻まれている","。Cloudflare Pages 環境では public/ の JSON も静的アセットなので、SSR 経由で取りに行くには server API ルートを噛ませて static asset binding 経由で読むほうが安全」。",[15,257,258,261],{},[37,259,260],{},"server/api/content/excel/[...].ts"," を新規追加した。",[82,263,267],{"className":264,"code":265,"language":266,"meta":87,"style":87},"language-typescript shiki shiki-themes vitesse-light vitesse-light","export default defineEventHandler(async (event) => {\n  const path = getRouterParam(event, '_')\n  const assets = event.context.cloudflare?.env?.ASSETS\n  const url = new URL(`/excel/${path}.json`, event.node.req.url)\n  const res = await assets.fetch(url.toString())\n  return res.json()\n})\n","typescript",[37,268,269,306,341,376,433,466,483],{"__ignoreMap":87},[91,270,271,275,278,282,286,290,293,297,300,303],{"class":93,"line":94},[91,272,274],{"class":273},"sHkkW","export",[91,276,277],{"class":273}," default",[91,279,281],{"class":280},"senZ8"," defineEventHandler",[91,283,285],{"class":284},"shFtX","(",[91,287,289],{"class":288},"stQ0i","async",[91,291,292],{"class":284}," (",[91,294,296],{"class":295},"s4oTP","event",[91,298,299],{"class":284},")",[91,301,302],{"class":284}," =>",[91,304,305],{"class":284}," {\n",[91,307,308,311,314,317,320,322,324,327,331,335,338],{"class":93,"line":100},[91,309,310],{"class":288},"  const ",[91,312,313],{"class":295},"path",[91,315,316],{"class":284}," =",[91,318,319],{"class":280}," getRouterParam",[91,321,285],{"class":284},[91,323,296],{"class":295},[91,325,326],{"class":284},",",[91,328,330],{"class":329},"sMJiu"," '",[91,332,334],{"class":333},"sdGka","_",[91,336,337],{"class":329},"'",[91,339,340],{"class":284},")\n",[91,342,344,346,349,351,354,357,360,362,365,368,371,373],{"class":93,"line":343},3,[91,345,310],{"class":288},[91,347,348],{"class":295},"assets",[91,350,316],{"class":284},[91,352,353],{"class":295}," event",[91,355,356],{"class":284},".",[91,358,359],{"class":295},"context",[91,361,356],{"class":284},[91,363,364],{"class":295},"cloudflare",[91,366,367],{"class":284},"?.",[91,369,370],{"class":295},"env",[91,372,367],{"class":284},[91,374,375],{"class":295},"ASSETS\n",[91,377,379,381,384,386,389,392,394,397,400,403,405,408,411,413,415,417,419,422,424,427,429,431],{"class":93,"line":378},4,[91,380,310],{"class":288},[91,382,383],{"class":295},"url",[91,385,316],{"class":284},[91,387,388],{"class":288}," new ",[91,390,391],{"class":280},"URL",[91,393,285],{"class":284},[91,395,396],{"class":329},"`",[91,398,399],{"class":333},"/excel/",[91,401,402],{"class":273},"${",[91,404,313],{"class":333},[91,406,407],{"class":273},"}",[91,409,410],{"class":333},".json",[91,412,396],{"class":329},[91,414,326],{"class":284},[91,416,353],{"class":295},[91,418,356],{"class":284},[91,420,421],{"class":295},"node",[91,423,356],{"class":284},[91,425,426],{"class":295},"req",[91,428,356],{"class":284},[91,430,383],{"class":295},[91,432,340],{"class":284},[91,434,436,438,441,443,446,449,451,454,456,458,460,463],{"class":93,"line":435},5,[91,437,310],{"class":288},[91,439,440],{"class":295},"res",[91,442,316],{"class":284},[91,444,445],{"class":273}," await",[91,447,448],{"class":295}," assets",[91,450,356],{"class":284},[91,452,453],{"class":280},"fetch",[91,455,285],{"class":284},[91,457,383],{"class":295},[91,459,356],{"class":284},[91,461,462],{"class":280},"toString",[91,464,465],{"class":284},"())\n",[91,467,469,472,475,477,480],{"class":93,"line":468},6,[91,470,471],{"class":273},"  return",[91,473,474],{"class":295}," res",[91,476,356],{"class":284},[91,478,479],{"class":280},"json",[91,481,482],{"class":284},"()\n",[91,484,486],{"class":93,"line":485},7,[91,487,488],{"class":284},"})\n",[15,490,491,492,495],{},"ところがこれを書いて dev server をリロードしても、API ルートが 404 のまま。",[22,493,494],{},"新規 server routes は HMR で取り込まれない","仕様で、dev server 自体を再起動しないと反映されないらしい。",[15,497,498],{},"ここは自分が手で踏んだ。「今止めて再起動しました」と Claude Code に伝えて、もう一度確認させた。",[15,500,501,502,505],{},"SSR HTML を grep したら、Excel 教材本文「ウィンドウ枠の固定」の文字列が 4 件含まれていた。動的 ",[37,503,504],{},"useHead"," も manifest meta から正しく生成されている。SSR が通った瞬間だった。",[10,507,509],{"id":508},"試行錯誤4-ci-failure-を-oom-だと誤診断した反省","試行錯誤4: CI failure を OOM だと誤診断した反省",[15,511,512,513,516],{},"push したら CI が失敗した。「まさかの OOM 再発」と思い込んで、応急処置で ",[37,514,515],{},"NODE_OPTIONS=--max-old-space-size=6144"," を CI に追加してしまった。当初 2GB のヒープ上限を 6144MB に引き上げる、力技の延命。",[15,518,519,520,523],{},"push 後に自分が「まだ根本対応してください」と指示して、ログをちゃんと読み直した。",[22,521,522],{},"OOM ではなかった","。実は既存の Stripe webhook unit テストが別件で落ちていて、Build フェーズ自体は成功していた。",[15,525,526,527,530],{},"「失敗バッジ = OOM 再発」と直感で結びつけて、ログを読まずに NODE_OPTIONS をいじった。push を取り消すほどではないが、診断の順序を飛ばした反省として記録しておく。引き継ぎドキュメントを ",[37,528,529],{},"memo/2026-05-10/session-handover.md"," に残した。",[10,532,533],{"id":533},"数字で押さえた成果",[535,536,537,553],"table",{},[538,539,540],"thead",{},[541,542,543,547,550],"tr",{},[544,545,546],"th",{},"指標",[544,548,549],{},"変更前",[544,551,552],{},"変更後",[554,555,556,570,581,592],"tbody",{},[541,557,558,564,567],{},[559,560,561,563],"td",{},[37,562,201],{}," import サイズ",[559,565,566],{},"490KB",[559,568,569],{},"18KB（3.7%）",[541,571,572,575,578],{},[559,573,574],{},"10コース教材データ",[559,576,577],{},"TS 直 import 616KB",[559,579,580],{},"public/ JSON 配信に分離",[541,582,583,586,589],{},[559,584,585],{},"Codex 再帰レビュー",[559,587,588],{},"—",[559,590,591],{},"3回で致命的指摘ゼロ",[541,593,594,597,600],{},[559,595,596],{},"ビルド OOM ヒープ上限",[559,598,599],{},"2GB（デフォルト）",[559,601,602],{},"6144MB（応急処置として残置）",[10,604,606],{"id":605},"人間が判断ai-が実行の構図","人間が判断、AI が実行の構図",[15,608,609],{},"このセッションで自分が判断した箇所は数えるほどしかない。",[143,611,612,615,618,621],{},[146,613,614],{},"PR #18 を独立判定して「スクワッシュで通す」と決めたこと",[146,616,617],{},"Codex サンドボックスエラーで「根本対応してくれ」と方針を切ったこと",[146,619,620],{},"SSR 404 で dev server を手で再起動したこと",[146,622,623],{},"CI failure で「OOM じゃなくテスト失敗だ」と切り分け直したこと",[15,625,626],{},"それ以外、つまり Codex の調査指示書、計画書の3回レビュー、Phase 1〜7 の実装、Loader 設計、server API ルート、引き継ぎドキュメントは全部 Codex と Claude Code が回した。",[15,628,629],{},"税理士・会計士業務に置き換えるなら、「巡回監査で違和感を拾う係」と「資料突合・整合性チェックを回す係」の分業に近い。違和感を拾う筋肉だけ落とさず鍛えれば、突合作業は AI に積める。",[10,631,632],{"id":632},"残タスク",[143,634,637,650,656],{"className":635},[636],"contains-task-list",[146,638,641,646,647,649],{"className":639},[640],"task-list-item",[642,643],"input",{"disabled":644,"type":645},true,"checkbox"," ",[37,648,515],{}," を外す（教材分離だけでビルドが通るはずなので、本来は 4096 か 2048 で再検証したい）",[146,651,653,655],{"className":652},[640],[642,654],{"disabled":644,"type":645}," Stripe webhook unit テストの修正（別 issue）",[146,657,659,661],{"className":658},[640],[642,660],{"disabled":644,"type":645}," MillerViewerLoader の SSR キャッシュ戦略（現状は毎リクエスト fetch）",[10,663,664],{"id":664},"参考メモ",[143,666,667,672,677],{},[146,668,669,671],{},[37,670,39],{}," — Codex 調査指示書",[146,673,674,676],{},[37,675,140],{}," — 実装計画書（3回レビュー反映済み）",[146,678,679,681],{},[37,680,529],{}," — 引き継ぎドキュメント",[683,684,685],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}",{"title":87,"searchDepth":100,"depth":100,"links":687},[688,689,690,691,692,693,694,695,696,697,698],{"id":12,"depth":100,"text":13},{"id":31,"depth":100,"text":32},{"id":62,"depth":100,"text":63},{"id":133,"depth":100,"text":134},{"id":177,"depth":100,"text":178},{"id":244,"depth":100,"text":245},{"id":508,"depth":100,"text":509},{"id":533,"depth":100,"text":533},{"id":605,"depth":100,"text":606},{"id":632,"depth":100,"text":632},{"id":664,"depth":100,"text":664},"dev","Cloudflare Pages の SSG ビルドが Node ヒープ 2GB を突き抜けて exit 134 で落ち続けた問題を、Excel 教材データの TS import → JSON fetch 分離で根本対応した。top.vue manifest は 490KB から 18KB に痩せ、10コース 616KB が public/ 配信に逃げた。Codex 再帰レビュー3回、Windows サンドボックス問題の根本対応、SSR fetch 404 のハマりまでを記録する。","md",{},null,"/excel-data-separation-cloudflare-ssg-oom","eurekapu-nuxt4",false,"2026-05-10T00:00:00.000Z",{"title":5,"description":700},"2026-05/2026-05-10/excel-data-separation-cloudflare-ssg-oom",[711,712,713,714,715,716,717],"Nuxt","Cloudflare Pages","SSG","OOM","Codex","SSR","Excel教材","SHwlnOzQy-j_zTMAjVHRQowJ8vovnQ3iWn-vhoR2HcA",[],"https://log.eurekapu.com/og/blog/excel-data-separation-cloudflare-ssg-oom.png?v=2026-05-10T00%3A00%3A00.000Z&title=Cloudflare%20Pages%20SSG%20%E3%81%AE%20OOM%20%E3%82%92%20Excel%20%E6%95%99%E6%9D%90%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E9%9B%A2%E3%81%A7%E6%A0%B9%E6%9C%AC%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%9F%E8%A9%B1%20%E2%80%94%20Codex%20%E5%86%8D%E5%B8%B0%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%81%A8%20SSR%20fetch%20404%20%E3%81%AE%E7%AA%81%E7%A0%B4&author=Kei%20Komatsu&sig=206a5239893b02d8",1782528834794]