Files
pangolin/web/website/README.md
T
wangjia 4df8dc6f87 feat(web): 官网 ui_kits/website → Astro 纯静态 SSG 迁移 (tsk_acMYQ-Z-EIF_)
新建 web/website/(Astro 零 SSR):

- 组件迁移:交互块保留 .jsx 经 @astrojs/react(Header/AnnouncementBar/SignupForm/PricingPlans),纯展示块转 .astro 去运行时 JS;像素以原型为基准。
- 令牌同源:build-tokens.mjs 从 design/colors_and_type.css 生成 tokens.gen.css,仅剔除第三方 Google Fonts @import,数值不改。
- 字体自托管:@fontsource(Sora/Manrope/Noto Sans SC/JetBrains Mono),无第三方 CDN。
- 图标:Lucide 构建期内联 SVG(替代 unpkg CDN),零运行时、零 CDN。
- i18n:/(zh)与 /en/(en)双路由单显,语言切换组件;文案沿用 ui_kits 脱敏文案。
- 安全:public/_headers 严格 CSP(全 self + 自托管资源 + 内联片段 sha256,无 unsafe-inline)+ HSTS;无支付表单。
- CI:.gitea/workflows/website.yml 构建(红线扫描+lint+CSP 哈希)→ 同时发布 Cloudflare Pages 主站与镜像。
- 灾备:README 写明干净环境 npm ci && npm run build 可直接部署到任意静态托管;dist 指纹确定性,主站镜像一致。

测试:npm run lint(0 error)/ npm test(build+CSP 哈希注入+红线扫描 0 命中)/ 两次干净构建 dist 指纹一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:26:40 +08:00

155 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 穿山甲 · Pangolin 官网(Astro 纯静态 / SSG
获客入口官网,从 `design/ui_kits/website/` 迁移到 **Astro 纯静态站点(零 SSR**
设计目标:极致加载性能 + **整站可秒级复制到任意备用域名**(灾备关键,doc/01 §1、doc/05)。
- 完全静态、无 API 依赖、无第三方统计/CDN(隐私承诺)。
- i18n 双路由单显:`/`(中文)、`/en/`(English),不并排(设计铁律 6)。
- 与后端全部任务解耦,可独立并行开发与部署。
---
## 快速开始
```bash
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` 下发严格 CSP`default-src 'self'``script-src`/`style-src`/`font-src`/
`img-src`/`connect-src` 全部收敛到 `self`(白名单仅 self + 自托管资源),外加 HSTS(含 preload)、
`X-Content-Type-Options``X-Frame-Options: DENY``Referrer-Policy``Permissions-Policy`
Astro island 水合产生的少量**内联** `<script>`/`<style>``postbuild``csp-hashes.mjs`
`'sha256-...'` 精确放行,**不开 `'unsafe-inline'`**。
curl 验证(部署后):
```bash
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)。
手动部署(任一项目):
```bash
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` 主站与镜像。需配 Secrets`CF_API_TOKEN` / `CF_ACCOUNT_ID` /
`CF_PAGES_PROJECT_MAIN` / `CF_PAGES_PROJECT_MIRROR`,并把 `runs-on` 改成实际 runner label。
> `_headers`CSP/HSTS)由 Cloudflare Pages 原生支持;换其它静态托管时需用各自机制下发等价头。
> 主站与镜像构建时 **SITE_URL 取同值**,使 canonical 始终指向主站(避免 SEO 竞争),
> 且两边产物 hash 完全一致。
### 主站 ↔ 镜像内容一致性校验
产物为内容哈希、构建确定性,相同源码 → 相同 `dist` → 相同指纹:
```bash
npm run hash:dist # 主站与镜像两路构建后比对此值,应完全相同
```
---
## 灾备重放(RTO 秒级)
从**任意干净环境**(仅需 Node + 本目录)即可重建并部署到**任意**静态托管/新域名:
```bash
# 1) 取本目录(git 仓库即备份)
git clone <repo> && cd <repo>/web/website
# 2) 干净安装 + 构建(无网络外部依赖,除 npm registry
npm ci
npm run build
# 3) 把 dist/ 整目录上传到任意静态托管 / 新 Pages 项目 / 对象存储+CDN,绑定新域名即可。
# 切换镜像/新域名只改「绑定」,无需改代码。
```
- 产物是纯相对路径静态文件,不依赖特定域名,可直接放到任意主机。
- 严格 CSP 全部 `self`,换域名无需调整。
- `_headers``dist/` 一并产出(Cloudflare Pages 自动识别)。
### 演练记录
- **2026-06-13**:在本仓库 worktree 干净环境执行 `npm ci && npm run build` 跑通,
`dist/` 产出 2 个页面(`/``/en/`+ 自托管字体 + `_headers`;两次连续干净构建
`npm run hash:dist` 指纹一致(`c1f6691e…cb65`),验证可复现 / 主站镜像一致性。
上传 `dist/` 到新静态托管为纯文件拷贝,<10 分钟可完成(受限于上传带宽,无构建/配置阻塞)。