- V3.0 总体方案、主题与板块设计、实施计划 - cms 现状差距分析 V2.0(V1.0 已标注作废)与复用评估 V1.0 - 纵向切片设计规格(路线 C:全新架构 + 白名单资产移植) - .gitignore 先行(storage/artifacts/env 永不入库) Co-Authored-By: Claude Code <noreply@anthropic.com>
17 KiB
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 导出 + 图片包副本)打通最险的核心链路一次:
WXR 导入 → 统一内容模型 → URL 注册表 → 极简主题 → 静态构建 → 门禁 → 本地预览
三条前提约束:
- 所有代码是正式资产:数据模型、契约、目录结构按 V3.0 设计,后续能力在此之上加厚,不推倒重写。
- 不改动 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 服务、后台业务页面。 - 完整性由清单驱动:每完成一个阶段对照 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. 仓库结构(切片实际创建的部分)
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(导入)
读 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(构建)
物化 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 教训直接落地的防线)
- 动工第零步:手动备份 cms 数据(pg_dump + storage/ 拷贝至第二存储,约半小时——V2.0 阶段 A 要求)。
.gitignore是仓库第一个 commit(storage/、artifacts/、node_modules、.env)。- 构建输入快照先于一切(seed 兜底事故的根治)。
- redirects 走独立表(不进 settings JSONB)。
- WXR 原件 + sha256 留档 source_connections。
- 未知字段不丢弃(extras)。
- 门禁 🔴=0 才算过。
- Windows 长路径:副本拷贝与构建脚本启用长路径处理(全角文件名目录)。
11. 验收标准(切片完成的定义)
口径采用 V2.0 §2.2 成熟度分级:本切片验收 = L3(真实数据本地验证);L4(生产部署、HTTP 验收、回滚演练)属后续部署阶段。
import两连跑:第一次全量入库为 draft(条目数与 WXR 基数对照一致),publish后转 published,第二次 import 0 新修订。build产出全站产物,gate七项全绿。preview启动后按 §9 抽检 20 URL 行为正确(含 301 跳转与 404)。pnpm test全绿(含负向与契约)。- 导入报告与 URL 对照报告可读、可复查(含媒体匹配率与未覆盖来源说明)。
12. 切片之后的路(指引后续子项目,不在本切片内)
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 项清单打钩。