Files
pangolin/web/website
wangjia dae321a3f0 fix(web): clay-500 品牌主色 #FF0000 → #B96A3D + 固定 next 14.2.35
- token 重生成:--clay-500 品牌主色从占位红 #FF0000 还原为 clay #B96A3D
  (usercenter public + website tokens.gen)
- usercenter package-lock:next / eslint-config-next 从 <14.3.0 固定到 14.2.35

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JEHzjEcFzvGwgbxT6Wbt6c
2026-06-21 21:34:22 +08:00
..

穿山甲 · Pangolin 官网(Astro 纯静态 / SSG

获客入口官网,从 design/ui_kits/website/ 迁移到 Astro 纯静态站点(零 SSR。 设计目标:极致加载性能 + 整站可秒级复制到任意备用域名(灾备关键,doc/01 §1、doc/05)。

  • 完全静态、无 API 依赖、无第三方统计/CDN(隐私承诺)。
  • i18n 双路由单显:/(中文)、/en/(English),不并排(设计铁律 6)。
  • 与后端全部任务解耦,可独立并行开发与部署。

快速开始

cd web/website
npm ci            # 干净、可复现安装(含自托管字体 @fontsource、Astro、React
npm run dev       # 本地预览 http://localhost:4321
npm run build     # 产物到 dist/(含 token 同源生成 + CSP 哈希注入)
npm run preview   # 预览构建产物

要求 Node ^18.20.8 || ^20.3.0 || >=22.0.0

常用脚本

命令 作用
npm run build 构建静态产物到 dist/prebuild 生成令牌,postbuild 注入 CSP 哈希)
npm run lint astro check 类型检查
npm run scan:redline 扫描 dist/ 红线词(脱敏铁律 13
npm run hash:dist 打印 dist/ 确定性指纹(主站/镜像一致性比对)
npm test 一键:生成令牌 → 构建 → CSP 哈希 → 红线扫描

目录结构

web/website/
├─ src/
│  ├─ layouts/Site.astro        整页布局(按 lang 单显),两路由复用
│  ├─ pages/index.astro         /     → lang="zh"
│  ├─ pages/en/index.astro      /en/  → lang="en"
│  ├─ components/
│  │  ├─ *.jsx                  交互组件(React island@astrojs/react):
│  │  │                         Header / AnnouncementBar / SignupForm / PricingPlans
│  │  ├─ *.astro                纯展示块(构建期渲染,零运行时 JS)
│  │  ├─ Icon.astro             Lucide 图标 → 构建期内联 SVG(替代原型的 unpkg CDN
│  │  └─ Brand.astro            品牌「行走穿山甲」内联 SVG
│  ├─ i18n/strings.ts           中/EN 字典(逐字沿用 ui_kits/website/site.js 脱敏文案)
│  └─ styles/
│     ├─ tokens.gen.css         由 design/colors_and_type.css 生成(见下)
│     ├─ website.css            逐字迁自 ui_kits/website/website.css
│     └─ site-extra.css         仅承载「原型内联 style= → 等价 class」
├─ public/_headers              Cloudflare Pages 安全头(严格 CSP + HSTS
├─ scripts/
│  ├─ build-tokens.mjs          令牌同源生成器
│  ├─ csp-hashes.mjs            构建后注入内联片段 sha256 到 CSP
│  ├─ check-redline.mjs         红线词扫描
│  └─ dist-hash.mjs             产物指纹
└─ .gitea/workflows/website.yml CI:构建+校验 → 同时发布主站与镜像

关键决策(务必理解)

1. 令牌同源(单一真相源)

颜色/字体/圆角/阴影等令牌的唯一真相源是仓库根 design/colors_and_type.css(铁律 1)。 scripts/build-tokens.mjs 在每次构建前把它读入并写出 src/styles/tokens.gen.css 只移除其中加载 Google Fonts 的 @import url('https://fonts.googleapis.com/...') 一行 (任务硬性要求:不引第三方字体 CDN)。令牌数值一字未改,仍是单一来源、可随时再生。

若只拷贝了 web/website/ 子目录到干净环境(源文件缺失),生成器自动沿用已提交的 tokens.gen.css,构建照常。

2. 字体全部自托管

原型用 Google Fonts CDN;本站改为 @fontsource/* 本地打包 Sora / Manrope / Noto Sans SC / JetBrains Mono),woff2 随 npm ci 落地、 经构建产出到 dist/_astro/,由站点自身提供(font-src 'self')。 无任何第三方 CDN 请求 —— 性能 + 隐私 + 可达性。

3. 安全头(严格 CSP / HSTS

public/_headers 下发严格 CSPdefault-src 'self'script-src/style-src/font-src/ img-src/connect-src 全部收敛到 self(白名单仅 self + 自托管资源),外加 HSTS(含 preload)、 X-Content-Type-OptionsX-Frame-Options: DENYReferrer-PolicyPermissions-Policy。 Astro island 水合产生的少量内联 <script>/<style>postbuildcsp-hashes.mjs'sha256-...' 精确放行,不开 'unsafe-inline'

curl 验证(部署后):

curl -sI https://<域名>/ | grep -iE 'content-security-policy|strict-transport-security'

4. 脱敏红线(铁律 13

任何页面不得出现红线词(VPN/翻墙/科学上网/突破封锁/自由穿越/Go anywhere)。 scripts/check-redline.mjs 扫描 dist/**/*.html(含正文、属性、aria-label、meta), 例外白名单与仓库 ci/scan-redline.sh 一致(外部渠道 handle @PangolinVPN_bot/@pangolinvpn)。 CI 命中即失败。演示渠道/价格为占位,落地替换(见 design/CLAUDE.md §8)。

5. 无支付表单

定价区 CTA 一律「获取激活码 / 免费下载」,仅引导到外部渠道,页面内无任何支付表单(铁律 10)。


部署:Cloudflare Pages 主站 + ≥1 活跃镜像

主站与镜像各是一个 Cloudflare Pages 项目,部署同一份 dist/,绑定不同域名 (域名归属 #16 域名池体系)。镜像用于「主域名被墙」场景(doc/05 Runbook)。

手动部署(任一项目):

cd web/website
npm ci && npm run build
npx wrangler pages deploy dist --project-name <PAGES_PROJECT> --branch main

CI.gitea/workflows/website.yml):push web/website/** → 构建(红线 + lint + CSP 哈希) → 同时 wrangler pages deploy 主站与镜像。需配 SecretsCF_API_TOKEN / CF_ACCOUNT_ID / CF_PAGES_PROJECT_MAIN / CF_PAGES_PROJECT_MIRROR,并把 runs-on 改成实际 runner label。

_headersCSP/HSTS)由 Cloudflare Pages 原生支持;换其它静态托管时需用各自机制下发等价头。 主站与镜像构建时 SITE_URL 取同值,使 canonical 始终指向主站(避免 SEO 竞争), 且两边产物 hash 完全一致。

主站 ↔ 镜像内容一致性校验

产物为内容哈希、构建确定性,相同源码 → 相同 dist → 相同指纹:

npm run hash:dist     # 主站与镜像两路构建后比对此值,应完全相同

灾备重放(RTO 秒级)

任意干净环境(仅需 Node + 本目录)即可重建并部署到任意静态托管/新域名:

# 1) 取本目录(git 仓库即备份)
git clone <repo> && cd <repo>/web/website
# 2) 干净安装 + 构建(无网络外部依赖,除 npm registry
npm ci
npm run build
# 3) 把 dist/ 整目录上传到任意静态托管 / 新 Pages 项目 / 对象存储+CDN,绑定新域名即可。
#    切换镜像/新域名只改「绑定」,无需改代码。
  • 产物是纯相对路径静态文件,不依赖特定域名,可直接放到任意主机。
  • 严格 CSP 全部 self,换域名无需调整。
  • _headersdist/ 一并产出(Cloudflare Pages 自动识别)。

演练记录

  • 2026-06-13:在本仓库 worktree 干净环境执行 npm ci && npm run build 跑通, dist/ 产出 2 个页面(//en/+ 自托管字体 + _headers;两次连续干净构建 npm run hash:dist 指纹一致(c1f6691e…cb65),验证可复现 / 主站镜像一致性。 上传 dist/ 到新静态托管为纯文件拷贝,<10 分钟可完成(受限于上传带宽,无构建/配置阻塞)。