diff --git a/web/website/.gitea/workflows/website.yml b/web/website/.gitea/workflows/website.yml new file mode 100644 index 0000000..195febb --- /dev/null +++ b/web/website/.gitea/workflows/website.yml @@ -0,0 +1,103 @@ +name: website + +# 官网 CI/CD:push web/website/** → 构建(红线词扫描 + lint + CSP 哈希)→ 同时发布主站与镜像。 +# 与仓库根 .gitea/workflows/ci.yml 解耦:官网完全静态、与后端无依赖,可独立并行。 +# +# 需在 Gitea 配置 Secrets: +# CF_API_TOKEN Cloudflare API Token(Pages:Edit 权限) +# CF_ACCOUNT_ID Cloudflare Account ID +# CF_PAGES_PROJECT_MAIN 主站 Pages 项目名 +# CF_PAGES_PROJECT_MIRROR 镜像 Pages 项目名(≥1 个活跃镜像,应对主域名被墙) +# 并把 runs-on 改成实际 runner label。 + +on: + push: + paths: + - 'web/website/**' + - '.gitea/workflows/website.yml' + pull_request: + paths: + - 'web/website/**' + workflow_dispatch: + +env: + # 主站 canonical 域名;镜像也用同一值 → canonical 指向主站 + 产物 hash 一致。 + SITE_URL: https://pangolin.example + +jobs: + # ── 构建 + 校验(红线 / lint / CSP / 指纹) ────────────────────────────── + build: + name: Build & Verify + runs-on: nas + defaults: + run: + working-directory: web/website + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install (clean, reproducible) + run: npm ci + + - name: Type-check (lint) + run: npm run lint + + - name: Build (含 token 同源 + CSP 哈希注入) + run: npm run build + + - name: Redline scan (脱敏铁律 13) + run: npm run scan:redline + + - name: CSP sanity (禁止 unsafe-inline) + run: grep -L "unsafe-inline" dist/_headers + + - name: Fingerprint (主站/镜像一致性基线) + run: npm run hash:dist | tee dist.fingerprint + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: website-dist + path: web/website/dist + + # ── 同时发布主站与镜像(同一份 dist,hash 必然一致) ───────────────────── + deploy: + name: Deploy main + mirror + needs: build + if: github.event_name == 'push' + runs-on: nas + defaults: + run: + working-directory: web/website + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install + run: npm ci + + - name: Build + run: npm run build + + - name: Deploy → 主站 (Cloudflare Pages) + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} + run: npx wrangler pages deploy dist --project-name "${{ secrets.CF_PAGES_PROJECT_MAIN }}" --branch main + + - name: Deploy → 镜像 (Cloudflare Pages) + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} + run: npx wrangler pages deploy dist --project-name "${{ secrets.CF_PAGES_PROJECT_MIRROR }}" --branch main + + - name: Verify main == mirror (内容一致性) + run: | + echo "主站与镜像部署的是同一份 dist,指纹:" + npm run --silent hash:dist diff --git a/web/website/.gitignore b/web/website/.gitignore new file mode 100644 index 0000000..ff13948 --- /dev/null +++ b/web/website/.gitignore @@ -0,0 +1,12 @@ +# build output +dist/ +# deps +node_modules/ +# astro +.astro/ +# logs / env +*.log +.env +.env.production +# OS +.DS_Store diff --git a/web/website/README.md b/web/website/README.md new file mode 100644 index 0000000..b631105 --- /dev/null +++ b/web/website/README.md @@ -0,0 +1,154 @@ +# 穿山甲 · 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 水合产生的少量**内联** `