d60123ba41
- V3.0 总体方案、主题与板块设计、实施计划 - cms 现状差距分析 V2.0(V1.0 已标注作废)与复用评估 V1.0 - 纵向切片设计规格(路线 C:全新架构 + 白名单资产移植) - .gitignore 先行(storage/artifacts/env 永不入库) Co-Authored-By: Claude Code <noreply@anthropic.com>
250 lines
17 KiB
Markdown
250 lines
17 KiB
Markdown
# 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 项清单打钩。
|