4df8dc6f87
新建 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>
155 lines
7.5 KiB
Markdown
155 lines
7.5 KiB
Markdown
# 穿山甲 · 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 分钟可完成(受限于上传带宽,无构建/配置阻塞)。
|