Files
novalon-website/CLAUDE.md
T
zhangxiang a0328a623f chore(qa): 验收台账与证据入库 + 构建/部署配置同步
- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
2026-09-28 10:48:09 +08:00

32 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Common Commands

# 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 <script> in <head> that resolves the preference and sets data-theme before first paint (FOUC prevention). It must stay semantically in sync with src/lib/theme.ts — change both together.
  • src/components/theme/theme-toggle.tsx cycles 跟随系统 → 浅色 → 深色. It listens to matchMedia('(prefers-color-scheme: dark)') for live OS changes (only effective in the system state) and to storage events for cross-tab sync.
  • html[data-theme] is an output of preference resolution — never read it back as the user's preference, or the system state collapses into dark on dark-OS machines and the cycle deadlocks.

HSI Information Architecture

The site follows a Hub-Spoke-Independent model (see CONTEXT.md and ADR-0002):

  • Hub: /products — product catalog, split into "企业套装" (6 enterprise products) and "专业产品" (standalone)
  • Spoke: /solutions — industry scenarios, each recommending product combinations from the Hub
  • Independent: Standalone products in the specialized zone have their own narrative path

Four-Layer Narrative Model

Every detail page (product/service/solution/standalone) follows the same four-layer structure:

  1. L1 Hero: Emotional entry with visual, title, value prop, status badge
  2. L2 Value Rationale: Product-specific — features/benefits for products, pain-points→architecture for solutions, challenges→results for services
  3. L3 Trust Proof: Case studies, data proofs, certifications, testimonials (currently stubbed — company is pre-launch)
  4. L4 CTA Conversion: Primary action + secondary action + cross-recommendations

The detail components implementing this live in src/components/detail/:

  • detail-hero.tsx, detail-product-value.tsx, detail-trust-section.tsx, detail-cta-section.tsx
  • solution-value.tsx, service-value.tsx (type-specific L2 variants)
  • detail-cross-recommend.tsx (cross-links between products↔solutions↔services)

CMS Content Architecture

src/lib/cms/ is a real, server-side CMS layer over Prisma — there is no mock layer and no client-side CMS SDK (the historical names mock-home.ts, ContentZoneRenderer.tsx, component-registry.ts, src/components/cms/ do not exist in the tree; do not look for them):

  • types.ts — FieldType/FieldDefinition, ContentModel, ContentStatus, ContentItem, ContentZone, ThemeConfig definitions
  • content-types.ts — CONTENT_TYPE_CONFIGS + registerAllContentTypes(): the per-model field schemas the admin form and the validator both read
  • data-server.ts — the read path: getPublishedItems (:37), getPublishedItemBySlug (:48), getPageZones (:70), getZone (:85), getHomePageCopy (:171), each wrapped in React cache so a render pass hits the DB once
  • workflow.ts / validate-content-data.ts / notifications.ts — status state machine, field validation, in-app notifications (status changes only through workflow.ts; see docs/cms/api-contract.md §10.5)
  • index.ts — re-exports the types plus CONTENT_TYPE_CONFIGS

The homepage is src/app/(marketing)/page.tsx (server component, export const revalidate = 3600 at :5): it resolves the home zones with getResolvedHomeZones() (:8) and, when the CMS has nothing configured, falls back to per-model getPublishedItems(...) calls (:23-29); the markup lives in home-content-v15.tsx. Earlier documents calling it home-content-cms.tsx are stale.

Design Token System

