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>
This commit is contained in:
wangjia
2026-06-13 14:26:40 +08:00
parent 787151245e
commit 4df8dc6f87
37 changed files with 9390 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
---
/**
* Site.astro — 官网整页布局(构建期单显一种语言)。
* 两条路由复用:/ → lang="zh"/en/ → lang="en"(铁律 6 单显不并排)。
*
* 样式来源顺序(全部自托管,无第三方 CDN):
* @fontsource/* → 自托管字体 @font-face(替代原型的 Google Fonts CDN
* tokens.gen.css → 由 design/colors_and_type.css 生成(唯一令牌真相源)
* website.css → 逐字迁自 ui_kits/website
* site-extra.css → 仅承载「原型内联 style= → class」的等价工具类
*/
import '@fontsource/sora/500.css';
import '@fontsource/sora/600.css';
import '@fontsource/sora/700.css';
import '@fontsource/manrope/400.css';
import '@fontsource/manrope/500.css';
import '@fontsource/manrope/600.css';
import '@fontsource/manrope/700.css';
import '@fontsource/noto-sans-sc/400.css';
import '@fontsource/noto-sans-sc/500.css';
import '@fontsource/noto-sans-sc/700.css';
import '@fontsource/jetbrains-mono/400.css';
import '@fontsource/jetbrains-mono/500.css';
import '../styles/tokens.gen.css';
import '../styles/website.css';
import '../styles/site-extra.css';
import { createT, type Lang } from '../i18n/strings';
import Header from '../components/Header.jsx';
import AnnouncementBar from '../components/AnnouncementBar.jsx';
import Hero from '../components/Hero.astro';
import Features from '../components/Features.astro';
import WhySignup from '../components/WhySignup.astro';
import Pricing from '../components/Pricing.astro';
import Download from '../components/Download.astro';
import Docs from '../components/Docs.astro';
import Blog from '../components/Blog.astro';
import CtaBand from '../components/CtaBand.astro';
import Footer from '../components/Footer.astro';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = createT(lang);
const headerT = {
product: t('nav.product'),
pricing: t('nav.pricing'),
download: t('nav.download'),
docs: t('nav.docs'),
blog: t('nav.blog'),
login: t('nav.login'),
get: t('nav.get'),
suBtn: t('su.btn'),
};
---
<!doctype html>
<html lang={lang === 'zh' ? 'zh-CN' : 'en'}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{t('meta.title')}</title>
<meta name="description" content={t('meta.desc')} />
<meta name="robots" content="index,follow" />
<link rel="canonical" href={new URL(lang === 'zh' ? '/' : '/en/', Astro.site).href} />
<link rel="alternate" hreflang="zh-CN" href={new URL('/', Astro.site).href} />
<link rel="alternate" hreflang="en" href={new URL('/en/', Astro.site).href} />
<link rel="alternate" hreflang="x-default" href={new URL('/', Astro.site).href} />
<meta name="theme-color" content="#B96A3D" />
<meta property="og:type" content="website" />
<meta property="og:title" content={t('meta.title')} />
<meta property="og:description" content={t('meta.desc')} />
<meta property="og:locale" content={lang === 'zh' ? 'zh_CN' : 'en_US'} />
</head>
<body>
<AnnouncementBar client:idle text={t('ann.txt')} cta={t('ann.cta')} />
<Header client:load lang={lang} t={headerT} />
<a id="top"></a>
<Hero t={t} />
<Features t={t} />
<WhySignup t={t} />
<Pricing t={t} />
<Download t={t} />
<Docs t={t} />
<Blog t={t} />
<CtaBand t={t} />
<Footer t={t} />
</body>
</html>