公開成功なのに欠けるページとファイルが分かる、bolt.new の Publish で Astro 製静的サイトが一部 404 になる現象の記録。
原因はアップロードでもホスティングの設定でもなく、Publishが内部で走らせるビルドが約47秒の時点で強制終了されていたことだった。この記事は、そこに至るまでの切り分けと、ビルドが途中で切れても全ページが配信されるようにする対策の記録である。
症状: 消えるのはビルドの後半
Publishボタンを押すとビルドが走り、エラー表示なしにURLが更新される。ところが配信結果はこう分かれていた。
配信されている (200)
/・/blog/**・/compare/**・/_astro/ のハッシュ付きCSS・画像類
欠けている (404)
/tools/ (24ページ)・/workflow/・/privacy/・/search/・/youtube/ ハブ・/rss.xml・/sitemap-index.xml・/og/*.png・/pagefind/ (検索インデックス)
特定パスの設定ミスに見えないのが特徴で、消えているのはビルドの後半で生成される成果物 (RSS・サイトマップ・OG画像・検索インデックス) と、prerenderの並びで後ろに来るページ群だった。Publishをやり直しても、毎回同じ場所で同じ欠け方をした。
実行環境
- Astro 5.6 (静的output・prerenderのみ。SSR adapterは使っていない)
- ビルド成果物は212ファイル (pagefindの検索インデックス・sitemap・OG画像のPNGを含む)
-
npm run build= フォント取得 → コンテンツ品質ゲート →astro check→astro build - bolt.newのworkspace (WebContainer・Node 22) で開発し、Publish (bolt hosting) で公開
- 確認日は2026-09-06
まず疑ったこと (と、外れた話)
最初は「アップロードがディレクトリを落としている」と考えた。公開URLとbolt側の dist をパス単位で照合すると、これは違った。/_astro/ のCSSはハッシュ付きのファイル名が手元のビルドと一致していて、boltの dist に無いものだけが404になっていた。配信は dist の中身をそのまま写している。欠けはすべて上流、ビルド側で起きている。
_redirects のようなrouting設定も違う。それは「存在するページへの到達経路」の問題で、今回欠けているのはファイルそのものだ。
原因: Publishのビルドが47秒でkillされている
Publishを実行したときのworkspaceのターミナルを見ると、astro buildの出力がこう終わっていた。
10:50:06 ▶ src/pages/newsletter.astro
10:50:06 └─ /newsletter/index.html (+3ms)
10:50:08 λ src/pages/og/[...slug].png.ts
~/project 47s
ビルドが最後まで済んでいればprerender完了の行に続いて Complete! が出る。それが無いままプロンプトに戻っている。47s はこのコマンドの累積時間で、ビルドプロセスがこの時点で終了させられたことを示す。prerenderの並びで og/[...slug].png.ts の次に来るページ群 (privacy・search・tools・workflow・youtubeハブ) と、ビルド完了後の処理で生成されるもの (rss.xml・sitemap・pagefind・OG画像) が、ちょうど404のリストと一致する。
このサイトのビルドは、同じWebContainer内で自分で npm run build を走らせると87秒かかる (成果物212ファイル・完了まで確認)。Publishのビルドは、フルビルドに必要な時間の半分ちょっとで切られている。
Publishはpackage.jsonのbuild scriptとは別のビルド
もう一つ、dist の中身から分かったことがある。Publishが走らせるビルドは package.json の build scriptそのものではない。
決め手は成果物の形だ。Publish後の dist には、renderers.mjs・manifest_*.mjs・_noop-middleware.mjs・pages/*.astro.mjs・chunks/*.mjs といったサーバー側実行 (SSR) 形式の中間生成物が混ざっている。このサイトは静的outputの設定で、手元のビルド成果物 (212ファイル) に pages/*.astro.mjs は1つも無い。bolt公式ドキュメントもPublishの配信にNetlifyを使うと書いていて、Publish時はNetlify向けの形式でビルドされる余地がある。少なくとも「こちらの build scriptをそのまま実行した結果」では説明がつかない。
ビルドコマンドを差し替える設定も見当たらない。Publishのダイアログにはドメイン管理とUnpublish・セキュリティ監査くらいしかなく、公式ドキュメントのPublish関連ページにもビルドコマンドやタイムアウトの設定項目は無い (2026-09-06確認)。
対策: ビルド成果物をpublic/に入れておく
Vite (Astroのビルド基盤) には、public/ ディレクトリのファイルをビルド時に dist へそのままコピーする仕様がある。公式ドキュメントの該当箇所は "copied to the root of the dist directory as-is" と書いている。
コピーのタイミングはビルドの最初だ。これはkillされた dist を調べると分かる: 途中で切れた dist に、public/ 起源のファイル (favicon・robots.txt・_redirects・画像類) が最初から入っていた。prerenderより前にコピーが済んでいる。
だから dist の中身をすべて public/ に入れてcommitしておけば、ビルドがどのタイミングでkillされても dist にはサイト全体が入った状態になる。ビルドの速さに依存しない。
運用ルーチンはこうなる。
npm run build
rm -rf public/_astro public/tools public/workflow public/youtube public/blog public/about \
public/compare public/disclosure public/experiments public/lp public/newsletter \
public/privacy public/search public/pagefind public/og
cp -r dist/* public/
git add public/ && git commit -m "build: public/ 内蔵ビルド更新" && git push
rm は、前の世代の生成物が新しいビルドに無いファイルを配信し続けるのを防ぐためで、対象ディレクトリはサイト構成に合わせる。cp の後に public/images/images のような二重ディレクトリができていたら、rm -rf public/images && cp -r dist/images public/ で潰す。
注意点が2つある。
-
public/は生成物で、手書きのファイルはsrc/側に置く。内容を変えたらビルドし直してcpまでやる。cpを怠ると古いビルドのまま公開される - PublishのビルドはSSR形式の中間生成物 (
/renderers.mjs等) も毎回生成して配信する。この構成ではどこからも参照されていないため無害だが、消す手段は見つかっていない (※この点は【訂正】で撤回 — 中間生成物はビルドクラッシュ時の残留物で、完走時には発生しない。【訂正】の実験3参照)
解決を確認した方法
対策後のビルドを public/ に入れてPublish → Updateした後、以下がすべて200を返すことを確認した。
/tools/ /tools/chatgpt/ /workflow/ /privacy/ /search/ /youtube/
/rss.xml /sitemap-index.xml /og/default.png /pagefind/pagefind.js
404だった11 URLが全て200になった。/renderers.mjs 等の中間生成物は200のまま (上のとおり無害)。翌日の再確認でもこの状態は保たれている。
公式情報と似た報告
bolt公式の「Publish issues」ページは、Netlifyへのpublishに失敗した場合の回避策として「自分でビルドコマンドを実行し、プロジェクトをダウンロードして、Netlifyのドラッグ&ドロップデプロイを使え」と書いている。ビルドのタイムアウトや中断、部分デプロイへの言及はこのページに無い (2026-09-06確認)。
コミュニティには、古いbolt→Netlify連携の時代の「Netlify側のテンプレートが dist ではなく public を見ていたので、boltに修正させた」という報告がある。成果物をpublic側に置くという発想は近いが、あちらは公開ディレクトリの設定の話で、ビルド中断の話ではない。
今回言えていないこと
- 47秒という値の内部原因 (固定タイムアウトか、リソース制限か) は調べていない。観測事実は「このサイトでは3回のPublishすべてで47秒前後で切れた」
- 切れるまでの時間はサイトの規模やbolt側の状況で変わる可能性がある。ビルドが47秒以内に収まる小さいサイトでは、そもそもこの問題は起きない
- 対策は「切れても成立する配置にする」もので、killそのものを止めるものではない
【訂正 2026-09-06】再現実験で分かったこと — 47秒killはboltの仕組みではなく、workspace固有のビルド失敗だった
公開後に、使い捨てのGitHubリポジトリ2つで最小再現実験を行った。いずれも新規workspaceにimportしてPublishした実測である。
実験1 (bolt-repro-min) — Astro 5静的サイト・5ページ。Publishビルドは 5 page(s) built in 15.86s で完走、~/project 42s、公開成功。生成されたlive siteにはSSR形式の中間生成物 (/renderers.mjs pages/*.astro.mjs 等) が一切存在しない (すべて404)。つまり「PublishのビルドはSSR中間生成物を毎回生成する」はboltの一般動作ではなく、当時のworkspace固有の現象だった (その正体は実験3のとおりクラッシュ時の残留物)。
実験2 (bolt-repro-pad) — getStaticPaths で5万ページ生成するstubサイト。boltのPublishビルドは 50001 page(s) built in 246.25s で完走し、killされなかった (~/project 4m 39s)。47秒どころか4分超のビルドが正常完了している。したがって「boltのPublishがビルドを約47秒で強制終了する」という機構は存在しない。
当サイト (wakuflo) のworkspaceで3回のPublishがすべて47秒前後で終了した原因は、OG画像用フォントのENOENTクラッシュと確定した (当該サイトのコードコメントに当時の実測記録を残している)。このサイトは日本語OG画像生成にNotoSansJP (約4.5MB×2ファイル) を使うが、WebContainerがcommit済みの大きなバイナリを切り詰める問題があるため、フォントをgitignoreしてビルド時に取得する設計だった。当時のOG画像モジュールはモジュールのtop-levelでフォントを readFileSync しており、フォントが存在しないboltのPublishビルドではOGルートのimport時点でENOENTになり astro build が異常終了、OG以降にレンダリングされるページ群とビルド完了後の生成物 (RSS・sitemap・pagefind・OG画像) が欠けた部分distがそのままdeployされた。ビルドの停止位置がコード上いつも同じなので終了時刻もほぼ一定になり、それが「47秒のkill」に見えた。この機構の確定根拠は、①ターミナル記録がOGルートの行の直後で止まっている、②欠落ページがprerender順でOG以降に一致する、③当時のコードがtop-levelでフォントを読んでいた、④フォントはgitignoreされworkspaceに存在しない——の4点で確定している。エラー文言の生テキストも、後日の手元再現 (実験3) で取得し同一クラッシュを確認した。
なぜ当初気づけなかったかも記録として残す。第一に、boltのPublishはビルドが異常終了してもエラーを表示せず、部分distを成功としてdeployする。失敗はUIに現れず、一部ページの404として後から見つかった。第二に、切り分けに使った npm run build はフォント取得から始まるため、同じworkspaceでの手元実行はフォントが揃った状態でビルドが走り、成功していた。「手元では成功し、Publishだけ失敗する」という差は、ビルドの内容の差ではなくbolt側の機構の差に見えてしまう。第三に、Publish中のターミナルを監視しておらず、Complete! が出ていないことにも気づかなかった。
実験3 (手元再現・2026-09-07) — 原因確定を受け、当時の旧実装 (OG画像モジュールがtop-levelでフォントを読むコード) を一時的に復元し、フォントが無い状態で素の astro build を実行した (ASTRO_SKIP_FONT_FETCH=1 でフォント自動取得を無効化)。結果は newsletter ページの直後、OGルートの読み込みで ENOENT: no such file or directory, open '.../src/assets/fonts/NotoSansJP-Regular.otf' により異常終了 (exit 1) — 事故時にターミナルで観測した停止位置 (newsletterの次が λ src/pages/og/[...slug].png.ts) と一致する。さらにクラッシュ時の dist には renderers.mjs・pages/*.astro.mjs・chunks/ といったサーバー側中間生成物が141ファイル残留した。現行実装 (遅延読み込み+フォールバックPNG) の対照実験では exit 0・Complete!・中間生成物0件で完走する。つまり事故時に配信されていたSSR形式の中間生成物は、クラッシュ時に書き出された途中成果物の残留であり、完走時には存在しない — 「PublishがSSR形式でビルドする」のではなく「クラッシュの残骸がdistに残り、それごとdeployされた」である。
対策の public/ 内蔵は引き続き有効どころか、効能が広がったと考える。killタイムアウト専用ではなく、クラッシュ・kill・リソース不足などビルド失敗全般に対して「ビルドが途中で切れても dist ⊇ public/ で全サイトが配信される」構造が効く。
実験2で新たに見つかった上限: 5万ページ (5万ファイル超) ではビルド完走後にdeploy段階が Failed to publish the project — Deploy failed while extracting zip で失敗し、bolt.host上は404になる。ファイル数上限 (zip展開の限界) に関する公開情報は見つかっていない。静的サイトでもページ数をこの規模にする場合はPublish経路では配信できない。
なお実験時に、boltのGitHub連携tokenが失効していることも確認した (10時間前に同一アカウントでimportできたリポジトリが拒否された)。GitHub経由の再現には再認証が必要で、zipの直接添付は5MB上限があるため今回の対象 (public/内蔵で14MB超) は添付経路では運べない。
この「再現実験で原因を確定する」流れはLLM選びにも使える。LLMはベンチマークで選ばない — 自業務の実タスクで60分比較する手順に、実タスク比較の記録テンプレートをまとめている。
本稿の観察は2026-09-06時点のbolt.new (Publish / bolt hosting) と、Astro 5.6・Node 22 (WebContainer) の組み合わせによる。