Files
cms-website/docs/superpowers/specs/2026-10-20-siteforge-vertical-slice-design.md
T
浅念 d60123ba41 docs: SiteForge 规划文档基线 + 纵向切片设计规格 V1
- V3.0 总体方案、主题与板块设计、实施计划
- cms 现状差距分析 V2.0(V1.0 已标注作废)与复用评估 V1.0
- 纵向切片设计规格(路线 C:全新架构 + 白名单资产移植)
- .gitignore 先行(storage/artifacts/env 永不入库)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 16:10:34 +08:00

250 lines
17 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.
# SiteForge 最小纵向切片 · 设计文档
> 日期:2026-10-20|状态:待评审
> 上位文档:`SiteForge_完整项目计划方案_V3.0.md`(执行基线)
> 姊妹文档:`cms现状与SiteForge差距分析_V1.0.md`(D:\project\cms 的教训清单)
> 本文决策已经用户逐项确认(建设顺序、切片范围、数据、路线、九项修正)。
---
## 1. 背景与定位
SiteForge 是多站点静态迁移与发布平台(V3.0 定义 17 项必须交付能力)。本切片是整个平台的**第一个里程碑**:用真实试点数据(hesupipe.com 的 WordPress WXR 导出 + 图片包副本)打通最险的核心链路一次:
```text
WXR 导入 → 统一内容模型 → URL 注册表 → 极简主题 → 静态构建 → 门禁 → 本地预览
```
三条前提约束:
1. **所有代码是正式资产**:数据模型、契约、目录结构按 V3.0 设计,后续能力在此之上加厚,不推倒重写。
2. **不改动 D:\project\cms,但白名单资产可移植**(路线 C,2026-10-20 确认):cms 冻结为只读源;试点数据以只读复制方式进入本仓库 `storage/`。**可移植资产(白名单)**:WXR 解析规则与测试夹具(重写到流式 XML 解析器之上)、Astro 页面模板(作为 pilot-minimal 主题起点——其产品 Tabs/相册/表格刚对齐过 hesupipe 老站)、门禁脚本逻辑、docker-compose/CI 配置。**不继承、按 V3.0 重写**:认证、BuildService、release/rollback 脚本、MigrationService 编排(迁移即发布)、settings JSONB 路由、Lead 服务、后台业务页面。
3. **完整性由清单驱动**:每完成一个阶段对照 17 项能力清单打钩,全绿才是项目完成;本切片覆盖其中 4 项的骨架(WordPress 导入、统一内容与修订、URL 注册表、Astro 静态构建)。
**文档权威关系**:`SiteForge_完整项目计划方案_V3.0.md` 是唯一产品与架构基线;`cms现状与SiteForge差距分析_V2.0.md` 是 cms 现状与改造优先级依据(**V1.0 差距分析已作废**);`SiteForge_现有CMS项目复用评估_V1.0.md` 是复用边界参考。
## 2. 目标与非目标
### 2.1 目标(本切片交付)
| # | 交付 | 验收口径 |
|---|---|---|
| G1 | WXR 全量导入管道(幂等,导入不发布) | hesupipe 全量导入成功(全部为 draft);显式 `publish` 后转 published;重复导入第二次产生 0 条新修订 |
| G2 | 统一内容模型 | 产品/新闻/页面落库,不可变修订 + externalRef 幂等键 + checksum |
| G3 | URL 注册表与路由策略 | 路径级保真 + 分类/归档/标签处置;URL 对照报告 🔴=0 |
| G4 | 极简主题静态构建 | 全站页面 + sitemap.xml + robots.txt + 媒体旧路径输出 |
| G5 | 门禁 | 死链=0、sitemap 一致、每页 title/canonical/唯一 H1、产物大小写冲突=0 |
| G6 | 本地预览服务器 | 按 route_registry 真实响应(html/301/404),noindex 头 |
### 2.2 非目标(明确不做,后续阶段做)
后台管理 UI、部署器(rsync/OSS)、差量同步与冲突队列(本切片只预留 `override` 字段)、SEO 三套数据 diff(P0–P3)、爬虫/Playwright、多语言(en 单语)、表单服务、MFA/认证体系(本地脚本无 HTTP 暴露)、Redis/BullMQ(任务先进程内)。
## 3. 技术选型与决策记录
| 决策 | 选择 | 理由(ADR 摘要,正式 ADR 实施时落 docs/adr/) |
|---|---|---|
| D1 ORM | Drizzle + PostgreSQL 16 | cms 实战验证;迁移即 SQL 可审查;SQL 控制力强(ADR-001) |
| D2 任务执行 | 进程内 JobRunner,接口抽象 | 切片无长任务并发需求;接口按 job 语义设计,BullMQ 后置替换不死代码(ADR-002) |
| D3 媒体存储 | 本地磁盘 `storage/`,产物保持旧路径 | cms 教训:MinIO 容器起了没人用;对象存储后置(ADR-003) |
| D4 URL 策略 | 路径级保真优先,301 兜底 | V3.0 §6/§7 要求;详见 §5(ADR-004) |
| D5 服务形态 | NestJS 模块化单体 + CLI 脚本复用服务层 | 架构与 V3.0 一致;切片不启动 HTTP 监听,脚本以 standalone app context 调用模块服务 |
| D6 契约范围 | packages/contracts 只含 UniversalRow + 内容/路由 DTO | YAGNI:无队列即无事件目录 |
| D7 WXR 解析 | 流式 XML 解析器(saxes 类)+ 资源限额 | cms 的正则解析在 33MB 级 WXR、CDATA/实体/命名空间边界有风险(V2.0 §5.1);限额:文件大小、节点数、处理时限;恶意 XML 防护(ADR-005) |
| D8 媒体安全底线 | 登记时魔数校验 + SVG 隔离 | 不信扩展名与声明 MIME;SVG 默认不进产物、单列报告(V2.0 P0-5)(ADR-006) |
运行环境:Node 22 LTS、pnpm workspace、TypeScript strict、vitest。开发机为 Windows(大小写陷阱见 §7 G5)。
## 4. 仓库结构(切片实际创建的部分)
```text
cms-one/
apps/api/ # NestJS:sites/content/routes/media/import/build/audit 模块
packages/contracts/ # UniversalRow、内容 DTO、路由 DTO(冻结+契约测试)
packages/adapter-wordpress/ # WXR 解析器 + WP REST 客户端(REST 通道切片内可选)
themes/pilot-minimal/ # 极简 Astro 主题
scripts/ # CLI:import.ts / build.ts / gate.ts / preview.ts
storage/wxr/ storage/media/ # 试点数据副本(git 忽略)
infra/docker-compose.yml # 仅 PostgreSQL
tests/fixtures/ # WXR/媒体夹具
docs/
plans/ # 根目录现有 4 份规划文档移入(实施第一步)
adr/ specs/ superpowers/
.gitignore # 仓库第一个 commit:node_modules/dist/artifacts/storage//.env/coverage
```
不预建空包(cms README 纪律);V3.0 §22.2 的其余包在对应阶段创建。
## 5. URL 与路由策略(本切片的核心正确性)
### 5.1 目标路径规则(按优先级)
| 旧 URL 形态 | 处置 | route_kind |
|---|---|---|
| 内容页 permalink(含 `.html`/`.php` 后缀、任意目录深度) | **产物按原路径原样输出**(路径级保真,非仅 slug) | html |
| 首页 `/` | 首页 | html |
| `product-category/{slug}`(产品分类) | 生成分类列表页,路径原样 | html |
| `category/{slug}`(文章分类) | 生成文章分类列表页 | html |
| 列表分页 `/page/N/`(含分类下分页) | 列表页分页输出 | html |
| `tag/{slug}`、`product_tag/{slug}`、日期归档、作者页 | 301 → 对应主列表页(/products/ 或 /news/) | redirect |
| 附件/媒体 `wp-content/uploads/...` | 媒体文件按**原路径**进产物 | asset |
| 无法保真的个别内容路径 | 301 → 新路径 | redirect |
| WP `?p=123` 短链 | 记录为已知限制,对照报告单列(不算 🔴,见 §8) | — |
### 5.2 route_registry(独立表,禁止塞 settings JSONB——cms 教训)
切片字段子集(V3.0 §6.4):`site_id / original_path / normalized_lookup_key / route_kind / target_content_id / target_file / redirect_location / indexable / source_status / source_snapshot_id`。
规范化键规则(仅作查重与对照,原始路径永不改写):percent-decode → Unicode NFC → 小写 → 去尾部斜杠。查重命中仅大小写/尾斜杠差异时**报告冲突**,不静默合并(V3.0 §6.3)。
### 5.3 URL 对照报告(门禁输入)
老 URL 全集 = WXR 可推导 URL(内容 permalink ∪ 术语归档 ∪ 分页推导)∪ 老站 sitemap.xml(若可拉取;被挑战页拦截则记说明)。三色口径与 cms 一致:🟢 产物存在同路径页面|🔵 route_registry 有 redirect|🔴 无去处(必须为 0)。
### 5.4 媒体旧路径保持
- `media_assets.source_url` 存旧完整 URL,`storage_path` 指向本地副本文件,`public_path` = 旧 URL 路径部分(`/wp-content/uploads/...`)。
- 构建时媒体复制到产物的 `public_path` 位置;正文 `<img>`、封面、相册引用全部重写为根相对 `public_path`。
- 未匹配到本地副本的媒体:列入导入报告(引用它的页面数、URL 清单),不阻塞导入;若被已发布页面引用则门禁降级为 🔴 由用户决定补图或改链。
- 登记时校验文件魔数与声明 MIME 一致,不一致入报告;**SVG 一律隔离**(不复制进产物、预览不可达),报告中单列清单。
## 6. 管道命令与数据流
### 6.1 `scripts/import.ts`(导入)
```text
读 storage/wxr/*.xml → adapter-wordpress 解析为 UniversalRow[]
→ 类型映射(post→news_article;product CPT→product;page→basic_page)
→ locale 取站点配置;未知 locale 记错误并跳过该条(禁止静默归 en——cms 实际踩坑)
→ 每条计算 normalized checksum:
与最新 content_revision.checksum 相同 → skip(不产生新修订)
不同或不存在 → 写入新 content_revision(kind=draft,不可变,source=import)
※ 导入不发布:发布是独立显式动作(见 6.2,V2.0 P0-6)
→ 媒体登记:按 WXR attachment 的 source_url 与本地副本文件名匹配;
全角/编码损坏文件名按规范化通配匹配(cms 踩坑),匹配结果写报告
→ 生成/更新 route_registry(§5.1 规则)与 301 规则
→ 未知 WP 字段保留进 data.extras(不丢弃)
→ 输出导入报告:条目数对照、skip/update/create 计数、媒体匹配率、
URL 冲突清单、行级错误定位
```
幂等键:`(site_id, source_connection_id, source_type, source_id, locale)`,与 externalRef(旧 permalink)双保险。
### 6.2 `scripts/publish.ts`(显式发布)
导入只产生 draft;发布是独立、显式、可审计的动作(V2.0 P0-6:禁止"迁移即发布"):
- 按批次/类型/条目批量将 draft 修订置为 published(更新 entry_locales.published_revision_id 指针)。
- 每次发布动作写 audit_logs(actor、对象、前后状态、时间)。
- 构建只消费 published 修订集合——与 cms「导入后逐条自动发布」的行为刻意相反。
### 6.3 `scripts/build.ts`(构建)
```text
物化 build_input_snapshot:
内容(published 修订集合)+ 路由清单 + 媒体清单 + 站点配置
→ 冻结为 artifacts/<buildId>/input/ 下带哈希的 JSON 文件
Astro 构建(themes/pilot-minimal):
只读输入快照文件,禁止实时查库,禁止任何 seed/演示兜底数据
(输入不完整 → 构建立即失败;此行为写回归测试)
SITE_URL 取站点配置的生产域名(hesupipe.com)→ canonical/og/sitemap 正确
产物:HTML 页面 + sitemap.xml(只含 route_kind=html 且 indexable)+ robots.txt
+ 媒体(旧路径)+ _redirects / nginx-redirects.conf(从 route_registry 编译,验证路由模型)
+ build-manifest.json(输入快照哈希、页面数、checksums)
落 artifacts/<buildId>/,build_jobs 表记录
```
### 6.4 `scripts/gate.ts`(门禁)
| 检查 | 通过条件 |
|---|---|
| URL 对照(§5.3) | 🔴 = 0 |
| 内部死链 | 产物内 href/src/srcset 指向的内部目标全部存在(html/asset 命中 route_registry) |
| sitemap 一致 | sitemap 条目 ⊆ 实际页面;indexable 页面 ⊆ sitemap |
| 页面 SEO 基线 | 每页有 title、canonical;H1 至多 1 个 |
| 大小写冲突 | 产物相对路径 lowercase 后无重复(Windows 开发盘不敏感、Linux 部署敏感——V3.0 §6.5) |
| 重定向图 | route_registry 的 redirect 关系无环;关键路径多跳(≥3 跳)告警(V2.0 §5.3:🔴=0 不代表无环) |
| 兜底数据回归 | 输入损坏时构建必须失败(负向测试) |
### 6.5 `scripts/preview.ts`(预览)
自研 Node 静态服务器(不引入 nginx 依赖):
- 按 route_registry 响应:html → 伺服产物文件;redirect → 301 + Location;无匹配 → 产物 404 页。
- 所有响应加 `X-Robots-Tag: noindex`;禁用目录列表。
- 用途:在本地验证"真实 URL 行为"(V3.0 §7.4 精神),并支撑 §8 的人工抽检。
## 7. 数据模型(切片子集,V3.0 §9 对齐)
| 表 | 关键字段与约束 |
|---|---|
| sites | site_id PK、name、default_locale、production_domain |
| source_connections | 类型=wxr、文件路径 + sha256、adapter 版本 |
| import_jobs / source_records | 批次状态机;源记录存原始条目位置 + checksum + 解析状态 |
| content_entries | (site_id, type_key, external_ref) 唯一;status: draft/published/archived |
| entry_locales | (site_id, locale, slug) 唯一;current/published_revision_id 指针 |
| content_revisions | **不可变**(插入式,禁 UPDATE);data jsonb、checksum、source=import、import_batch_id;预留 `overridden_fields`(下阶段冲突队列用) |
| route_registry | §5.2 字段;独立表 |
| media_assets | (site_id, source_url) 唯一幂等;sha256、public_path(旧路径)、alt、尺寸 |
| build_jobs / artifacts | 构建记录 + 产物目录 + 输入快照哈希 |
| audit_logs | 追加式;actor(切片为 system/importer)、action、对象、前后值 |
多语言位:模型带 locale,切片只写 en。
## 8. 错误处理
- **行级容错**:单条解析/映射失败记录行号与原因后继续;整批结构错误(文件损坏、站点不存在)才中断。
- **未知字段**:一律进 `data.extras` 保留,报告中汇总清单。
- **构建 fail-fast**:输入快照不完整(内容缺失、路由空、媒体引用悬空)立即失败,无任何降级/兜底路径。
- **已知限制**:`?p=` 短链、WXR 外的老站 URL(日志/GSC 来源)不在本切片对照范围,报告中单列"未覆盖来源"节——不算 🔴,进入下阶段 SEO diff。
- **重跑语义**:无变化 skip / 有变化新修订 / 人工改动保护(override 标记)下阶段实现,本切片在修订上预留字段。
## 9. 测试策略(vitest)
| 层 | 内容 |
|---|---|
| 单元:WXR 解析 | 夹具必须覆盖:`_thumbnail_id`、`_product_image_gallery`、`wppop_sections`(PHP serialize 的 Tab 数据)、Yoast `_yoast_wpseo_title/metadesc`、全角/编码损坏文件名、分页与术语 URL 推导、`.html` 后缀 permalink(全部来自 cms 实际踩坑清单) |
| 单元:路由规则 | §5.1 每行规则一个用例;大小写/尾斜杠冲突报告 |
| 单元:解析器限额 | 超大/畸形 XML(节点数、大小、时限超限)→ 明确失败不崩溃;CDATA、实体、命名空间边界夹具 |
| 单元:媒体安全 | 魔数与扩展名不符 → 报告;SVG → 进隔离清单 |
| 单元:重定向图 | 环 → 🔴;多跳 → 告警 |
| 集成:导入 | 真实 WXR 小切片夹具 → 落库 → 断言修订/checksum/幂等(第二次 0 新修订) |
| 集成:构建 | 快照物化 → Astro 构建 → 产物结构断言(sitemap/robots/媒体旧路径/_redirects) |
| 负向 | 输入损坏 → 构建失败(兜底禁令);route_registry 缺目标 → 门禁 🔴 |
| 契约 | UniversalRow/DTO 字段冻结(packages/contracts) |
| 验收 | §2.1 G1–G6 全部可执行复验(真实 hesupipe 数据 + 抽检 20 个 URL:10 内容页 + 5 分类页 + 3 redirect + 2 404) |
## 10. 实施注意(从 cms 教训直接落地的防线)
0. **动工第零步**:手动备份 cms 数据(pg_dump + storage/ 拷贝至第二存储,约半小时——V2.0 阶段 A 要求)。
1. `.gitignore` 是仓库第一个 commit(storage/、artifacts/、node_modules、.env)。
2. 构建输入快照先于一切(seed 兜底事故的根治)。
3. redirects 走独立表(不进 settings JSONB)。
4. WXR 原件 + sha256 留档 source_connections。
5. 未知字段不丢弃(extras)。
6. 门禁 🔴=0 才算过。
7. Windows 长路径:副本拷贝与构建脚本启用长路径处理(全角文件名目录)。
## 11. 验收标准(切片完成的定义)
> 口径采用 V2.0 §2.2 成熟度分级:本切片验收 = **L3(真实数据本地验证)**;L4(生产部署、HTTP 验收、回滚演练)属后续部署阶段。
1. `import` 两连跑:第一次全量入库为 draft(条目数与 WXR 基数对照一致),`publish` 后转 published,第二次 import 0 新修订。
2. `build` 产出全站产物,`gate` 七项全绿。
3. `preview` 启动后按 §9 抽检 20 URL 行为正确(含 301 跳转与 404)。
4. `pnpm test` 全绿(含负向与契约)。
5. 导入报告与 URL 对照报告可读、可复查(含媒体匹配率与未覆盖来源说明)。
## 12. 切片之后的路(指引后续子项目,不在本切片内)
```text
SP0' 后台 UI(Art Design Pro + 认证/MFA)
SP1' 差量同步与冲突队列(override 字段启用)
SP2' SEO 三套数据 diff(P0–P3)+ 爬虫扩充 URL 全集
SP3' 主题/板块协议(Block Schema/模板约束/第二主题)
SP4' 部署器(rsync/SSH → OSS/CDN)+ 发布锁/回滚
SP5' 表单服务 + 备份监控
```
每阶段对照 `cms现状与SiteForge差距分析_V1.0.md` 的 17 项清单打钩。