CLAUDE.mdを分割したらビルドが壊れた話
ネストCLAUDE.md構成に分割した約9時間20分後、Astroのcontent collectionsがglobパターンでCLAUDE.mdを拾いスキーマ検証エラーでビルド失敗した実例を、実行ログとdiffで解説する。
この記事の結論: Astroのcontent collectionsでglob()ローダーのpatternに緩いパターン(**/*.md等)を使うなら、対象ディレクトリに非コンテンツファイルを置く予定がある時点で除外パターンを配列で必ず添える。「良かれと思ったドキュメント整理」でも壊れる。
リナ社長(高校生社長のやり手ギャル)× タクヤ(入社3年目)— 今回は、CLAUDE.mdを分割した約9時間20分後にビルドがサイレントに壊れてた回。(登場人物について)
リナ社長「タクヤ、この前のCLAUDE.md分割の件さ、コミットログ見てたら気になるのあったんだけど。分割した約9時間20分後に、なんか慌てて直してるコミットあるじゃん」
タクヤ「a8624e2ですね……ちょっと待ってください、これ見て青ざめました。分割コミットの68e4639から約9時間20分でビルドが落ちてたっぽいです」
事件発覚: ドキュメント整理の約9時間20分後に何が起きていたか
68e4639(“docs: CLAUDE.mdを用途別に分割(ネストCLAUDE.md構成)“)は、肥大化していたルートのCLAUDE.mdを、Claude Codeが作業ディレクトリに応じて自動で追加読み込みする「ネストCLAUDE.md」の仕組みに載せて分割したコミットだ。ブログ投稿ルールはsrc/content/blog/CLAUDE.mdへ、新規ページ作成ルールはsrc/pages/CLAUDE.mdへ、AdSense対策の共通チェックリストはdocs/adsense-checklist.mdへ切り出し、ルートのCLAUDE.mdはwrangler.toml運用ルールと目次だけの最小構成に縮小した。
その約9時間20分後のa8624e2(“chore: blogコレクションのglobパターンからCLAUDE.mdを除外”)のコミットメッセージ本文には、こう書かれている。「src/content/blog/CLAUDE.mdがblogコレクションのglob(**/*.{md,mdx})に拾われ、frontmatterスキーマ検証エラーでnpm run buildが失敗していたのを修正。」
リナ社長「え、待って。ドキュメント整理しただけで、なんでビルドが落ちるの?CLAUDE.mdとブログ記事、フォルダも用途も全然違うじゃん」
タクヤ「そこなんです。src/content/blog/CLAUDE.mdを新しく作った場所が悪かったんですよ。ブログのcontent collectionはsrc/content.config.tsで、base: './src/content/blog'配下の**/*.{md,mdx}を全部拾う設定になってました。つまり拡張子が.mdならCLAUDE.mdだろうと関係なく、ブログ記事の1本として読み込もうとしてたんです」
原因: globの緩いパターンとZodスキーマ検証エラーの読み方
修正前のコードはこうなっていた。
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.optional(image()),
enableComments: z.boolean().optional(),
tags: z.array(z.string()).optional(),
}),
});
修正後(現行コード)は、patternを配列にして除外パターンを1行足しただけだ。
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: ['**/*.{md,mdx}', '!**/CLAUDE.md'] }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.optional(image()),
enableComments: z.boolean().optional(),
tags: z.array(z.string()).optional(),
}),
});
タクヤ「ポイントはpatternが単一の文字列だと除外パターンを足せない、ってところです。Astroのglob()ローダーはpatternに配列を渡した場合だけ、!始まりの否定パターンで対象から除外できます。修正前の'**/*.{md,mdx}'みたいに文字列一本で書いてる限り、CLAUDE.mdを弾く方法がそもそも無かったんですよ」
リナ社長「じゃあビルドが落ちた時、画面には何て出てたの?」
タクヤ「実際に確かめてみましょう。分割直後のbeforeパターンに一旦戻して、npx astro buildを回してみます」
このセッション内で編集長自身が、修正前のパターンに一時的に戻してnpx astro buildを実際に実行し、再現・採取した。採取後すぐにafterの状態へ戻し、現状のコードが無傷であることも確認済みだ。
$ npx astro build
...
22:07:57 [content] Syncing content
[InvalidContentEntryDataError] blog → claude data does not match collection schema.
title**: **title: Required
description**: **description: Required
pubDate**: **pubDate: Expected type `"date"`, received `"object"`
Hint:
See https://docs.astro.build/en/guides/content-collections/ for more information on content schemas.
Error reference:
https://docs.astro.build/en/reference/errors/invalid-content-entry-data-error/
Location:
/home/user/jinbei-lab/src/content/blog/CLAUDE.md:0:0
タクヤ「blog → claudeってエントリ名が出てるのが手がかりです。CLAUDE.mdのファイル名がスラッグ化されてclaudeというブログ記事扱いになってる。中身にfrontmatterが無いので、title: Required・pubDate: Expected type "date", received "object"とZodスキーマの必須項目が軒並みコケてます。エラーメッセージを逆から読むと『frontmatterの無いファイルがコンテンツとして誤認識されてる』というのが一発で分かる形になってますね」
タクヤ「仕組みとして面白い」、リナ社長「それ運用リスクじゃん」
タクヤ「個人的にはこの一件、けっこう興味深いバグだと思ってます。原因のもう一段深いところに、Claude CodeのネストCLAUDE.md自動読み込みという便利機能があって、それが結果的にsrc/content/blog/という本来ブログ記事しか置かない場所に、フォーマット的には.mdだけど中身は全然違う『データっぽく見えるファイル』を増やす副作用を生んでる。機能同士が組み合わさって初めて表面化する系のバグで、直す側としては面白いです」
リナ社長「いや、うちは面白がってる場合じゃないでしょ。9時間20分だよ、9時間20分。それだけの時間、誰も気づかないままビルドが壊れてたってことじゃん。もし気づくのがもっと遅れてたらどうなってた?」
タクヤ「うちの構成だとCloudflare Pagesがpush時にnpm run build相当を走らせてデプロイするので、気づかなくてもデプロイ自体は失敗して止まるはずです。無言でおかしい記事が公開される、みたいな最悪は避けられます」
リナ社長「止まるのは止まるけど、それって『デプロイが刺さって記事が一切公開されなくなる』ってことでしょ?原因が分からないまま『なんかデプロイ落ちてる』って騒ぐ時間、うちみたいな週3本ペースの更新頻度でやってたら普通に痛いよ。しかも今回はたまたま9時間20分後に気づいて1行直しただけで済んでるけど、これ他のディレクトリでも同じ形の地雷埋まってる可能性あるよね?」
タクヤ「……それは否定できないです。実際、修正自体はpatternに配列と除外1行を足すだけで完了してます。致命傷ではないんですが、確かに『気づくのが遅れる』前提のリスクとして見ると、修正の軽さとは別の話ですね」
リナ社長「そう、そこ。直すのが軽いのと、気づくまでの時間が読めないのは別問題。しかもこれ、うちだけの話じゃなくて、Astroのcontent collectionsを使ってる開発者全般、あと『AIエージェント向けの設定ファイルをプロジェクトの中に増やしてるチーム』全般に刺さる話だと思う」
まとめ
src/content/blog/配下にドキュメント用のCLAUDE.mdを新設したことで、blogコレクションのglob()パターン**/*.{md,mdx}がそれを1本のブログ記事として誤認識し、frontmatter必須項目のZodスキーマ検証エラーでnpm run buildが失敗した。原因は「patternが文字列一本だと除外パターンを足せない」というAstroのglob()ローダーの仕様上の勘所で、修正はpatternを配列にして!**/CLAUDE.mdを1行足すだけで完了している。
持ち帰りとして具体化するなら判断基準はこうだ。Astroのcontent collectionsでglob()のpatternを書くとき、**/*.mdのような緩いパターンを使うなら、対象ディレクトリに設定ファイル・READMEなど非コンテンツファイルを将来置く予定が少しでもあるかを必ず確認する。予定があるなら、最初からpatternを配列にして除外パターン(!**/ファイル名)をセットで書く。ネストCLAUDE.mdのような「便利な自動読み込み機能」を採用するなら、それが将来どのディレクトリにファイルを増やしうるかまで含めて設計しておいた方がいい。
タクヤ「あなたのプロジェクトのglobパターン、除外条件ちゃんと書いてますか?」
コメント