[{"data":1,"prerenderedAt":470},["ShallowReactive",2],{"content-/app-static-build-heroku-retirement":3,"all-pages-for-dir":448,"related-/app-static-build-heroku-retirement":449,"og-image-/app-static-build-heroku-retirement":469},{"id":4,"title":5,"body":6,"category":429,"concepts":430,"description":431,"extension":432,"meta":433,"navigation":434,"ogImage":430,"path":435,"project_name":436,"published":437,"publishedAt":438,"seo":439,"stem":440,"tags":441,"todo":430,"unpublished":437,"updatedAt":430,"__hash__":447},"pages/2026-07/2026-07-27/app-static-build-heroku-retirement.md","Herokuを止められるか、nuxt generate を4回回して確かめた — 旧Nuxt 2アプリ静的化の実測",{"type":7,"value":8,"toc":418},"minimark",[9,13,17,20,23,26,29,32,35,38,41,44,47,50,53,85,88,91,95,101,104,107,110,155,158,165,169,172,191,194,197,200,204,219,222,225,228,279,282,362,366,369,376,379,386,389,393,396,399,402,405,408,411,414],[10,11,12],"p",{},"計画書に「静的化してHerokuを止める」と1行だけ書いてあった。読み返していて手が止まった。",[14,15,16],"h2",{"id":16},"前日の続きから",[10,18,19],{},"昨日の棚卸しで論点を5つ残したまま終わっていたので、まず仕掛かりを洗い出させた。出てきたのは2件。片方はほぼ終わっていたので、もう片方のサイト整理へ進み、計画書をChromeの前面タブに開かせた。",[10,21,22],{},"論点は判断の前提を読みながら答えたい。ターミナルの選択肢では前提が見えないので、計画書のHTMLに3択のボタンを埋め込む形にしてもらった。",[10,24,25],{},"読み進めていて、課題の一覧から学習アプリが抜けているのに気づいた。Herokuで動いている旧世代のアプリで、昨日の調査では別枠扱いになっていた。指摘して、課題に加えさせた。ついでにアフター像を図にしてほしいと頼んだら「もう描いてあります」と返ってきて、タブの奥に埋もれていた2枚が出てきた。ビフォーとアフターを並べたSVGで、カードの高さが揃っていない箇所だけ直させた。",[10,27,28],{},"決裁フォームの方は少しもたついた。サーバーは動いてHTTP 200を返しているのに、こちら側のバックグラウンド追跡が2回続けて切れた。その状態だと、回答を送ってもこちらが気づけない。3回目は立て直しをやめて、回答ファイルが作られた瞬間を検知する監視に切り替えさせた。11時09分に5問すべての回答が届いた。",[10,30,31],{},"学習アプリについて選んだのは「静的化してHerokuを止め、移譲は順次進める」だった。",[14,33,34],{"id":34},"一行の記述に引っかかった",[10,36,37],{},"回答をもとにフェーズ2の実行計画を作らせた。そのステップ2が「静的化してHerokuを止める」である。",[10,39,40],{},"読んでいて、順序がおかしい気がした。この計画では、Vue 2で書かれた画面をVue 3へ移す作業が先にあるように読める。だが静的化とは生成済みのHTMLとJSを配信することだ。だとすれば、ビルドさえ通れば書き換えを待つ理由はない。Herokuを解約して、生成物をCloudflareに置いて、DNSを向け直す。今日でも終わる話ではないのか。",[10,42,43],{},"そう聞いた。合っているなら、月々の支払いが1つ今日消える。",[10,45,46],{},"ただし解約は戻せない。前のセッションの調査結果を鵜呑みにさせず、リポジトリを自分で読んで裏を取るところから始めさせた。",[14,48,49],{"id":49},"サーバー側は33行しかなかった",[10,51,52],{},"コードを読ませた結果は、拍子抜けするほど薄かった。",[54,55,56,65,75,78],"ul",{},[57,58,59,60,64],"li",{},"Expressのエントリは33行。Nuxtのレンダラを ",[61,62,63],"code",{},"app.use"," するだけで、独自ルートもDB接続も認証処理もない",[57,66,67,70,71,74],{},[61,68,69],{},"process.env"," の参照は1箇所（",[61,72,73],{},"NODE_ENV","）のみ。他はすべてコメントアウト済み",[57,76,77],{},"ページは799ファイル、動的ルートは1本だけ",[57,79,80,81,84],{},"記事サイトからの最多参照先である仕訳問題のページ（68箇所）は ",[61,82,83],{},"asyncData"," を持たない。データをJSONのimportでバンドルし、クエリはクライアント側で読んでいる",[10,86,87],{},"手当てが要るものは3つに絞れた。外部APIを叩く動的ルート1本、ランタイムプロキシを使う1ページ、HTTPS強制のミドルウェア。どれも記事サイトからの導線には乗っていない。",[10,89,90],{},"計画書には「関門はNode 18でのビルド」と書いてあった。旧世代のwebpackはNode 18より新しい環境でOpenSSL絡みのエラーを吐く。手元にはNode 22しかない。",[14,92,94],{"id":93},"関門はopensslではなくメモリだった","関門はOpenSSLではなくメモリだった",[10,96,97,100],{},[61,98,99],{},"--openssl-legacy-provider"," を付けて走らせた。無害で可逆なので、確認を挟まずそのまま実行させた。",[10,102,103],{},"8分後、EXIT=134で落ちた。",[10,105,106],{},"ログを見ると、webpackのコンパイルはClient / Serverとも通っていた。OpenSSLは何も起こしていない。落ちたのは799ページのレンダリング段階で、ヒープ4GBを使い切っていた。計画書に書いた関門の想定が、そもそも外れていたことになる。",[10,108,109],{},"搭載32GBのうち12GBを割り当てて2回目。",[111,112,117],"pre",{"className":113,"code":114,"language":115,"meta":116,"style":116},"language-bash shiki shiki-themes vitesse-light vitesse-light","NODE_OPTIONS=\"--openssl-legacy-provider --max_old_space_size=12288\" \\\n  ./node_modules/.bin/nuxt generate\n","bash","",[61,118,119,146],{"__ignoreMap":116},[120,121,124,128,132,136,140,142],"span",{"class":122,"line":123},"line",1,[120,125,127],{"class":126},"s4oTP","NODE_OPTIONS",[120,129,131],{"class":130},"shFtX","=",[120,133,135],{"class":134},"sMJiu","\"",[120,137,139],{"class":138},"sdGka","--openssl-legacy-provider --max_old_space_size=12288",[120,141,135],{"class":134},[120,143,145],{"class":144},"senZ8"," \\\n",[120,147,149,152],{"class":122,"line":148},2,[120,150,151],{"class":138},"  ./node_modules/.bin/nuxt",[120,153,154],{"class":138}," generate\n",[10,156,157],{},"メモリ不足は再発しなかった。通知には「exit code 0で完了」と出た。",[10,159,160,161,164],{},"その報告は取り消された。0はラッパーコマンド全体の値で、",[61,162,163],{},"nuxt generate"," 自体はEXIT=1で終わっていたという。794ルートは生成できていて、失敗が6件あった。誤報が自己申告で訂正されたのはよかったが、通知の数字を信じてはいけないという事実だけが残った。",[14,166,168],{"id":167},"失敗6件は静的化のせいではなかった","失敗6件は、静的化のせいではなかった",[10,170,171],{},"内訳を見て、少し笑ってしまった。",[10,173,174,175,178,179,182,183,186,187,190],{},"1件は想定どおりの動的ルートだった。残り5件はすべて、存在しないページを指しているサイト内リンクである。加えて、生成を止めていた致命的なエラーの正体は ",[61,176,177],{},"\u003CNuxtLink>"," の ",[61,180,181],{},"to"," に残った ",[61,184,185],{},"http://localhost:3000/..."," の1行だった。クローラーがそれを辿り、Windowsのパスとして無効な ",[61,188,189],{},":"," を含むディレクトリを作ろうとして落ちていた。",[10,192,193],{},"どれも静的化で生まれた問題ではない。SSRなら404を返して終わるので誰も気づかなかっただけで、本番には開発用のURLへ飛ぶリンクが出ていた。静的生成がそれをエラーとして表に出した。",[10,195,196],{},"「一旦リンクを外していいので、3つまとめて完走させて」と指示した。",[10,198,199],{},"返ってきたのは、指示と少し違う結果だった。",[14,201,203],{"id":202},"消す前に行き先が実在していた","消す前に、行き先が実在していた",[10,205,206,207,210,211,214,215,218],{},"調べたところ、リンク切れ5パターンのうち4つは正しいページが実在していたという。",[61,208,209],{},"06_"," というファイル名のプレフィックスが落ちている、",[61,212,213],{},"cockpit"," が ",[61,216,217],{},"ccockpit"," になっている、単数形と複数形が食い違っている。要するにタイポである。",[10,220,221],{},"消せば本来つながるはずの導線まで失う。だから実在が確認できた7箇所は正しいパスに直し、行き先が自明でない1パターン6箇所だけリンクを外した。テキストは残してある。指示と違う判断をしたという申告が先にあったので、そのまま通した。",[10,223,224],{},"外した1パターンは、統合ページが3つに分割された跡のように見える。どこへ向けるべきか、いまの私にも決められない。",[10,226,227],{},"外部APIに依存する動的ルートは、生成の対象から外した。",[111,229,233],{"className":230,"code":231,"language":232,"meta":116,"style":116},"language-js shiki shiki-themes vitesse-light vitesse-light","generate: { exclude: [/^\\/questions$/] },\n","js",[61,234,235],{"__ignoreMap":116},[120,236,237,240,242,245,248,250,253,256,260,264,268,271,273,276],{"class":122,"line":123},[120,238,239],{"class":144},"generate",[120,241,189],{"class":130},[120,243,244],{"class":130}," {",[120,246,247],{"class":144}," exclude",[120,249,189],{"class":130},[120,251,252],{"class":130}," [",[120,254,255],{"class":134},"/",[120,257,259],{"class":258},"sHkkW","^",[120,261,263],{"class":262},"svWSF","\\/",[120,265,267],{"class":266},"sLkVR","questions",[120,269,270],{"class":258},"$",[120,272,255],{"class":134},[120,274,275],{"class":130},"]",[120,277,278],{"class":130}," },\n",[10,280,281],{},"3回目のビルドはEXIT=0で完走した。失敗は1件だけ残り、それも同じプレフィックス欠落だった。2回目は途中で止まっていたので、その行まで到達していなかっただけである。同じ要領で直して4回目を回した。",[283,284,285,304],"table",{},[286,287,288],"thead",{},[289,290,291,295,298,301],"tr",{},[292,293,294],"th",{},"回",[292,296,297],{},"終了コード",[292,299,300],{},"生成ルート",[292,302,303],{},"失敗ルート",[305,306,307,321,335,348],"tbody",{},[289,308,309,313,316,319],{},[310,311,312],"td",{},"1回目",[310,314,315],{},"134（メモリ不足）",[310,317,318],{},"—",[310,320,318],{},[289,322,323,326,329,332],{},[310,324,325],{},"2回目",[310,327,328],{},"1（致命エラー）",[310,330,331],{},"794",[310,333,334],{},"6",[289,336,337,340,343,345],{},[310,338,339],{},"3回目",[310,341,342],{},"0",[310,344,331],{},[310,346,347],{},"1",[289,349,350,353,355,357],{},[310,351,352],{},"4回目",[310,354,342],{},[310,356,331],{},[310,358,359],{},[360,361,342],"strong",{},[14,363,365],{"id":364},"_402箇所を照合したら25件の欠けは集計バグだった","402箇所を照合したら、25件の欠けは集計バグだった",[10,367,368],{},"生成できたことと、リンクが切れないことは別の話だ。記事サイトのソース全体から学習アプリへの参照を機械的に抽出させ、生成物と1件ずつ突き合わせさせた。",[10,370,371,372,375],{},"最初の集計では25件が「生成物に無い」と出た。抽出の正規表現がMarkdownのリンク記法の ",[61,373,374],{},"]("," を拾って、実在しないパスを作っていたのが原因だった。区切り文字を足して再集計すると、ユニーク39URL・延べ402箇所、欠けは0件になった。",[10,377,378],{},"数字が悪い方に振れたときは、まず自分の集計を疑う。今回はそれで正解だった。",[10,380,381,382,385],{},"生成物をローカルで配信して実物も見た。最多参照先の仕訳問題を ",[61,383,384],{},"?chapter=1&section=1"," 付きで開くと、18問が読み込まれ、問題文も借方貸方の選択肢もボタンも描画された。コンソールのエラーと警告は0件だった。ログイン画面も認証フォームまで出た。コードから「クエリはクライアント読み」と判断した内容がそのまま実挙動と一致している。",[10,387,388],{},"Cloudflare Pagesの制限も測らせた。総ファイル数4,610に対して上限は20,000、25MBを超える単一ファイルは0件だった。総容量750MBのうち375MBがレッスン、179MBが画像である。画像は重いが、制限には当たらない。",[14,390,392],{"id":391},"それでも今日は止めない","それでも、今日は止めない",[10,394,395],{},"技術的にできることは実測で確定した。ここで解約に走りたくなるが、まだ静的化した生成物をデプロイしていない。",[10,397,398],{},"止める順番はこうなる。まず静的版をCloudflareに載せてDNSを向ける。そのうえでHeroku側へのアクセスが消えたことを確認する。誰も来ていないと分かってから停止する。この確認を飛ばすと、まだ生きている導線を自分で切ることになる。",[10,400,401],{},"検証記録と実行計画をドキュメントに残して、学習ゲートをスキップしてコミットした。停止判断は明日の積み残しとして登録してある。",[14,403,404],{"id":404},"残ったもの",[10,406,407],{},"前提を1つ壊した。計画書に書いた「関門はNodeのバージョン」は外れで、実際の関門はメモリだった。手元のマシンで12GBを渡せば通るものを、旧バージョンのNodeを用意するところから始めようとしていた。",[10,409,410],{},"そしてビルドを4回回して見えたのは、静的化の可否ではなく、SSRが隠していた不備の量だった。本番に出ていたlocalhostへのリンクが1本、タイポで404に落ちるリンクが5パターン。SSRは404を返して黙るが、静的生成はビルドを止めてまで教えてくる。うるさいが、正しい。",[10,412,413],{},"止めるかどうかは明日決める。決め手はビルドの成否ではなく、アクセスログだ。",[415,416,417],"style",{},"html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}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}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}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 .svWSF, html code.shiki .svWSF{--shiki-default:#BDA437;--shiki-dark:#BDA437}html pre.shiki code .sLkVR, html code.shiki .sLkVR{--shiki-default:#AB5E3F;--shiki-dark:#AB5E3F}",{"title":116,"searchDepth":148,"depth":148,"links":419},[420,421,422,423,424,425,426,427,428],{"id":16,"depth":148,"text":16},{"id":34,"depth":148,"text":34},{"id":49,"depth":148,"text":49},{"id":93,"depth":148,"text":94},{"id":167,"depth":148,"text":168},{"id":202,"depth":148,"text":203},{"id":364,"depth":148,"text":365},{"id":391,"depth":148,"text":392},{"id":404,"depth":148,"text":404},"dev",null,"Herokuで動かしている旧Nuxt 2の学習アプリを静的化できるか、コードを読ませて4回ビルドを回した記録。関門はOpenSSLではなくメモリだった。失敗6ルートの正体、参照402箇所の全件照合、Cloudflare Pagesの制限確認まで。","md",{},true,"/app-static-build-heroku-retirement","eurekapu",false,"2026-07-27T00:00:00.000Z",{"title":5,"description":431},"2026-07/2026-07-27/app-static-build-heroku-retirement",[442,443,444,445,446],"静的サイト","Nuxt","Heroku","Cloudflare Pages","ビルド検証","eDQjmTAcZ7ASxQOWBAEgPZ2s1uA5gl85AgQ8xzDnh6c",[],[450,454,457,461,465],{"title":451,"path":452,"publishedAt":453},"Herokuを止める前に導線を移す — app.eurekapu.com 静的化の本番切替とUdemyリンク62本の移設","/app-cutover-udemy-links-finish","2026-07-28T00:00:00.000Z",{"title":455,"path":456,"publishedAt":438},"移行サイトのSEO・AI-SEO改善を1日で回した — 画像altを480件、記事の要点を203本、OG画像を署名付きに","/wordpress-nuxt-ai-seo-phase-c",{"title":458,"path":459,"publishedAt":460},"本番切替の前に移行先サイトを仕上げた日 — 画像の謎、フォームの文字化け、トップページの作り直し","/wordpress-site-final-polish","2026-07-26T00:00:00.000Z",{"title":462,"path":463,"publishedAt":464},"「汎用的にはAstro」は何を指しているのか","/astro-vs-nuxt-general-purpose","2026-07-23T00:00:00.000Z",{"title":466,"path":467,"publishedAt":468},"英語の図解Artifactを日本語化して単独Vueページで公開するまで - AI導入5ステップの移植記録","/ai-adoption-steps-page","2026-07-17T00:00:00.000Z","https://log.eurekapu.com/og/blog/app-static-build-heroku-retirement.png?v=2026-07-27T00%3A00%3A00.000Z&title=Heroku%E3%82%92%E6%AD%A2%E3%82%81%E3%82%89%E3%82%8C%E3%82%8B%E3%81%8B%E3%80%81nuxt%20generate%20%E3%82%924%E5%9B%9E%E5%9B%9E%E3%81%97%E3%81%A6%E7%A2%BA%E3%81%8B%E3%82%81%E3%81%9F%20%E2%80%94%20%E6%97%A7Nuxt%202%E3%82%A2%E3%83%97%E3%83%AA%E9%9D%99%E7%9A%84%E5%8C%96%E3%81%AE%E5%AE%9F%E6%B8%AC&author=Kei%20Komatsu&sig=7a93b547702421fc",1785379974595]