# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Common Commands ```bash # Development npm run dev # Start dev server on port 3000 npm run dev:clean # Clean .next/dist then start dev server # Build & Preview npm run build # Build production files to dist/ npm run build:clean # Clean then build npm run start # next start -p 3000 — serve the built output npm run preview # ALIAS of `start` (identical `next start -p 3000`); it is NOT `npx serve` # 说明(2026-09-23 文档同步轮核实):`next start` 与 `output: 'standalone'` 组合下 Next 16 会打印 # `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" # instead.`(实测见 `docs/acceptance/2026-09-21-gates/ga4-production-run.txt`;抛出点 # `node_modules/next/dist/server/next.js` 的 `getServer()`)。它是 **warn 而非 throw**——服务照常起来, # 但走的是不受支持的降级路径,所以门禁脚本正逐步脱离 `next start`:`test:e2e:prod` 已改为直接 # `node dist/standalone/server.js`(`e2e/playwright.config.ts:132-135`);`check:headings` 也已脱离 # `next start` —— `scripts/utils/check-heading-hierarchy.ts:55-66` 现为「存在 dist/standalone/server.js 就直接 # 起 standalone(注入 HOSTNAME/PORT/NODE_ENV),仅当产物缺失才回退 `npm run preview` 并打印告警」, # 见验收报告 N-24③。至此门禁脚本不再依赖不受支持的 `next start` 路径。 # standalone 的受支持启动方式是 `node dist/standalone/server.js`,且服务启动前 `public/` # 与 `dist/static` 必须已在 standalone 根目录下(server.js 启动时缓存静态索引,缺资源会让 # /_next/static/** 全 404)。**这一步由 `package.json:9` 的 `postbuild` 自动完成**(`npm run build` # 结尾 `rm -rf` + `cp -R dist/static → dist/standalone/dist/static`、`cp -R public → dist/standalone/public`), # 所以正常路径下"先 build 再起 standalone"即可,不必手工拷;仅当产物来自别处或要复用旧 dist 时才参照 # `Jenkinsfile:353-355`(axe 阶段复用上一阶段产物时的同套拷贝)与 `scripts/deploy.sh:162-175` # (发布侧把 `public/` 与 `dist/static → dist/_next/static` 同步进 Nginx 站点根)。npm 侧没有包一层的启动脚本。 # Deploy (统一发布脚本) ./scripts/deploy.sh build # 构建静态产物 ./scripts/deploy.sh deploy # 构建并发布到生产服务器 ./scripts/deploy.sh deploy --skip-build # 使用现有 dist/ 直接发布 ./scripts/deploy.sh rollback # 回滚到最近一次远程备份 ./scripts/deploy.sh status # 查看生产环境发布状态 # Linting & Type Checking npm run lint # ESLint flat config at repo root (`eslint.config.mjs`; `config/lint/.eslintrc.json` no longer exists) npm run type-check # tsc --noEmit # E2E Testing (Playwright) npm run test # 全链路 E2E:先 test:functional(4 功能 project),再 test:visual:all(5 视觉 project),串行以免写库用例与截图并发污染基线 npm run test:functional # 仅功能 project(chromium / chromium-mobile / firefox / webkit) npm run test:e2e # Same as test:functional npm run test:smoke # Only @smoke-tagged tests npm run test:e2e:prod # @smoke+@critical+@journey 打构建产物(E2E_TARGET=production → node dist/standalone/server.js,CI 门禁) npm run test:visual # Visual regression (Desktop Chrome) npm run test:visual:all # Visual regression (all projects) npm run test:visual:update # Update visual snapshots npx playwright test --grep "test name" # Run a single E2E test by name # ⚠ E2E 目标口径(默认跑的是 dev server,不是产物) # `npm run test` / `test:functional` 的 webServer 是 `npm run dev`(`e2e/playwright.config.ts:132-135`), # 因此 **16 个 @critical GA4 用例(TC-GA4-001..004 × 4 project)在这一轮里是 skipped,不是 passed**: # `NEXT_PUBLIC_GA_MEASUREMENT_ID` 只写在 `.env.production`,dev 读不到 → `GoogleAnalytics.tsx:81,119` # 提前 return null → 用例内 `test.skip(measurementId === null, …)` 触发。证据: # `docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json`(28 skipped 中该组恰为 16)。 # 它们真正断言的唯一入口是 `npm run test:e2e:prod`(`E2E_TARGET=production` → 直接 # `HOSTNAME=localhost PORT=3000 node dist/standalone/server.js`,非 `next start`;须先 `npm run build` # 并把 `dist/static` + `public` 并入 standalone 根目录,见 Build & Preview 段的装配口径), # 见同目录 `ga4-production-run.txt` 的 4 passed。故「`npm run test` 全绿」不等于 GA4 覆盖已验证。 # Unit Testing (Jest) npm run test:unit # Run all unit tests npm run test:coverage # Coverage report(阈值以 jest.config.js 的 coverageThreshold 为准;根配置 re-export config/test/jest.config.js 以免双份漂移) npx jest --testPathPattern="button" # Run a single test file matching pattern # Quality Checks npm run check:a11y # 可访问性静态门禁伞(= check:contrast + check:headings + check:brand-token,已并入 test:all) npm run check:contrast # 令牌对比度审计(读 globals.css 令牌表,浅色+暗色双主题、含 alpha 组,缺令牌即红) npm run check:headings # 标题层级审计(有 dist/standalone/server.js 时直起 standalone;产物缺失才回退 `npm run preview` 并打印告警 —— scripts/utils/check-heading-hierarchy.ts:52-67) npm run check:brand-token # 品牌红文字必须走 text-brand-ink* 令牌(DESIGN.md 双通道红规则) npm run check:motion # 动效契约门禁(N-29 收口):R1 令牌档位(instant 100ms / fast·normal 180–280ms,CONTEXT.md:76)、 # R2 时长 ≤700ms(CSS 声明、framer transition 对象的 duration、Tailwind duration-* 类、tailwind.config 的 animation; # infinite 循环与 *-delay 豁免)、R3 transition 时长必须走 var(--transition-*)、 # R4 写死的 cubic-bezier 必须属于令牌层 --ease-*(允许集合从 CSS 解析,不硬编码)且 --ease-ink==[0.22,1,0.36,1]。 # 退出码 0 干净 / 1 违规 / 2 未能度量(空扫描、缺令牌文件)—— 空扫描不读作干净。 # ⚠ 当前树 23 处违规(EXIT=1),故**尚未**并入 check:a11y,详见 docs/development/quality-gates.md §5.1 npm run check:motion:test # 上述门禁的自证伪测试(19 例,双向:合规夹具判 0 + 违规夹具判对应规则红); # 测试在 scripts/ 下,jest 默认 roots 只含 src,故该脚本显式传 --roots npm run check:axe:routes # 全站路由清单(sitemap ∪ 预渲染产物 ∪ 站内链接 BFS)→ /tmp/axe-routes.xml # 同样需要 :3100 的生产服务(SEED 默认取 ${BASE}/sitemap.xml) npm run check:axe # 双引擎(chromium/firefox)×双主题(light/dark)逐页 axe 节点计数 + 三条规则级 # 规则(autocomplete-valid/presentation-role-conflict/svg-img-alt)的分母断言; # 退出码 0 通过 / 1 判红 / 2 清单缺失或 0 条(假绿熔断)。证据落 # docs/acceptance/2026-09-21-axe/axe-evidence.json。需一个生产服务在 :3100: # PORT=3100 HOSTNAME=127.0.0.1 node dist/standalone/server.js(先拷 dist/static 与 public,见上文) # CI 侧由 Jenkinsfile「♿♿ 全站 axe 节点计数」阶段(仅 main)自动串起 npm run test:all # type-check + lint + test:coverage + test:integration:real + check:a11y + test:e2e:fast + test:security:headers npm run test:e2e:prod # E2E_TARGET=production(构建产物目标);harness **不代跑构建**,须先 `npm run build` # (standalone 根目录的 dist/static + public 由 postbuild 自动装配,见上文) npm run lighthouse # Lighthouse CI(配置 config/test/lighthouserc.json;axe critical/serious 逐条 minScore:1;inspector-issues 亦判红——CSP/弃用类问题只走 DevTools issue 通道,errors-in-console 与 0.9 分类阈值都看不见它;报告落 lighthouse-reports/) # Database (Prisma + SQLite) npm run db:seed # Seed the dev database npm run db:reset # Reset database with migrations ``` ## Architecture ### Tech Stack - **Next.js 16.3**(App Router)混合渲染 — SSG/ISR 静态页 + API routes;`output: 'standalone'`(`next.config.mjs:44`)、`distDir: 'dist'`(`:46`)。standalone 入口是 `node dist/standalone/server.js`,需另拷 `dist/static` 与 `public`;`npm run start`(= `next start`)在 standalone 下只算不受支持的降级路径,见下文 Build Output - **中间件命名为 `proxy.ts`**(Next 16 起 `middleware.ts` 更名);其 matcher 排除 `/api/*`,因此每个 API 路由必须自行做鉴权 - **React 18**, **TypeScript 5** (strict mode with `noUncheckedIndexedAccess`) - **Tailwind CSS 3** with design tokens exposed as CSS custom properties (all tokenized via `var()` references in `tailwind.config.js`) - **Framer Motion** for animations, **Lucide React** for icons, **Zod** for validation - **shadcn/ui** pattern (Radix UI + class-variance-authority + tailwind-merge) - **Prisma** with SQLite for backend data (admin, auth, CMS) ### Path Alias `@/` maps to `src/` — configured in both `tsconfig.json` (`paths`) and `jest.config.js` (`moduleNameMapper`). ### TypeScript Strictness Beyond `strict: true`, the project enables: - `noUncheckedIndexedAccess: true` — array/object index access returns `T | undefined`, requiring null checks. This is a significant constraint to be aware of when writing code. - `noImplicitReturns`, `noFallthroughCasesInSwitch`, `noUnusedLocals`, `noUnusedParameters` ### Route Structure (App Router) ``` src/app/ ├── layout.tsx # Root layout: fonts, metadata, theme, analytics, SEO schemas ├── (marketing)/ # Route group — all public marketing pages │ ├── layout.tsx # Shared: Header + Footer + PageTransition + ErrorBoundary │ ├── page.tsx # Home: server component, CMS zones via `lib/cms/data-server` → `home-content-v15.tsx` │ ├── about/ # About page (client.tsx) │ ├── news/ # News list + [slug] detail │ ├── contact/ # Contact form │ ├── products/ # Products hub (HSI model) │ │ ├── page.tsx # Product listing (enterprise suites + standalone) │ │ ├── [id]/ # Product detail (four-layer narrative) │ │ ├── standalone/[id]/ # Standalone product detail │ │ ├── erp-upgrade/ # Specific product landing pages │ │ └── erp-upgrade-v3/ │ ├── services/ # Services list + [id] detail │ ├── solutions/ # Solutions list + [id] detail (cross-references products) │ ├── cases/ # Case studies │ └── team/ # Team page ├── api/ │ ├── admin/ # Admin API │ ├── auth/ # Authentication API │ ├── cms/ # CMS API routes (draft mode, revalidation) │ └── contact/ # Contact form submission ├── privacy/, terms/ # Legal pages └── fonts/ # Local font files (Geist Sans/Mono) ``` ### Archiving Convention When replacing a page or component with a new version, move the old one to an `_archive/` subdirectory (e.g., `src/app/(marketing)/_archive/` for old homepage iterations). The archive is excluded from TypeScript compilation via `tsconfig.json`. ### Dark Mode Dark mode uses the `data-theme="dark"` HTML attribute (not Tailwind's `dark:` class). Tailwind is configured with `darkMode: ['variant', '[data-theme="dark"] &']` — use the **variant** form, not `['selector', ...]`, because the selector form is accepted by the config but JIT emits zero `dark:` rules under Turbopack + Next.js 16. **Three-state preference model** (`src/lib/theme.ts` is the single source of truth): | Stored value (`localStorage['novalon-theme']`) | Meaning | |---|---| | `'light'` / `'dark'` | Explicit user choice — always wins over the OS | | `'system'` / absent | Follow OS `prefers-color-scheme` (**default for new visitors**) | - `src/app/layout.tsx` has an inline `