Nuxt SSGビルドエラーを3つ潰して本番デプロイを復旧した
本番サイトを開いたら、2月22日以降の記事がごっそり消えていた。ブラウザのキャッシュかと思いシークレットモードで確認しても同じ。devサーバーでは全記事が表示されているのに、Cloudflare Pagesにデプロイされた本番だけがおかしい。ログを追い始めたら、原因が3つ重なっていた。
原因1: importパスの階層が1段足りなかった
tax-patterns/ 配下の4つのVueファイル(curated.vue, index.vue, quiz.vue, voucher-demo.vue)で、internal/data/extracted/ への相対importパスが1階層不足していた。
// NG: 4階層では届かない
import { ... } from '../../../../internal/data/extracted/curated-200'
// OK: 5階層必要
import { ... } from '../../../../../internal/data/extracted/curated-200'
pages/tax-patterns/curated.vue から internal/ に到達するには pages/ → app/ → web/ → apps/ → root/ の5段登る必要がある。4段だとViteがモジュールを解決できず、ビルドがそこで止まる。
この1つのエラーがViteのビルドプロセス全体を壊し、全ページのSSG生成が中断されていた。devサーバーではHMRが個別にモジュールを解決するため問題が表面化せず、本番ビルド時にだけ爆発するという厄介なパターンだった。
原因2: BlogCalendar.vueのDate型問題
BlogCalendar.vue の getDisplayDate() が、z.coerce.date() で返ってくるDate型オブジェクトを文字列として .split('T') していた。
// 修正後: String()でDate→文字列に変換してからsplit
const getDisplayDate = (article: Article): string | null => {
const date = article.updatedAt || article.publishedAt
if (!date) return null
return String(date).split('T')[0]
}
content.config.ts で publishedAt: z.coerce.date() と定義しているため、ランタイムでは Date オブジェクトが返る。Date.prototype.split は存在しないので、カレンダー表示がクラッシュしていた。String() でラップすることで toISOString() 相当の文字列に変換してから split('T') する形に修正。
原因3: 未コミットのコンテンツが本番に含まれていなかった
2月26日以降のコンテンツファイル群がgit管理外のままだった。ローカルのdevサーバーではワーキングツリーのファイルを直接読むため表示されるが、Cloudflare Pagesのビルドはgitリポジトリからチェックアウトするため、未コミットのファイルは当然含まれない。
git status で確認したら apps/web/content/2026-02-26/ 以降のディレクトリが全て ??(untracked)だった。コミットしてプッシュすることで本番に反映。
付随して修正したもの
make-diaryスキルのドキュメント改善
日記作成スキルのドキュメントに、category と project_name のバリデーション情報を追記した。content.config.ts の z.enum() に定義されていない値を指定するとZodバリデーションエラーでビルドが壊れるため、有効な値の一覧をスキル側にも持たせた。
content-managementスキルのcategory表にdiaryが欠落
content-managementスキルのcategory表に "diary" が載っていなかった。content.config.ts には z.enum(["personal", "dev", "diary"]) と定義されているのに、スキルのドキュメントには "personal" と "dev" しか記載がなかった。追記して整合性を取った。
前日の日記の404問題
前日の日記(diary-2026-03-04)へのクライアントサイドナビゲーションで404が返る問題があった。ブラウザのアドレスバーに直接URLを入力すると表示される。devサーバーを再起動したら解消した。Nuxt Contentのインメモリキャッシュが古い状態を保持していたと推測される。
振り返り
devサーバーで動いているのに本番で壊れる、という状況で2週間気づかずに放置していた。「pnpm build をローカルで回してから記事を書く」という習慣がなかったのが根本原因で、CIでビルドチェックを走らせるか、デプロイ前にローカルビルドを通すフローを入れるべきだった。
相対importパスの階層ミスは、エディタの補完に頼っていると見落とす。ファイルを pages/ の深い階層に置いた時点で、importパスを手動で数え直す癖をつけたい。TypeScriptのpath aliasを使えばそもそもこの問題は起きないが、モノレポで internal/ ディレクトリを参照する都合上、相対パスに頼っている部分が残っている。