最近、Galgame リソースサイトを Hugo から Next.js にまるごと書き直した。リポジトリは vns-next だ。create-next-app の最初の commit からオンラインに載せられるようになるまで、全部で 13 commit、期間は 20 時間足らず。
後からリポジトリを見返していて、なかなか笑えることに気づいた——Claude 用に書いたあの約定ファイル、全文たった 1 行しかない。しかもその 1 行、僕が書いたんじゃないんだよね…
この記事はレンダリングモードの話でもデプロイの話でもなく、「プロジェクト立ち上げ時に AI にどんな約定を残すべきか」という一点だけ。いくつかのプロジェクトで試行錯誤してたどり着いた結論を記録しておく…
僕の CLAUDE.md は全文 1 行だけ
まず原文から。vns-next ルートの CLAUDE.md、全文はこう:
@AGENTS.mdそう、この 1 行だけ。import 構文で隣の AGENTS.md を指している。じゃあ AGENTS.md はというと、全文 5 行:
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->注目してほしいのは BEGIN / END のマーカー——この部分は僕が書いたんじゃなく、create-next-app が自分で突っ込んだものだ。核心はただ一言:自分の訓練データに入っている Next.js を信じるな、コードを書く前にまず node_modules/next/dist/docs/ を読め、と。
以前 thdl(東方同人リソースサイト)を立ち上げたときも同じやり方で、CLAUDE.md はまったく同じ、1 行の import で AGENTS.md を指し、中身も一字一句違わない。利点は明快:単一の情報源だから、二つのルールがぶつかり合わない~
これだけ。足りるのかって?最初は僕も絶対足りないと思ってた…
もう一つの極端:1 リポジトリに三つの約定ファイル
以前のプロジェクト acg-site はまったく逆の道を歩んでいた——三つを同時にメンテしていた:
- ルートの
CLAUDE.md、111 行 .cursor/rules/配下の 9 個の.mdc、うちproject.mdcとlibrary-versions.mdcはalwaysApply: true.github/copilot-instructions.md、冒頭で自分を索引に格下げしている:「完全な約定はプロジェクトルートのCLAUDE.mdを参照」
CLAUDE.md の 3 行目はこう始まる:
鮮度に関する注意:本プロジェクトは最先端バージョンのライブラリを多用しており、AI の訓練データにはカバーされていない可能性が高い。不確かな API の使い方に遭遇したら、必ず最新のドキュメントを参照すること(Cursor では Context7 MCP が使える)。
そしてわざわざ「重要なバージョン差異(AI がよくやるミス)」という節を立て、6 項目:
- `params` / `searchParams` 在 Next.js 16 中是 **Promise**,服务端用 `await`,客户端用 `use()`
- Tailwind v4 用 **CSS 配置**(`@import "tailwindcss"` + `@theme {}`),无 `tailwind.config.ts`
- Prisma 7 generator 为 `"prisma-client"`(不是 `"prisma-client-js"`),导入路径 `@/generated/prisma/client`
- Zod 4 导入 `import { z } from "zod"`(不是 `zod/v4`)
- 日期用 `dayjs`,不用 `date-fns`
- 认证用 Better Auth(`@/lib/auth-client`),不是 next-auth.cursor/rules/library-versions.mdc の末尾には ❌→✅ の対照表まであり、7 項目:
1. ❌ `params.id` → ✅ `(await params).id` 或 `use(params).id`
2. ❌ `tailwind.config.ts` 的 `theme.extend` → ✅ `globals.css` 的 `@theme { }`
3. ❌ `import { PrismaClient } from "@prisma/client"` → ✅ `import { PrismaClient } from "@/generated/prisma/client"`
4. ❌ `provider = "prisma-client-js"` → ✅ `provider = "prisma-client"`
5. ❌ `import { z } from "zod/v4"` → ✅ `import { z } from "zod"`
6. ❌ 用 `date-fns` 处理日期 → ✅ 用 `dayjs`
7. ❌ 用 `next-auth` API → ✅ 用 Better Auth(`@/lib/auth-client`)これは本当に役に立つ。なぜなら書かれているのは全部、AI には知りようがないこと——バージョン差異だからだ。Prisma 7 で generator の名前が変わり、Zod 4 でインポートパスが変わり、こういうのを書いておかないと、モデルは記憶の中の古い書き方でそれっぽく仕上げてきて、こっちは存在しない import を 20 分デバッグすることになる。この代償は何度も払ってきた…
三つのファイルの代償はドリフト
commit 519e5a7 でドキュメントを更新したとき、CLAUDE.md の書き間違えた説明を分解した——元の文は「pnpm lint — ESLint + TypeScript チェック」だったけど、package.json の lint は eslint . しか走らせておらず、型チェックはまったく含んでいない。直した結果は三つ:
- `pnpm lint` — 仅 ESLint
- `pnpm typecheck` — 仅 TypeScript 类型检查(`tsc --noEmit`)
- `pnpm check` — ESLint + TypeScript 类型检查(提交前完整校验)同じ commit でバージョン番号も埋めた:tRPC 11.15 → 11.17、Zod 4.3 → 4.4、Prisma 7.6 → 7.8、Better Auth 1.5 → 1.6。
で、どうなったか?.cursor/rules/project.mdc には今も tRPC 11.15、Zod 4.3、Prisma 7.6、Better Auth 1.5 と書かれたままで、あの間違った lint の説明すらそのまま残っている。片方を直して、もう片方を忘れる、これがドリフトってやつだよね…
複数人で協力するリポジトリだとこれはさらに悪くなる:二つの AI ツールがそれぞれ別のディレクトリを読み、得られるコンテキストがそもそも同じじゃない。その話は別の記事に書いたので、ここでは広げない。
約定ファイルもコードだ、そして期限切れになる。しかも期限切れを知らせてくれる CI なんてない…
じゃあ知識は実際どこに落ちているのか
vns-next を振り返ると:.claude/ はなく、.cursor/ もなく、.github/ もなく、docs/ もなく、タスクリストも仕様書もない。でもコンテキストは少しも欠けていない。
なぜなら知識は全部コードコメントと commit body に書かれているから~
next.config.ts の trailingSlash の部分は、「なぜ必要か」と「なぜ内蔵のものまでオフにするのか」を一緒に書き込んでいる:
// 与旧站(Hugo)URL 完全一致:尾斜杠必须保留,
// Artalk/Valine 历史评论按 /p/{id}/ 路径存储,去掉斜杠会丢评论
trailingSlash: true,
// 但内置的尾斜杠 308 会把 POST /api/auth/* 重定向成 /api/auth/*/ 导致 404(Better Auth 全挂),
// 故关掉内置重定向,由 src/proxy.ts 只对页面 GET 请求补尾斜杠
skipTrailingSlashRedirect: true,src/proxy.ts:21-23 は純粋な警告で、このコードを「最適化」しようとする次の人(おそらく AI)に向けて書かれている:
// 注意:不能用 nextUrl.clone() 再改 pathname——NextURL 的 setter 会按
// trailingSlash 配置把尾斜杠再规范化掉,Location 指回自身造成死循环。
// 官方 skipTrailingSlashRedirect 示例即用裸 new URL() 构造。src/lib/cache.ts:1-5 はこのファイルが将来的にどう置き換えられるかを明記している:
/**
* 内存 TTL 缓存 helper
*
* 接口对齐 acgn-flow 的 redis helper(getCache/setCache/deleteCache/deleteCachePattern),
* 将来若要换 ioredis 可以无痛替换本文件实现,调用方不需要改动。
*/commit b07e20d のメッセージには検証手段まで書かれている——この罠そのもの(ビルド時にデータベースがない)については別の記事で詳しく書いたので、ここでは commit body に何を突っ込んだかだけを見る:
fix(deploy): 全站 force-dynamic——Docker 构建环境无数据库,静态预渲染必然失败
根 layout 声明 dynamic='force-dynamic'(查库页面请求时渲染,内存缓存兜底),
sitemap/feed.xml/llms.txt/index.xml 同步声明。已用死库 DATABASE_URL 本地复现
构建通过。これらの場所に共通しているのは、次に必ず読まれるということだ。proxy.ts を変更する人は必ずあの無限ループの警告を目にするし、next.config.ts を見る人は必ず末尾スラッシュの経緯を目にする。一方、docs/architecture.md が開かれるかどうかは、まったく運次第だ…
現在の使い分け
| 内容の種類 | 置き場所 | 使用頻度 |
|---|---|---|
| フレームワークのバージョン差異と API の罠 | CLAUDE.md | ★★★★★ |
| ある一行のコードがなぜそう書かれているか | コードコメント | ★★★★★ |
| ある変更の動機と検証方法 | commit body | ★★★★ |
| デプロイと運用の手順 | 別途 README を1つ | ★★★ |
| プロジェクト紹介、ディレクトリ構成 | 書かない、すぐ古くなる | ★ |
モデルがコードから推測できないのは最初の種類だけで、書かなければ分からない。まあ「本プロジェクトは App Router を採用しており、ディレクトリ構成は以下の通り……」みたいなのは、モデルが ls すればすぐ分かることだ~
ついでに README を自嘲しておく
古くなるものといえば。vns-next の README は今も create-next-app のデフォルトテンプレートのままで、36 行、npm/yarn/pnpm/bun run dev と Deploy on Vercel について書いてあり、最初の commit 85aa812 から一字も変わっていない。
しかもこのプロジェクトは実際には bun + Docker Compose のセルフホストで、デプロイは Vercel を一切通っていない(next.config.ts のあの VERCEL ブランチは保険として残しているだけだ)。README と現実は完全に乖離している…
ただ、.dockerignore:13-19 にはこう書いてある:
# 内容导出快照 / 文档 / 部署自身
content-export
deploy
README.md
AGENTS.md
CLAUDE.md
.claudeREADME、AGENTS.md、CLAUDE.md、.claude はまとめてイメージのコンテキストから除外されている(.claude はついでに防いでいるだけで、このプロジェクトでは実際そのディレクトリを一度も作ったことがない)。だからもう直す気も起きない。どうせイメージには入らないし…
ちなみに、その 13 個の commit のうち 12 個には Co-Authored-By: Claude Opus 4.8 の trailer が付いていて、唯一付いていないのは create-next-app が生成した最初の commit だ。すべての commit の author は自分自身——署名は署名、責任はやっぱり自分のものだ~
どうせ次のプロジェクトでも一行だけ書いて、ハマったらその都度足していくんだろうな~