All visual tokens are defined as CSS custom properties in src/app/globals.css (:root block) and mapped into Tailwind's config via var() references:

  • Colors: --color-ink (deep charcoal #0A0E14), --color-brand (vermilion #C41E3A), accent colors for service-coding (blue, teal, amber, purple), functional colors (success, warning, error, info), dark-section overrides

  • Typography: --font-size-*, --line-height-*, --letter-spacing-* all mapped to Tailwind's fontSize/lineHeight/letterSpacing scales

  • Spacing, radius, shadows all tokenized through CSS variables

  • Transitions: --transition-fast/normal/slow, --ease-ink (cubic-bezier), --ease-sharp(--ease-spring* 死令牌已于 2026-09-19 polish 删除)

  • Dark mode: data-theme="dark" attribute (set via inline script before paint to prevent flash); flipped by the [data-theme='dark'] override block in globals.css, which only re-maps CSS variables — components do not restyle per element

  • Opacity: use theme tokens (bg-brand/20, border-ink/30) — they resolve through rgb(var(--x-rgb) / <alpha-value>). The arbitrary-value form bg-[var(--color-brand)]/20 is accepted by the parser but emits no CSS (Tailwind v3 cannot apply an alpha modifier to a plain-hex custom property; see tailwind.config.js:12-15), so it fails silently. Verify any "is this style live?" claim against the compiled stylesheet, not against the source class list.

  • Two-channel red: --color-brand (#C41E3A) is for surfaces only and does not flip in dark mode; text must use text-brand-ink (flips to #F87171). Enforced by scripts/utils/check-brand-text-token.ts on the text channel.

  • Hover / translucent states are unguarded: axe skips :hover and check:contrast only asserts declared token pairs, so a hover background nobody noticed was dead becomes a live AA failure the moment you fix it. Composite it yourself — a semi-transparent layer replaces the element's own base color: alpha*brand + (1-alpha)*pageBg; at 20% that yields 4.18:1 against text-brand-ink in light theme (fail), at the brand-soft 12% token 4.80:1 (pass).

  • Motion tokens are gated, and the gate is currently RED: scripts/utils/check-motion-constraints.ts (npm run check:motion, 配套 npm run check:motion:test) enforces the CONTEXT.md §动效设计四原则 bands (entrance 180–280ms, ≤700ms ceiling, durations must flow through --transition-* tokens, bezier allow-list parsed from the CSS --ease-* tokens — --ease-out is an alias of ease-ink, and the token layer deliberately carries 5 further curves). It is deliberately not part of check:a11y: the tree measures 23 violations / 13 files, so wiring it in would make the aggregate permanently red. scripts/** is eslint-ignored, so adding it did not move the lint baseline.

Design DNA Framework

The site follows a three-dimensional design system (see CONTEXT.md):

  • Dimension 1 — Design System: Quantifiable tokens (colors, typography, spacing, radius, shadows, motion, components)
  • Dimension 2 — Design Style: Qualitative perception (atmosphere, visual language, composition, imagery, brand tone)
  • Dimension 3 — Visual Effects: Scroll animations, micro-interactions, parallax, SVG effects

The Consulting Professional aesthetic (inspired by Accenture + Bain) is the primary skeleton. Ink cultural elements (水墨) are secondary decorations limited to ≤6 locations (logo, dividers, transition animations, footer texture).

Brand red (#C41E3A) usage rule: Every page must have ≥3 brand-red touchpoints. It must never be used as paragraph text color, as a large background, or alongside accent colors in the same card. Area ≤10%. Exception: full-bleed dark-red statement/CTA blocks use the separate crimson-veil (#8B1530) token — see DESIGN.md「Crimson Veil」(rule clarified 2026-09-19 critique to resolve this apparent contradiction).

Motion design: Animations are purposeful (not decorative). Durations come from tokens, not from a free-form "150-300ms" range — that phrasing conflicted with CONTEXT.md「动效设计四原则」and is replaced by its three tiers: 入场 180–280ms(--transition-fast: 180ms … --transition-normal: 280ms,src/app/globals.css:264-265)· hover 150ms · 反馈 100ms;--transition-slow: 450ms / --transition-slower: 700ms 只用于揭示型动效。ease-ink [0.22, 1, 0.36, 1](globals.css:280)为默认缓动,children stagger 30-60ms、Section 间 stagger 100-150ms。No continuous looping animations except pulse-soft for skeletons. No spring easings for content entry(spring 仅用于按钮按压反馈). No entry animations >700ms. Note that duration-300 一类的 Tailwind 预设时长绕过令牌、且 300ms 不属任何档位(残留清点见 CONTEXT.md「duration-300 → 动效令牌」行,该项 in flight,勿引用其计数)。

Data Layer

Most structured marketing content is still src/lib/constants/ TypeScript constants, and the pages render from them; on top of that, published CMS items now override specific slots — the home page's hero/services/cases/stats/news data comes from lib/cms/data-server (Prisma) and only falls back to constants when the CMS has nothing configured (src/app/(marketing)/page.tsx:8-36), and per-section copy comes from getHomePageCopy() with in-component fallbacks. "Marketing content has no database" is therefore no longer true:

  • products.ts — 6 enterprise products (ERP, CRM, CMS, BI, SDS, OA) + standalone products (NovaVis)
  • services.ts, solutions.ts — Service and solution definitions
  • cases.ts — Case studies with industry filtering
  • navigation.ts — Nav structure + mega dropdown data
  • company.ts, stats.ts, team.ts, news.ts, methodology.ts
  • hero-themes.ts — Per-product hero visual theme variants
  • cross-references.ts — Cross-links between products/solutions/services

Each product/solution/service implements the Product/Solution/Service interface which includes caseStudies[], dataProofs[], certifications[], etc. for the four-layer narrative.

Backend Layer (Prisma + API Routes)

The project has a backend layer for admin/auth/CMS functionality:

  • Prisma with SQLite (prisma/dev.db) for admin user data, auth, and CMS content management
  • API routes at src/app/api/admin/, auth/, cms/, contact/
  • The marketing read path and the admin write path share this backend: pages fetch published items through src/lib/cms/data-server.ts, while api/admin/* writes drafts and api/cms/* (draft/enable, draft/disable, revalidate) handles preview cache invalidation

Error Monitoring (Sentry)

@sentry/nextjs@10 is wired through src/instrumentation.ts (server/edge, incl. onRequestError) and src/instrumentation-client.ts (client init + onRouterTransitionStart); next.config.mjs exports withSentryConfig(nextConfig). This Next 16 build defaults to Turbopack, which never loads the root sentry.client.config.ts — client init therefore lives in instrumentation-client.ts, and the old file is kept as an empty stub solely to prevent a double Sentry.init on the --webpack path. Everything is gated on NEXT_PUBLIC_SENTRY_DSN: absent it, Sentry.init is skipped, both hooks no-op, and the CSP is byte-identical (connect-src/worker-src only gain the DSN's protocol//host when a valid DSN is present, next.config.mjs:17-26). Verify "is this inert?" by building without a DSN and diffing the emitted header, not by reading the source.

Component Organization

src/components/
├── ui/              # Base: Button, Card, Input, Badge, ScrollReveal, AnimatedCounter, etc.
├── layout/          # Header, Footer, MobileTabBar, Breadcrumb, MegaDropdown(MobileMenu 已删除,抽屉内置于 Header)
├── sections/        # Page section components: HeroSectionV2, ServiceGrid, CTASection, etc.
├── detail/          # Four-layer narrative: DetailHero, ProductValueSection, DetailTrustSection, etc.
├── content/         # 仅 sections.tsx(+ 同名 .test.tsx)
├── admin/           # admin-layout.tsx、auth-context.tsx(React Context 在此,不在 src/contexts/)
├── theme/           # theme-toggle.tsx
├── seo/             # Structured data (OrganizationSchema, WebsiteSchema, etc.)
└── analytics/       # GA4, error tracking, cookie consent, scroll depth, outbound links

口径核对(2026-09-23,验收 N-33「不存在的目录」一项):cms/、providers/、effects/ 三个目录在工作树与 git ls-tree -r HEAD 中均不存在,旧图里的对应条目是残留。ContentRenderer / SectionRenderer / FieldRenderer 三个标识符在 src/**/*.{ts,tsx} 中零命中,CMS 区块渲染实际由 src/components/content/sections.tsx + src/lib/cms/ 承担;effects/ 一族已在提交 37296b5(2026-05-10)删除,见 CONTEXT.md「特效组件」。

Testing Setup

  • Jest (unit): Tests alongside source in src/**/*.test.{ts,tsx}, coverage thresholds live in config/test/jest.config.js (global 75% stmts/lines/funcs, 82% branches) and are the single source of truth — root jest.config.js only re-exports it, never duplicate thresholds. Measured values are recorded in exactly one place: docs/development/quality-gates.md §3 (2026-09-23 本树复跑:134 suites / 1697 tests / EXIT=0);该文件顶部注释里的百分比是滞后快照,引用前先复跑。Uses @/ path alias and ts-jest with jsx: 'react-jsx' transform (since tsconfig uses 'preserve'). Run single tests with npx jest --testPathPattern="component-name".
  • Playwright (E2E): Tests in e2e/, config in e2e/playwright.config.ts — run from cd e2e (npm run test does this). webServer is HOSTNAME=localhost PORT=3000 node dist/standalone/server.js when E2E_TARGET=production (e2e/playwright.config.ts:132-135 — build artifacts, real security headers; CI gate uses npm run test:e2e:prod, and npm run build + the dist/static / public copies must already exist because the harness never builds) and npm run dev otherwise; reuseExistingServer is disabled for production targets, so a busy :3000 aborts the run. Functional projects: chromium, chromium-mobile (iPhone 14, 390×844), firefox, webkit — plus 5 visual-regression projects at desktop/tablet/mobile breakpoints; snapshots in e2e/visual-snapshots/. mobile-*.spec.ts must pin their own viewport because they also execute under the desktop projects. Touch-target gate lives in e2e/touch-targets.ts (AA SC 2.5.8 ≥24px hard, AAA SC 2.5.5 44px advisory). Visual snapshots are server-target independent — every spec imports test from e2e/fixtures.ts, whose context fixture injects an init script that removes the next dev-only <nextjs-portal> devtools indicator (MutationObserver on document, because documentElement is still null at document start). The indicator's shadow-DOM <footer class="error-overlay-footer"> is pierced by Playwright's CSS engine and made locator('footer') resolve to 2 elements (56 failures). devIndicators: false is not a fix here — measured: the CSP-blocked-eval dev error keeps the overlay mounted, so the portal survives; don't reintroduce that env gate. visual-regression.spec.ts additionally pins the cookie-consent state via addInitScript (constant timestamp) because the banner renders on a 2 s timer and the visual-firefox-desktop / visual-webkit-desktop projects use an empty storage state — without the pin, slower engines photograph the banner into the first viewport and faster ones don't (34 engine-specific failures). Client-component interactions must call expectHydrated() from e2e/hydrated.ts before hover/click — SSR HTML is visible ~1.7 s before React attaches handlers, and pointer events in that window are dropped without replay. Hover-driven UI additionally needs expect(...).toPass() re-driving the pointer, because a single hover() can land mid-layout-transition and mouseenter never re-fires. Run single tests with npx playwright test --grep "test name".

Build Output

The project builds to dist/ (distDir in next.config.mjs:46) with output: 'standalone' (next.config.mjs:44) — this is a hybrid render model, not a static export (output: 'export' was dropped and the project has since moved on to standalone; CONTEXT.md「生产部署模式」records the 2026-08 switch). Concretely:

  • Prerendered/ISR: the marketing pages, privacy, terms, sitemap.ts, robots.ts.
  • Runtime-backed: src/app/api/** (20 route files — admin/*, auth/*, cms/*, contact), the admin/ pages, and any ISR revalidation source. proxy.ts matchers exclude /api/*, so each API route authenticates itself.
  • Serving: Nginx keeps a disk copy of the client static assets (/_next/static/ with try_files … @nextjs, nginx-static-production.conf:151-157) plus /uploads/ hardening, and proxies /api/ (:213), /admin (:226) and everything else (location / :238-240, @nextjs :249) to the nextjs_app upstream running dist/standalone/server.js. Page HTML no longer has a static-file fast path — it was removed on purpose (nginx-static-production.conf:20-25), so "nginx serves the pages from dist/" is no longer true. assetPrefix is driven by CDN_DOMAIN. Container build is Dockerfile.prod + docker-compose.server.yml.
  • Images are unoptimized (next.config.mjs:48-61, value at :58) because distribution goes through Nginx + CDN, and flipping it to false would require every environment to have a Node runtime taking over /_next/image (otherwise every image 404s). The reason is deployment topology, not a static-export limitation.
  • Boot command: npm run start / npm run preview both run next start -p 3000, which Next 16 warns "does not work with output: standalone" (warning only — see the Build & Preview note above). The supported server is dist/standalone/server.js; public/ and dist/static are copied into the standalone root automatically by package.json:9 的 postbuild,所以 npm run build 之后直接 node dist/standalone/server.js 即可。该启动方式仍没有包成 npm script —— CI 在 Jenkinsfile:353-355 复用产物时重做同套拷贝,发布侧由 scripts/deploy.sh:162-175 把 public/ 与 dist/static → dist/_next/static 同步进 Nginx 站点根。

Commit Convention

Uses Conventional Commits with commitlint (@commitlint/config-conventional). Husky + lint-staged enforces linting on pre-commit.

Submission Flow (PR-First)

dev/main are only reached via feature branch + PR: sync origin/dev → rebase → push → PR gate → merge by Rebase (linear history, no merge commit). bash scripts/check-pr-checklist.sh <pr-description-file> must pass before opening a PR — it verifies .gitea/PULL_REQUEST_TEMPLATE.md has the three required sections (全链路检查 / 测试分层检查 / 质量门禁) with ≥20 checklist items, and that every item in the PR body is checked (mark irrelevant ones as N/A:<reason>). Rebase rewrites hashes: an already-pushed branch may only be updated with --force-with-lease. Always rebase onto origin/dev, never a possibly-stale local dev.

Component Versioning History

The project has gone through multiple design iterations. Older component versions:

  • components/detail-v2/ — previous iteration (deleted per git status, migration to detail/ completed)
  • components/detail-v3/ — another iteration (deleted, same reason)
  • home-content-v2.tsx through home-content-v11.tsx — archived homepage iterations in (marketing)/_archive/
  • The current canonical components are in components/detail/ and home-content-cms.tsx