- 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 产物装配与部署形态。
32 KiB
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 intailwind.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 returnsT | 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.tsxhas an inline<script>in<head>that resolves the preference and setsdata-themebefore first paint (FOUC prevention). It must stay semantically in sync withsrc/lib/theme.ts— change both together.src/components/theme/theme-toggle.tsxcycles跟随系统 → 浅色 → 深色. It listens tomatchMedia('(prefers-color-scheme: dark)')for live OS changes (only effective in thesystemstate) and tostorageevents for cross-tab sync.html[data-theme]is an output of preference resolution — never read it back as the user's preference, or thesystemstate collapses intodarkon 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:
- L1 Hero: Emotional entry with visual, title, value prop, status badge
- L2 Value Rationale: Product-specific — features/benefits for products, pain-points→architecture for solutions, challenges→results for services
- L3 Trust Proof: Case studies, data proofs, certifications, testimonials (currently stubbed — company is pre-launch)
- 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.tsxsolution-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,ThemeConfigdefinitionscontent-types.ts—CONTENT_TYPE_CONFIGS+registerAllContentTypes(): the per-model field schemas the admin form and the validator both readdata-server.ts— the read path:getPublishedItems(:37),getPublishedItemBySlug(:48),getPageZones(:70),getZone(:85),getHomePageCopy(:171), each wrapped in Reactcacheso a render pass hits the DB onceworkflow.ts/validate-content-data.ts/notifications.ts— status state machine, field validation, in-app notifications (status changes only throughworkflow.ts; seedocs/cms/api-contract.md§10.5)index.ts— re-exports the types plusCONTENT_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'sfontSize/lineHeight/letterSpacingscales -
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 inglobals.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 throughrgb(var(--x-rgb) / <alpha-value>). The arbitrary-value formbg-[var(--color-brand)]/20is accepted by the parser but emits no CSS (Tailwind v3 cannot apply an alpha modifier to a plain-hex custom property; seetailwind.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 usetext-brand-ink(flips to #F87171). Enforced byscripts/utils/check-brand-text-token.tson the text channel. -
Hover / translucent states are unguarded: axe skips
:hoverandcheck:contrastonly 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 againsttext-brand-inkin light theme (fail), at thebrand-soft12% 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 theCONTEXT.md§动效设计四原则 bands (entrance 180–280ms, ≤700ms ceiling, durations must flow through--transition-*tokens, bezier allow-list parsed from the CSS--ease-*tokens —--ease-outis an alias ofease-ink, and the token layer deliberately carries 5 further curves). It is deliberately not part ofcheck: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 definitionscases.ts— Case studies with industry filteringnavigation.ts— Nav structure + mega dropdown datacompany.ts,stats.ts,team.ts,news.ts,methodology.tshero-themes.ts— Per-product hero visual theme variantscross-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, whileapi/admin/*writes drafts andapi/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 inconfig/test/jest.config.js(global 75% stmts/lines/funcs, 82% branches) and are the single source of truth — rootjest.config.jsonly 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 withjsx: 'react-jsx'transform (since tsconfig uses'preserve'). Run single tests withnpx jest --testPathPattern="component-name". - Playwright (E2E): Tests in
e2e/, config ine2e/playwright.config.ts— run fromcd e2e(npm run testdoes this). webServer isHOSTNAME=localhost PORT=3000 node dist/standalone/server.jswhenE2E_TARGET=production(e2e/playwright.config.ts:132-135— build artifacts, real security headers; CI gate usesnpm run test:e2e:prod, andnpm run build+ thedist/static/publiccopies must already exist because the harness never builds) andnpm run devotherwise;reuseExistingServeris 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 ine2e/visual-snapshots/.mobile-*.spec.tsmust pin their own viewport because they also execute under the desktop projects. Touch-target gate lives ine2e/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 importstestfrome2e/fixtures.ts, whosecontextfixture injects an init script that removes thenext dev-only<nextjs-portal>devtools indicator (MutationObserver ondocument, becausedocumentElementis still null at document start). The indicator's shadow-DOM<footer class="error-overlay-footer">is pierced by Playwright's CSS engine and madelocator('footer')resolve to 2 elements (56 failures).devIndicators: falseis not a fix here — measured: the CSP-blocked-evaldev error keeps the overlay mounted, so the portal survives; don't reintroduce that env gate.visual-regression.spec.tsadditionally pins the cookie-consent state viaaddInitScript(constant timestamp) because the banner renders on a 2 s timer and thevisual-firefox-desktop/visual-webkit-desktopprojects 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 callexpectHydrated()frome2e/hydrated.tsbefore 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 needsexpect(...).toPass()re-driving the pointer, because a singlehover()can land mid-layout-transition andmouseenternever re-fires. Run single tests withnpx 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), theadmin/pages, and any ISR revalidation source.proxy.tsmatchers exclude/api/*, so each API route authenticates itself. - Serving: Nginx keeps a disk copy of the client static assets (
/_next/static/withtry_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 thenextjs_appupstream runningdist/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 fromdist/" is no longer true.assetPrefixis driven byCDN_DOMAIN. Container build isDockerfile.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 tofalsewould 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 previewboth runnext start -p 3000, which Next 16 warns "does not work with output: standalone" (warning only — see the Build & Preview note above). The supported server isdist/standalone/server.js;public/anddist/staticare copied into the standalone root automatically bypackage.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 todetail/completed)components/detail-v3/— another iteration (deleted, same reason)home-content-v2.tsxthroughhome-content-v11.tsx— archived homepage iterations in(marketing)/_archive/- The current canonical components are in
components/detail/andhome-content-cms.tsx