使用 Next.js 啟動專案
地基在第一天鋪設最便宜:之後每一條規則都得覆蓋在違反它的程式碼之上。如果有 frontend-starter 範本——請從範本的副本開始: 範本中的步驟已經完成並驗證,而這份文件則保留作為決策說明與稽核查核清單。
這份文件定義了
步驟的順序與最小集合。如何編寫程式碼本身——請見技能 fsd-frontend(如果專案中有此技能),授權——請見 frontend-auth-session,提交前檢查——請見 anti-slop-review。
現成設定檔——請見 references/configs.md,Next + FSD 結構與程式碼範例——請見 references/next-structure.md。
規則優先順序。 使用者的明確指示與專案的 agent 檔案(AGENTS.md、CLAUDE.md)的權重高於此文件。套件版本會變更:在複製設定檔之前,請先比對已安裝版本的官方文件,並且在實際執行之前不要聲稱它能夠正常運作。
關於 Next.js 的事實——請參考套件內的文件,而非此文件或記憶。 自 16.2 版起,文件位於 node_modules/next/dist/docs/,專案的 AGENTS.md 也會引用它。如果與此處的範例有出入,請以已安裝版本的官方文件為準。
標記:MUST — 不得違反;SHOULD — 預設為是,偏離需要有原因。
1. 編碼前的決策
表格中的每個決策都必須記錄在 README(「決策」章節)或 ADR 中,並附上原因。未記錄原因就偏離預設值——不要這麼做。
| 問題 | 預設值 | 何時例外 |
|---|---|---|
| Next.js 或 SPA | 如果需要至少以下一項,就使用 Next.js:SSR/SEO、BFF(機密、HttpOnly-cookie)、route handlers | 內部應用程式已登入、無 SEO 且無 BFF → 使用 Vite + React Router:建置與部署更簡單 |
| 路由器 | App Router | Pages Router — 僅適用於現有專案 |
| Monorepo | 否,單一應用程式 | 當確有第二個應用程式或套件出現 → 使用 pnpm workspaces;Nx/Turborepo — 當建置或 CI 開始變慢時 |
| Multi-Zones、微前端 | 否 | 具有獨立發布的獨立團隊 |
| 伺服端資料 | Server Components 用於初始載入;TanStack Query 用於互動性的客戶端資料與 Mutation | 純客戶端應用程式 → 僅使用 TanStack Query |
| 文字與 i18n | 從第一天起就建立翻譯模組(next-intl 或類似套件),即使是單一語言環境,如果專案遵循 fsd-frontend 的話 | 明確的「不使用 i18n」的決策已記錄在 README 中 |
| 套件管理工具 | pnpm,版本寫在 packageManager 中 | — |
| 部署 | 立即決定:Vercel、Node 伺服器(output: "standalone")或 Docker | 這會影響 env、快取、CI |
MUST: 不要為了「未來」而加入技術。Monorepo、第二個路由器、Storybook、狀態管理工具、MSW(作為後端類型)只有在它們有第一個真正的使用者時才出現。
2. 步驟順序
- 建立專案(
pnpm create next-app@latest):TypeScript、ESLint、Tailwind、App Router。固定packageManager、engines.node(Next 16 需要 Node 20.9+)以及.nvmrc。 - 立即刪除產生器範本: 起始頁面、
public/中的 SVG、範本 README、示範樣式。將 README 替換為references/configs.md中的基礎架構。留下來「以後再處理」的範本會存活到正式環境。create-next-app產生的AGENTS.md與CLAUDE.md不要刪除:<!-- BEGIN:nextjs-agent-rules -->與<!-- END:nextjs-agent-rules -->之間的區塊會導引 agent 至已安裝版本的官方文件,next dev會自行維護它。你自己的專案規則應該寫在這些標記之外。 - 嚴格 TypeScript(
references/configs.md的第 4 節)與typecheck腳本(tsc --noEmit)。 - 在 Next 之上疊加 FSD 結構(下面的第 3 節)。
- Linter 與格式化: 來自
eslint-config-next的 ESLint flat config + 類型感知規則 + 邊界 lint + Prettier。Next 16 移除了next lint,並且在next build時不會執行 lint——linter 需透過獨立腳本執行並在 CI 中執行。 - Env 設定(第 4 節)。
shared/api中的錯誤模型與 API 客戶端(第 5 節)。_app層中的 Provider: QueryClient、主題、i18n、全域服務(toast、confirm)作為shared中的 port + 在_app中實作。- UI 基礎(第 6 節)。
- 測試: Vitest + Testing Library + MSW、用於 e2e 的 Playwright(第 7 節)。
- Git hooks: husky + lint-staged (eslint --fix, prettier) + commitlint。
- CI: 每次 push 與 PR 都執行 lint、typecheck、test、build(第 8 節)。
- 預設安全性(第 9 節)。
- README 已經填入真實的內容(第 10 節),並且已完成「完成定義」(第 11 節)。
步驟 1–3 應作為一個獨立的「scaffold」提交:這樣在歷史中就能清楚看到產生器做了什麼,而你做了什麼。
3. 在 Next 路由之上疊加 FSD
根據 App Router 的官方 FSD 指南:Next 的服務資料夾 app/ 位於專案根目錄,FSD 層位於 src/,而 app 與 pages 層則加上前綴 _,以避免與 Next 的服務資料夾混淆。
app/ Next 路由:僅包含路徑檔案
├── layout.tsx 根層佈局 → 來自 src/_app 的 Provider
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts route handler (BFF),薄層
proxy.ts Next 16+(以前是 middleware.ts),位於 app/ 旁
src/
├── _app/ app 層:Provider、全域樣式、服務實作
├── _pages/ pages 層
├── widgets/ (如果專案使用它們)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
app/中的檔案要薄:從src/_pages重新匯出頁面、generateMetadata、解析params/searchParams(在 Next 15+ 中它們是Promise,需要await)並透過 props 傳遞。其中不應包含邏輯。- Route handlers 與 Server Actions 要薄:解析並驗證輸入 → 呼叫
src中的函式 → 回傳回應。每一個都是公開的進入點:自行檢查授權,而不是依賴proxy.ts。 - 不要建立
src/app與src/pages:Next 會在根目錄與src中尋找服務資料夾,具有此名稱的資料夾很容易變成第二個路由器。 - 邊界 lint 知道前綴:使用
src/_app、src/_pages/*模式,而非src/app、src/pages/*(設定檔 —fsd-frontend、references/conventions.md,第 11 節)。 "use client"應盡可能放在樹的底層。頁面預設為伺服器端,客戶端則是具有互動性的葉節點。- 包含機密的模組應以
import "server-only"開頭,包含window的模組則以import "client-only"開頭。
詳細說明與程式碼:references/next-structure.md。
4. 環境設定
MUST:
- 變數在兩個模組中讀取:
shared/config/env.server.ts(import "server-only")與shared/config/env.client.ts。模式使用zod,在匯入時驗證:設定錯誤會在啟動時發生,而不是在第一次請求時。在其他程式碼中禁止使用process.env(lint 規則)。 NEXT_PUBLIC_*在建置時嵌入 bundle,且僅在直接引用process.env.NEXT_PUBLIC_X時才會:不要使用解構賦值或process.env[name]。這樣無法為具有不同公開值的多個環境建置單一映像——這些值應在伺服器端讀取並向下傳遞。- 機密只能使用不含
NEXT_PUBLIC_前綴的變數。 .env.example應包含每個變數,附有註解與安全值;.env*應列入.gitignore,但.env.example除外。CI 建置應使用.env.example中的值。
5. 錯誤模型與 API 客戶端
MUST:
- 錯誤應在
shared/api的邊界處正規化為帶有kind屬性的AppError(模型 —fsd-frontend、references/conventions.md,第 1 節)。在shared/api之外,沒有人應該解析Response、狀態碼與錯誤訊息。 - 在
fetch之上建立一個統一的request()函式(或使用同樣包裝過的生成客戶端):它會傳遞signal、將網路錯誤轉換為network、將 HTTP 狀態碼轉換為kind、並使用 schema 驗證回應主體。範例 —references/next-structure.md。 - 錯誤類型應根據其代碼決定:狀態碼、
extensions.code(GraphQL 在錯誤時也回傳 200)、主體中的code。如果後端只回傳文字,則在單一 mapper 中根據文字進行比對,並附上原因註解與測試。 - 後端回應應在邊界處映射為領域類型;DTO 不應流入 UI。
- 使用者資料不應在不同使用者之間快取:對於依賴 session 的回應,不要使用
"use cache"/unstable_cache。 - 如果存在 schema(OpenAPI、GraphQL),則客戶端應從 schema 生成。Schema 或生成的程式碼應存放在 repository 中:CI 不應連接到真實的後端。
SHOULD: 在傳輸層之上建立介面(AuthApi + factory)——僅當它已經有第二個消費者時(fsd-frontend、references/state-and-data.md,2.1)。不要使用像 type: "mock" 的設定值,它會拋出「未實作」。
6. UI 基礎
- 設計 Token — 在
:root中使用 CSS 變數(並用於暗色主題),透過@theme連接到 Tailwind v4。在元件中僅使用 token;原始 HEX 與任意值(text-[#3a3a3a])——僅作為一次性例外,並附上註解。 - 類別必須存在。 從 shadcn/ui 與範例複製過來的元件會帶有自己的 token(
bg-card、text-muted-foreground)。你必須建立這些 token,或者替換這些類別——否則元件會默默地沒有樣式。 shared/ui中的基本元件應建立在 headless 函式庫之上(Radix、React Aria)以確保無障礙性;變體使用cva,類別合併使用cn(clsx+tailwind-merge)。- 字型使用
next/font,圖片使用next/image並指定尺寸,images.remotePatterns應明確列出。 - Storybook — 僅當 UI Kit 被多個應用程式或團隊重複使用時才使用,不會更早。
7. 測試
- Vitest + Testing Library + jsdom + MSW 並搭配
onUnhandledFrame: "error"(MSW 3;在 MSW 2 中為onUnhandledRequest)。設定檔 —references/configs.md。 - Vitest 無法渲染非同步 Server Components。將其中的邏輯提取到函式中並透過單元測試測試,頁面本身則透過 e2e 測試。
- Playwright 用於 3–10 個關鍵路徑。在 CI 中,e2e 應針對可預測的後端(mock 伺服器、具有測試資料庫的測試環境)執行,而不是針對會導致測試修改他人資料的共用環境。
- 第一個測試應在 Day 0 編寫:它會驗證測試基礎設施是否正常運作,並提供一個範例。
8. CI
MUST: 每次 push 到主要分支以及每個 PR 都應執行:lint、typecheck、test、build。GitHub Actions 範本 — references/configs.md。
對於公開 repository,Actions 是免費的;對於私有 repository,則有每月分鐘數配額。
- CI 建置應使用
.env.example,不含機密。 - e2e 應作為獨立的 job 執行,當有隔離的後端時;在此之前,應在 README 中明確標記為何 CI 中沒有 e2e。
- README 中的 CI badge 應與 workflow 同時出現。
9. 預設安全性
poweredByHeader: false;透過next.config.ts中的headers()設定標頭:X-Content-Type-Options: nosniff、Referrer-Policy: strict-origin-when-cross-origin、X-Frame-Options: DENY(或 CSP 中的frame-ancestors)。CSP 與 nonce 是另一項任務,但「有 / 無及其原因」的決策應立即記錄下來。- Server Actions 與 route handlers 是公開端點:每個都應使用 schema 驗證輸入並檢查權限。
dangerouslySetInnerHTML— 僅使用 sanitizer 並附上原因註解。- 個人資料與 token 不應被記錄。
- 授權 — 請見
frontend-auth-session。
10. README
基礎架構 — references/configs.md。規則只有一個:README 中只應包含對該提交中的程式碼為真的事實。 每個章節應在描述它的程式碼所在的同一個提交中更新。計劃應放在獨立的「下一步」章節中,並標記為尚未完成。
11. 啟動的完成定義
- 產生器範本已刪除;README 是基礎架構,並已填入真實內容。
-
packageManager、engines.node、.nvmrc已固定。 - TS 嚴格模式已啟用;
typecheck、lint、test、build能在本地通過(已執行,而非假設)。 - 使用
_app/_pages前綴的 FSD 佈局已就緒;邊界 lint 能捕獲向上匯入。 - Env 透過兩個帶有 schema 的模組進行管理;
process.env在這些模組之外被 lint 禁止;.env.example是完整的。 -
shared/api中已有AppError與request(),並有測試覆蓋(網路、狀態碼、無效 schema)。 -
_app中的 Provider 已就緒;QueryClient 不應在伺服器上跨使用者共用。 - 設計 token 已存在於 CSS 變數中;所有使用的類別都存在。
- Vitest + MSW + Playwright 已設定完畢,各有一個實際的測試。
- Git hooks 與 commitlint 正常運作。
- CI 是綠燈。
- 安全性標頭已設定,且
poweredByHeader: false。 - 第 1 節中的決策已記錄。
12. 當你猶豫不決時
- 在「現在就加入」與「等到需要時再加入」之間——選擇等到需要時再加入。
- 在「手動設定」與「保留 create-next-app 產生的設定」之間——在沒有理由改變之前先保留,並且在改變時記錄原因。
- 在「單一應用程式」與「Monorepo」之間——選擇單一應用程式。
- 在「參考記憶中的設定」與「參考目前版本的官方文件」之間——選擇官方文件。