Files
cms-website/docs/superpowers/specs/2026-10-20-siteforge-vertical-slice-design-v2.md
T

44 KiB
Raw Blame History

SiteForge 最小纵向切片设计与实施计划 V2.0

日期:2026-10-20
状态:已确认(2026-10-20 用户采纳为设计基线,附四项修订:①资产移植白名单列明 ②Tag/归档页默认 301 ③ZIP 输入移出切片 ④?p= 查询路由降级为登记不实现)
上位文档:SiteForge_完整项目计划方案_V3.0.md
现状依据:cms现状与SiteForge差距分析_V2.0.md
复用依据:SiteForge_现有CMS项目复用评估_V1.0.md
被替代文档:2026-10-20-siteforge-vertical-slice-design.md
验收目标:L3——使用真实试点数据完成本地全链路验证;不把本切片认定为生产上线完成。


1. 文档定位

SiteForge 是多站点静态迁移、内容维护、静态构建和发布平台。本计划定义第一个最小纵向切片,用 hesupipe.com 的 WordPress WXR 和媒体副本打通一条可重复、可审计、可继续演进的正式主链路:

授权与输入登记
→ 原始快照
→ WXR 流式解析
→ 内容/分类/媒体标准化
→ 草稿修订
→ URL 注册表
→ 显式发布修订
→ 冻结构建输入
→ 极简主题静态构建
→ 全量门禁
→ 基于同一 Manifest 的本地预览

本切片的目标不是尽快拼出一个演示站,而是建立后续后台、主题、差量迁移、部署和回滚都能继续使用的领域模型与契约。

1.1 权威关系

  1. SiteForge_完整项目计划方案_V3.0.md 是产品范围和总体架构的唯一基线。
  2. 本文是第一个纵向切片的设计与实施基线。
  3. cms现状与SiteForge差距分析_V2.0.md 说明旧代码的真实成熟度和风险。
  4. 旧 cms 文档和旧切片 V1 只作为历史参考,不覆盖本文。

1.2 前提约束

  • D:\project\cms 保持只读,不在其中实施新架构。
  • 资产移植白名单(路线 C,列明如下)——可移植:WXR 解析规则与映射经验(重写到流式解析器之上)、测试夹具(脱敏后)、门禁规则逻辑、docker-compose/CI 配置;旧 cms 的 Astro 页面仅作视觉参考,不移植代码。不继承、按本设计重写:认证、BuildService、release/rollback、MigrationService 编排(迁移即发布)、settings JSONB 路由、Lead 服务、后台业务页面、整页硬编码的前台架构。
  • 真实试点数据必须获得使用授权,只复制完成切片所需的 WXR、媒体和 Sitemap。
  • 真实数据、媒体、数据库、凭据、完整报告不进入 Git。
  • 测试夹具必须脱敏、最小化并可公开给项目开发者。
  • 本切片只启动本地 CLI 和绑定回环地址的 Preview Server,不对公网开放控制 API。
  • 所有新 Schema、契约和目录均按可长期演进的方式设计,不使用明确会被下一阶段推翻的临时结构。

2. 切片目标、范围与成功标准

2.1 目标

编号 目标 验收口径
G1 WXR 范围内全量导入 纳入范围的 post、page、product、taxonomy、term、attachment 全部被创建、更新、跳过或明确报错;不支持对象有统计和原因
G2 幂等与不可变修订 第二次导入同一 Source Snapshot 产生 0 条新修订;内容变化产生新修订,不 UPDATE 历史修订
G3 导入不发布 首次导入只更新 current revision;只有显式 publish 才更新 published revision
G4 URL 注册表 WXR + 本地 Sitemap 构成的“当前已知 URL 基线”全部有明确处置;未处理 🔴=0
G5 路径和媒体保真 可保真的内容、分类、分页和媒体按旧路径输出;不能保真的路径使用明确 Redirect/Gone;登记为 unresolved 的路径必须在门禁前清零
G6 可复现静态构建 构建只读取冻结快照;相同输入连续构建的业务文件 Hash 一致;禁止 Seed 和在线数据库读取
G7 最小主题契约 pilot-minimal 使用正式 Theme Manifest、Render Snapshot 和公共基础组件,不复制旧 cms 的整页硬编码架构
G8 自动门禁 全量检查路由、SEO、死链、媒体、Sitemap、重定向图、路径冲突和远程依赖
G9 本地 HTTP 预览 Preview 只读取该 Build 的 Route Manifest 和 Dist,正确响应 HTML、Asset、301、410 和 404
G10 可审计证据 Import、Publish、Build、Validation 全部持久化状态;生成 JSON/HTML 报告和脱敏验收摘要

2.2 本切片支持的 WordPress 范围

必须支持:

  • post → news_article
  • page → basic_page
  • product CPT → product
  • 分类、标签、产品分类、产品标签及内容关系
  • Attachment 元数据、特色图片、正文图片、产品图库
  • Yoast 常见 Title、Description 和主分类字段
  • _thumbnail_id
  • _product_image_gallery
  • 试点已知 wppop_sections 等主题字段
  • .html、.php、多级目录等原 permalink
  • Sitemap 文件中的额外已知 URL

必须识别但允许进入“不支持清单”:

  • 未配置映射的 CPT
  • Elementor/WPBakery 等复杂布局数据
  • 未知短代码
  • 插件私有表中、WXR 不包含的数据
  • 复杂 WooCommerce 变体、库存、订单等非展示数据
  • 评论、用户和敏感个人数据

“全量导入”是指本切片范围内的所有对象均有明确结果,不代表已经支持 WordPress 的全部插件生态。

2.3 非目标

以下内容不在本切片交付范围:

  • Art Design Pro 管理后台。
  • ZIP 媒体包解包输入(本切片仅接受已解包目录);查询参数路由(?p= 等仅登记不实现)。
  • 公网认证、MFA 和会话体系。
  • WordPress REST、SQL 直连和在线爬虫。
  • 日志、Search Console、GA4 等完整 URL 发现来源。
  • 差量同步、删除检测和人工冲突队列;本切片只建立未来所需的数据位。
  • 第二主题和可视化 Block Editor。
  • Redis/BullMQ;但 Job 状态必须先持久化。
  • Nginx/SSH、OSS/CDN 正式部署。
  • 生产发布锁、CDN 刷新和生产回滚。
  • 公网表单服务。
  • 完整多语言工作流;模型支持 Locale,本切片只验收 en。
  • ClickHouse、Event Gateway、客户画像和增长分析。

2.4 能力边界声明

本切片的 URL 基线只来自:

WXR 可推导 URL
∪ WXR Attachment URL
∪ 本地提供的 Sitemap/Sitemap Index
∪ 人工补充 URL 清单(可选)

报告必须显示“当前已知基线”,不得使用“发现了全部历史 URL”的表述。日志、GSC、GA4、在线爬虫未接入的事实必须显示在报告首页。


3. 技术决策

ADR 决策 本切片选择 约束
ADR-001 ORM Drizzle + PostgreSQL 16 所有 Schema 通过迁移文件管理;关键约束落数据库
ADR-002 服务形态 NestJS 模块化单体 + Standalone App Context + CLI 本切片不监听公网端口;服务层未来由 HTTP Controller 复用
ADR-003 Job 执行 进程内 Runner + 数据库 Job 状态 不要求 BullMQ,但不能只存内存;进程中断后可识别 stale Job
ADR-004 原始输入 Source Connection 与 Source Snapshot 分离 每个 WXR/Sitemap/已解包媒体目录形成不可变 Snapshot 和 Hash
ADR-005 WXR 解析 流式 SAX 解析 禁止整文件正则解析;限制字节、节点、深度、字段长度和处理时间
ADR-006 内容身份 Source Record 稳定键 WordPress ID 是来源身份;Permalink 只是 URL 证据,不作为稳定主键
ADR-007 URL 模型 Exact Route Key 与 Collision Key 分离 实际路由不大小写折叠、不去尾斜杠;碰撞键只用于风险检查
ADR-008 媒体 本地 Content-Addressed Storage 本切片不用 MinIO;对象存储阶段可更换 Storage Adapter
ADR-009 HTML 安全 白名单清洗,原文保留在 Source Snapshot 清洗后内容进入 Revision;差异写报告
ADR-010 发布模型 current/published 双指针 导入不改变 published pointer;发布不修改 Revision
ADR-011 构建 冻结 Render Snapshot Astro 禁止查数据库、禁止在线拉内容、禁止 Seed fallback
ADR-012 主题 最小 Theme Manifest + Render Contract 完整 Block 协议后置,但第一主题不得依赖站点专属数据导入代码
ADR-013 预览 Artifact 自包含 Preview 读取同一 Build 的 Route Manifest,不读取实时数据库

运行环境:

  • Node.js 22 LTS
  • pnpm Workspace
  • TypeScript Strict
  • PostgreSQL 16
  • Astro
  • NestJS
  • Drizzle ORM
  • Vitest
  • HTML Parser/Sanitizer
  • SAX XML Parser

依赖版本必须写入 Lockfile。使用新第三方库前检查许可证和维护状态。


4. 仓库结构

cms-one/
  apps/
    api/                         # NestJS 模块化单体;本切片由 CLI 创建 standalone context
  packages/
    contracts/                   # Source/Content/Route/Render/Report DTO
    adapter-wordpress/           # WXR 流式解析与映射
    route-engine/                # URL 建模、冲突检测、Route Manifest
    validation-engine/           # Gate 规则与报告
    theme-runtime/               # Render Snapshot 读取和公共渲染约定
    ui-primitives/               # 最小无业务耦合 Astro 组件
  themes/
    pilot-minimal/
      theme.manifest.ts
      src/layouts/
      src/components/
      src/pages/
      src/styles/
  scripts/
    register-source.ts
    import.ts
    publish.ts
    build.ts
    gate.ts
    preview.ts
    verify.ts
  infra/
    docker-compose.yml           # 本切片仅 PostgreSQL
    database/
  storage/                       # 全部 Git Ignore
    incoming/                     # 待登记的临时暂存输入(source:register 登记后复制进 snapshots/,可清理)
    snapshots/
    media/
    quarantine/
    sitemap/
  artifacts/                     # 全部 Git Ignore;本地完整证据
    jobs/<job-id>/
      input/
      dist/
      manifests/
      reports/
      logs/
  tests/
    fixtures/                    # 脱敏最小夹具,可提交
    contract/
    unit/
    integration/
    acceptance/
  docs/
    adr/
    specs/
    runbooks/
    evidence/                    # 可提交的脱敏验收摘要

暂不移动根目录现有规划文档,避免在实现启动时同时制造链接和历史迁移噪音。文档整理另开独立提交。

.gitignore 已经存在,实施开始时只需验证以下路径确实被忽略:

  • storage/
  • artifacts/
  • .env*
  • node_modules/
  • dist/
  • coverage/
  • 数据库本地卷

5. 核心领域模型

5.1 站点与配置

sites

id uuid PK
key text UNIQUE
name text
default_locale text
production_origin text
created_at timestamptz
updated_at timestamptz

production_origin 必须是明确的 https://host[:port],用于 Canonical、OG 和 Sitemap。路径不写入该字段。

site_config_revisions

id uuid PK
site_id FK
version integer
data jsonb
checksum text
created_at timestamptz
UNIQUE(site_id, version)

第一版配置至少包含:

  • 公司名称和站点名称。
  • 首页路径和主要列表路径。
  • Logo、联系方式和 Footer 信息。
  • 默认 SEO 信息。
  • 主导航配置;如果 WXR 中存在可用菜单,可以导入后生成配置修订。

5.2 来源、快照与导入

source_connections

id uuid PK
site_id FK
kind enum(wxr)
name text
configuration jsonb
created_at timestamptz

Connection 只描述来源,不保存某次文件路径和 Hash。

source_snapshots

id uuid PK
site_id FK
source_connection_id FK
kind enum(wxr, sitemap, media_bundle, manual_url_list)
storage_path text
sha256 text
byte_size bigint
adapter_version text
captured_at timestamptz
metadata jsonb
created_at timestamptz
UNIQUE(site_id, kind, sha256)

Snapshot 创建后不可变。替换文件必须创建新 Snapshot。

import_jobs

id uuid PK
site_id FK
source_snapshot_id FK
status enum(queued, running, succeeded, succeeded_with_issues, failed, cancelled, stale)
mapping_version text
idempotency_key text UNIQUE
attempt integer
started_at / finished_at
error_code / error_summary
report_path text
created_at timestamptz

source_records

id uuid PK
site_id FK
source_connection_id FK
source_snapshot_id FK
import_job_id FK
source_type text
source_id text
locale text
source_url text
raw_location text
source_checksum text
normalized_checksum text
parse_status text
content_entry_id uuid nullable
UNIQUE(site_id, source_connection_id, source_type, source_id, locale, source_snapshot_id)

跨 Snapshot 的稳定映射键为:

site_id + source_connection_id + source_type + source_id + locale

5.3 内容与修订

content_entries

id uuid PK
site_id FK
type_key text
lifecycle_status enum(active, archived)
created_at / updated_at
INDEX(site_id, type_key)

不使用 Permalink 作为 Content Entry 的唯一身份。

entry_locales

id uuid PK
site_id FK
content_entry_id FK
locale text
slug text
current_revision_id uuid nullable
published_revision_id uuid nullable
created_at / updated_at
UNIQUE(site_id, content_entry_id, locale)

Slug 是否唯一由 Route Registry 决定,不单独假设所有内容类型共享同一 Slug 空间。

content_revisions

id uuid PK
site_id FK
content_entry_id FK
locale text
schema_version integer
data jsonb
checksum text
source_kind enum(import, manual, restore)
source_record_id uuid nullable
import_job_id uuid nullable
overridden_fields jsonb default []
created_by text
created_at timestamptz

约束:

  • 只 INSERT,不提供 UPDATE/DELETE 业务接口。
  • 数据库权限或 Trigger 阻止对既有 Revision 的更新。
  • current_revision_id 指向最新工作修订。
  • published_revision_id 指向公开修订。
  • 两个指针不同表示“已发布但有未发布修改”。
  • 构建只读取 published_revision_id。

5.4 分类和关系

taxonomies

id uuid PK
site_id FK
key text
object_type text
public_base_path text
UNIQUE(site_id, key, object_type)

terms

id uuid PK
site_id FK
taxonomy_id FK
source_id text
slug text
name text
parent_term_id uuid nullable
data jsonb
UNIQUE(site_id, taxonomy_id, source_id)

content_terms

site_id FK
content_entry_id FK
term_id FK
is_primary boolean
PRIMARY KEY(site_id, content_entry_id, term_id)

这些表支撑分类页、标签页、产品分类、分页和 Yoast 主分类,不允许临时塞进内容 JSON 后再重构。

5.5 媒体与使用关系

media_assets

id uuid PK
site_id FK
source_connection_id FK
source_id text nullable
source_url text
source_path text
storage_path text
public_path text
sha256 text
detected_mime text
declared_mime text nullable
byte_size bigint
width / height integer nullable
alt_text text nullable
status enum(ready, missing, quarantined, unsupported)
created_at timestamptz
UNIQUE(site_id, source_url)
UNIQUE(site_id, public_path)

media_usages

site_id FK
media_asset_id FK
content_revision_id FK
field_path text
source_url text
PRIMARY KEY(media_asset_id, content_revision_id, field_path)

媒体匹配顺序:

  1. 完整旧 URL。
  2. wp-content/uploads 后的完整相对路径。
  3. 编码修复后的完整相对路径。
  4. 仅文件名模糊匹配只能生成候选,不得自动选中;多个候选进入冲突报告。

5.6 路由注册表

route_registry

id uuid PK
site_id FK
host text
source_path_raw text
route_path text
exact_route_key text
collision_key text
query_policy enum(ignore, exact, preserve, dynamic)
query_key text nullable
source_status text
target_status text
route_kind enum(html, asset, redirect, gone, dynamic, unresolved)
target_content_id uuid nullable
target_file text nullable
redirect_status integer nullable
redirect_location text nullable
indexable boolean
priority integer
source_snapshot_id uuid nullable
verification_status text
created_at / updated_at
UNIQUE(site_id, host, exact_route_key, query_key)

规则:

  • UNIQUE(site_id, host, exact_route_key, query_key) 中 query_key 为 NULL 的纯路径路由,需以 PostgreSQL NULLS NOT DISTINCT(或哨兵值)实现数据库级唯一——写入 Schema 约束,避免迁移编写时才发现。
  • source_path_raw 永远保存来源原文。
  • route_path 是经过安全校验的实际公开路径。
  • exact_route_key 用于请求匹配,保留大小写和尾斜杠语义。
  • collision_key 仅用于检测 Windows/Linux、大小写、Unicode NFC 和尾斜杠风险,不用于实际路由。
  • Percent Encoding 只解码 RFC 非保留字符;%2F、%5C 等保留编码不改写。
  • 拒绝 NUL、反斜杠、路径穿越、控制字符、Windows 保留设备名、尾部空格/点和超长路径。
  • Fragment 不属于服务端路由,不写入 Route Key。
  • 查询路由单独建模,不与 Path 静默合并。

5.7 Job、构建和验收

build_jobs

id uuid PK
site_id FK
status enum(queued, snapshotting, building, validating, succeeded, failed, cancelled, stale)
idempotency_key text UNIQUE
attempt integer
worker_id text nullable
started_at / heartbeat_at / finished_at
error_code / error_summary
created_at timestamptz

build_input_snapshots

id uuid PK
build_job_id FK UNIQUE
site_id FK
content_snapshot_hash text
route_snapshot_hash text
asset_manifest_hash text
site_config_revision_id FK
theme_key text
theme_version text
theme_source_hash text
dependency_lock_hash text
source_commit text
node_version text
pnpm_version text
astro_version text
locale text
timezone text
build_timestamp timestamptz
manifest_path text
created_at timestamptz

build_artifacts

id uuid PK
build_job_id FK
kind enum(dist, manifest, report, log)
storage_path text
sha256 text
byte_size bigint
created_at timestamptz
UNIQUE(build_job_id, kind, storage_path)

validation_runs / validation_issues

保存 Gate 的规则版本、开始/结束时间、结果和每条问题的严重级别、URL、证据、修复状态。

5.8 审计日志

audit_logs 为追加式,记录:

  • site_id
  • actor_type:system、cli_user
  • actor_id/actor_label
  • command_id
  • action
  • object_type/object_id
  • before/after 摘要
  • occurred_at

CLI 必须接受或生成明确 Actor,不使用模糊的固定 system/importer 代替所有操作。


6. URL 与页面生成策略

6.1 路径处置

来源 URL 切片处置 Route Kind
内容 Permalink 尽量按原路径输出 HTML html
首页 使用站点配置指定路径 / html
产品/新闻主列表 使用站点配置的旧路径,生成分页 html
产品分类/文章分类 根据 Taxonomy/Term 生成列表和分页 html
Tag/Product Tag 默认 301 到对应主列表(本切片不生成 Tag 页;生成能力留待 SP3 后按需评估) redirect
日期/作者归档 默认 301 到新闻主列表,逐条登记 redirect
Attachment 页面 确定性默认规则:出现在老站 Sitemap → html;仅 WXR 证据 → 301 到对应媒体文件(或父列表页);确认已删除 → 410。偏离默认的条目在报告中逐条列出 html/redirect/gone
上传媒体路径 按旧 wp-content/uploads/... 输出 asset
无法保真的内容路径 人工映射;不得默认跳首页 redirect/unresolved
已确认永久删除 410 gone
?p=ID、?page_id=ID 登记为 excluded_with_reason(报告单列记录在案);查询路由能力留待 SP3 实现 —(登记不实现)

6.2 当前已知 URL 基线

每条来源 URL 必须归入:

verified_html
verified_asset
verified_redirect
verified_gone
unresolved
excluded_with_reason
unknown_source_gap

Gate 要求 unresolved=0。unknown_source_gap 不伪装成成功,必须在报告首页显示,但因本切片明确未接日志/GSC/爬虫,不单独阻止 L3 验收。

6.3 Redirect 约束

  • Redirect 状态码只能是批准的 301/302;本切片默认 301。
  • Location 必须可解析。
  • 目标必须落到 HTML、Asset、Gone 或批准的外部 URL。
  • Redirect 环直接失败。
  • 任意多跳大于 1 均告警;超过配置上限直接失败。
  • 禁止把无法映射的路径批量跳转到首页。

6.4 静态文件映射

示例:

/about-us/          → dist/about-us/index.html
/news/a.html        → dist/news/a.html
/legacy/page.php    → dist/legacy/page.php 或由 Route Manifest 映射
/wp-content/uploads/2024/a.jpg
                    → dist/wp-content/uploads/2024/a.jpg

所有文件映射必须经过安全 Join,最终绝对路径必须位于该 Build 的 Dist 根目录内。


7. WXR 导入管道

7.1 输入登记

pnpm sf source:register \
  --site hesupipe \
  --wxr storage/incoming/hesupipe.xml \
  --media storage/incoming/uploads \
  --sitemap storage/incoming/sitemap.xml

登记过程:

  1. 验证文件位于允许目录。
  2. 计算 SHA-256、字节数和媒体清单。
  3. 将输入复制到 Content-Addressed Snapshot 目录。
  4. 创建 source_snapshots。
  5. 原输入之后不再被任务直接读取。

本切片媒体输入只接受已解包目录(自 cms 只读复制 storage/media/ 下的站点目录)。ZIP 包输入及其防护(Zip Slip、绝对路径、符号链接逃逸、压缩炸弹、数量/单文件/总解压限额、文件名碰撞)移出本切片,待后续支持 ZIP 上传的阶段实现。

7.2 流式解析

解析器限制通过配置明确:

  • 最大 WXR 字节数。
  • 最大 Item 数。
  • 最大 XML 深度。
  • 最大单字段和 CDATA 长度。
  • 最大 Term、Meta、Attachment 数。
  • 总处理时限。
  • PHP Serialized 数据最大深度和元素数。

解析器不得解析外部实体或访问网络。

7.3 UniversalRow

最低契约:

interface UniversalRow {
  sourceType: string
  sourceId: string
  status: string
  locale: string
  sourceUrl?: string
  slug?: string
  title?: string
  excerpt?: string
  bodyHtml?: string
  publishedAt?: string
  modifiedAt?: string
  taxonomyRefs: Array<{ taxonomy: string; sourceTermId: string }>
  attachmentRefs: Array<{ role: string; sourceId?: string; sourceUrl?: string }>
  seo: Record<string, unknown>
  knownFields: Record<string, unknown>
  extras: Record<string, unknown>
  rawLocation: string
}

契约版本变更必须提升 Schema Version 并保留转换测试。

7.4 HTML 清洗

处理顺序:

原始 HTML 保留在 Source Snapshot
→ WordPress Shortcode/主题字段适配
→ URL 和媒体引用提取
→ HTML 白名单清洗
→ 链接/媒体路径重写
→ 生成 Revision Data

默认删除或限制:

  • script、object、embed。
  • 事件属性。
  • javascript:、危险 Data URL。
  • 未批准 iframe。
  • 外部追踪像素。
  • 未知 Form。

清洗差异必须按内容记录到 Import Report。

7.5 Checksum 与幂等

Normalized Checksum 输入:

canonical_json({
  universal_row_version,
  adapter_version,
  mapping_version,
  sanitizer_version,
  locale,
  mapped_content,
  taxonomy_relations,
  media_reference_identities,
  route_source_values
})

规则:

  • JSON Key 稳定排序。
  • 时间统一 UTC ISO 8601。
  • 不包含 Import Job ID、处理时间等非业务字段。
  • 相同 Snapshot 重跑不得产生新 Revision。
  • 相同 Source Record 在新 Snapshot 中无业务变化时同样 Skip。
  • 适配器、映射或清洗版本变化可显式触发重新标准化。

7.6 事务和失败

  • Job 创建和 Snapshot 绑定在事务中完成。
  • 每个 Source Record 的内容、Revision、分类关系和路由写入使用数据库事务。
  • 单条失败不回滚整个 Import,但 Job 最终为 succeeded_with_issues。
  • 结构损坏、Snapshot Hash 不符或数据库约束错误可使整个 Job 失败。
  • 重跑使用新的 Import Job,但复用已有 Snapshot。
  • 不支持的对象必须计数并输出原因,不得静默跳过。

7.7 导入报告

报告至少包含:

  • Snapshot 和 Adapter/Mapping/Sanitizer 版本。
  • 按 WP 类型和状态统计的输入基数。
  • Created/Updated/Skipped/Failed/Unsupported。
  • 内容修订数量。
  • Taxonomy/Term/关系数量。
  • 媒体 Ready/Missing/Quarantined/Unsupported/Ambiguous。
  • HTML 清洗统计。
  • URL Created/Conflict/Unresolved。
  • 行级错误和 Raw Location。
  • 未覆盖来源声明。

8. 显式发布模型

8.1 发布语义

导入只更新 entry_locales.current_revision_id。如果内容从未发布,published_revision_id 保持 NULL;如果已经发布过,旧 Published Revision 继续对构建可见。

发布只执行:

published_revision_id = current_revision_id

不得 UPDATE content_revisions。

8.2 CLI 安全要求

pnpm sf publish \
  --site hesupipe \
  --import-job <id> \
  --actor <label> \
  --dry-run

pnpm sf publish \
  --site hesupipe \
  --import-job <id> \
  --actor <label> \
  --confirm <preview-token>

发布前必须:

  • 展示条目数、类型和现有/目标修订差异。
  • 拒绝包含未解决 P0 Import Issue 的批次。
  • 拒绝跨 Site 对象。
  • 写 Audit Log。
  • 重复发布同一 Revision 幂等。

本切片中的“发布”是内容修订批准,不是生产部署。


9. 最小主题与组件契约

9.1 Theme Manifest

themes/pilot-minimal/theme.manifest.ts 至少声明:

interface ThemeManifest {
  key: 'pilot-minimal'
  version: string
  renderContractVersion: string
  supportedContentTypes: string[]
  requiredSiteConfig: string[]
  templates: Array<{
    key: string
    appliesTo: string
  }>
}

9.2 Render Snapshot

主题只接收冻结文件:

site.json
content.json
taxonomies.json
routes.json
assets.json
render-manifest.json

主题不得:

  • 查询 PostgreSQL。
  • 调用 Control API。
  • 读取 storage/ 原始输入。
  • 使用站点专属 Seed 数据。
  • 在构建时访问网络。

9.3 第一批公共组件

  • BaseLayout
  • SeoHead
  • SiteHeader
  • SiteFooter
  • Breadcrumbs
  • RichText
  • ResponsiveMedia
  • ProductCard/ProductGrid
  • NewsCard/NewsList
  • TaxonomyList
  • Pagination
  • NotFoundPage

业务内容只能通过 Render Snapshot 进入组件,不允许组件内部硬编码 hesupipe 内容。

9.4 暂不实现的主题能力

  • 可视化 Block Editor。
  • 多主题切换。
  • Slot 管理后台。
  • Block Schema UI。
  • Theme Token 在线编辑。

但 Theme Manifest、Render Contract 和公共组件边界必须在本切片建立,以避免第二主题时重写第一主题。


10. 构建输入和静态构建

10.1 Build Job 状态

queued
→ snapshotting
→ building
→ validating
→ succeeded

异常状态:failed、cancelled、stale。

进程启动时将超过阈值且无心跳的 Running Job 标记为 stale,不自动宣告成功。

10.2 冻结输入

Snapshot 物化过程在数据库一致性读取中完成:

  1. 读取站点配置 Revision。
  2. 读取所有 Published Revision 指针及内容。
  3. 读取 Taxonomy、Term 和关系。
  4. 读取 Route Registry 当前版本。
  5. 读取 Media Manifest 和 Usage。
  6. 写临时目录。
  7. 生成 Canonical JSON 和 Hash。
  8. 原子重命名为最终 Input 目录。
  9. 创建 build_input_snapshots 记录。

10.3 Artifact 统一结构

artifacts/jobs/<build-job-id>/
  input/
    site.json
    content.json
    taxonomies.json
    routes.json
    assets.json
    build-input.json
  dist/
  manifests/
    build-manifest.json
    route-manifest.json
    asset-manifest.json
    checksums.txt
  reports/
    validation-summary.json
    validation-report.html
    url-coverage.json
  logs/
    build.log

不再同时使用 artifacts/<buildId> 和 artifacts/jobs/<jobId> 两套路径。

10.4 构建要求

  • 只读 Input 目录。
  • 默认断网;至少通过测试证明构建不需要网络。
  • 无 Seed fallback。
  • 输入为空、Hash 不符或 Schema 不兼容立即失败。
  • SITE Origin 来自冻结 Site Config。
  • Sitemap 只包含 route_kind=html && indexable=true。
  • Route/Asset/Build Manifest 与 Dist 同步生成。
  • 构建日志不得包含敏感原始数据和凭据。

10.5 可复现性

对同一 Build Input 连续构建两次,必须满足:

  • HTML、CSS、JS、媒体和 Route Manifest Hash 相同。
  • 允许 Build Metadata 中时间、Job ID 不同,但不得进入业务文件内容。
  • 排序、时区、随机数和生成时间固定或显式注入。

11. 媒体处理

11.1 安全校验

  • 文件路径安全 Join。
  • 魔数和实际解码结果校验。
  • 声明 MIME 与检测 MIME 比对。
  • 图片像素、字节和尺寸限制。
  • SVG 默认 Quarantine,不进入 Dist。
  • 可执行文件拒绝。
  • 未支持附件类型登记为 Unsupported。

11.2 缺失分级

情况 Import Build Gate
内容引用没有 Media Asset 记录 允许完成并产生 P0 Issue 允许生成候选报告,但 Build Job 最终失败 🔴
Media Asset 已登记但本地文件缺失 允许完成并标 Missing 允许生成候选报告,但不得标记 succeeded 🔴
SVG 被隔离且仍被公开内容引用 允许完成并标 Quarantined 不复制 🔴
不影响公开内容的孤立缺失媒体 告警 可继续 🟡

为了获得完整诊断,可以生成候选 Dist;但只要存在公开引用的 P0 媒体问题,Build Job 最终状态必须是 failed,不能进入可发布状态。

11.3 旧路径输出

public_path 使用旧 URL Path。写文件前必须验证:

  • 路径位于 Dist 根下。
  • 与 HTML 路由无冲突。
  • 大小写、NFC 和 Windows 文件名无冲突。
  • 同一路径不同 Hash 不允许静默覆盖。

12. Route Manifest 与 Preview Server

12.1 Route Manifest

Route Manifest 是某个 Build Input 的不可变派生产物,至少包含:

manifest_version
site_id
build_job_id
origin
generated_from_route_snapshot_hash
routes[]

每个 Route 包含 Exact Key、Query Policy、Kind、目标文件、状态码、Location、Content-Type 和 Indexable。

12.2 Preview Server

pnpm sf preview --build <build-job-id> --host 127.0.0.1 --port 4173

约束:

  • 只绑定 127.0.0.1,除非用户显式指定其他地址。
  • 只读取指定 Job 的 Dist 和 Route Manifest。
  • 不读取实时数据库。
  • 防止 ..、编码路径穿越、反斜杠和符号链接逃逸。
  • 禁用目录列表。
  • HTML/Asset 使用正确 Content-Type。
  • Redirect 返回 Manifest 声明的状态和 Location。
  • Gone 返回 410。
  • 未命中返回自定义 404。
  • 所有响应添加 X-Robots-Tag: noindex, nofollow。
  • HTML 添加严格的预览 CSP,阻止未批准远程脚本。
  • 自动测试完成后可以无交互停止。

13. 自动门禁

13.1 URL 与响应

门禁 CLI 分工:gate.ts 对 Artifacts 做静态检查(清单/文件/Hash/路径安全);verify.ts 启动 Preview 后对 Route Manifest 全部路由做真实 HTTP 验证(本节规则与 §14.5)。

  • 当前已知 URL 基线 unresolved=0。
  • 自动请求 Route Manifest 中全部路由,不以 20 条抽样代替全量检查。
  • HTML、Asset、Redirect、Gone 状态符合 Manifest。
  • Redirect Location 有效。
  • Redirect 无环。
  • 多跳大于 1 告警,超过配置上限失败。
  • 404 测试路径返回 404 页面而不是 200。

13.2 页面 SEO

对所有 Indexable HTML:

  • 恰好一个非空 Title。
  • 一个 Meta Description;缺失为错误或按批准策略降级。
  • 恰好一个 Canonical。
  • Canonical Host 和 Path 与 Route 策略一致。
  • 恰好一个 H1;明确豁免模板除外。
  • 页面不是 Noindex。

对 Preview 响应:

  • 必须有 X-Robots-Tag: noindex,但该 Header 不写入正式 Dist 的 SEO 内容判断。

13.3 Sitemap

  • Sitemap URL 全部对应 Indexable HTML Route。
  • 所有 Indexable HTML Route 全部进入 Sitemap。
  • 无 Redirect、Gone、404 和 Asset。
  • Origin 与冻结 Site Config 相同。
  • URL 编码规则一致。

13.4 链接和资源

  • 使用 HTML Parser,不使用单纯正则。
  • 检查 href、src、srcset、Poster 和批准的 CSS URL。
  • 正确处理 Query、Fragment、Base URL 和 HTML Entity。
  • 内部链接必须命中 Route Manifest。
  • 外部脚本、iframe、字体和图片列入远程依赖报告。
  • 未批准远程脚本直接失败。

13.5 文件系统兼容

  • Lowercase Collision。
  • Unicode NFC Collision。
  • 尾斜杠映射冲突。
  • Windows 保留设备名。
  • 尾部点和空格。
  • 路径过长。
  • 文件与目录同名冲突。
  • 编码分隔符和路径穿越。

13.6 数据与构建完整性

  • Build Input Hash 正确。
  • Dist 文件都有 Checksums。
  • Route Target File 存在。
  • Asset Manifest 和实际文件一致。
  • 公开引用媒体无 Missing/Quarantined。
  • 相同 Input 的重复构建业务 Hash 一致。
  • 损坏输入必须失败,禁止兜底。

13.7 报告

每次 Validation 输出:

  • JSON 机器报告。
  • HTML 人工报告。
  • P0/P1/P2/P3 汇总。
  • URL 和文件证据。
  • 规则版本。
  • Build Input/Dist/Manifest Hash。

完整报告保存在忽略目录或 CI Artifact;docs/evidence/ 只保存不含真实客户内容的验收摘要。


14. 测试计划

14.1 单元测试

WXR

  • CDATA、Entity、Namespace。
  • 超大字段、深度、节点数和时限。
  • _thumbnail_id。
  • _product_image_gallery。
  • wppop_sections。
  • Yoast Title/Description/主分类。
  • Post/Page/Product/Attachment/Term/Nav Item。
  • 未知 CPT 和未知 Meta。
  • .html、.php 和多级 Permalink。
  • PHP Serialized 数据深度和长度限制。

URL

  • 大小写不同路径。
  • 有无尾斜杠。
  • Unicode NFC/NFD。
  • Percent Encoding。
  • %2F、%5C 不被错误解码。
  • Query Policy。
  • Windows 保留名称。
  • 路径穿越和超长路径。
  • Redirect 环、链和不存在目标。

媒体

  • 魔数不匹配。
  • SVG Quarantine。
  • 相同文件不同路径。
  • 相同路径不同 Hash。
  • 同名文件多候选。
  • Missing Asset 两个等级。
  • (ZIP 防护测试随 ZIP 支持阶段补充;本切片媒体输入为目录。)

HTML 安全

  • Script。
  • 事件属性。
  • javascript: URL。
  • Data URL。
  • iframe 白名单。
  • 外部追踪像素。
  • 清洗差异记录。

14.2 契约测试

  • UniversalRow。
  • Source Snapshot Manifest。
  • Render Snapshot。
  • Route Manifest。
  • Theme Manifest。
  • Import/Validation Report。
  • 契约版本不兼容时构建失败。

14.3 数据库集成测试

  • 从空数据库运行全部 Migration。
  • Revision 不可更新。
  • Source Snapshot 不可替换。
  • Stable Source Identity 幂等。
  • 第二次导入 0 新 Revision。
  • Publish Pointer 幂等。
  • 已发布 A、导入 B 后仍构建 A。
  • 发布 B 后构建 B。
  • Site A 不可读写 Site B。
  • Route Exact Key 唯一。
  • Media Public Path 唯一。
  • Job 崩溃后标记 Stale。

14.4 构建集成测试

  • Snapshot 物化。
  • Astro 仅从 Snapshot 构建。
  • 数据库/API 停止时仍可从已冻结 Snapshot 构建。
  • 构建断网仍成功。
  • 输入损坏立即失败。
  • 无 Seed fallback。
  • 相同 Snapshot 双构建 Hash 相同。

14.5 Preview 与验收测试

  • 自动遍历 Route Manifest 全部路由。
  • HTML 200。
  • Redirect 301 和 Location。
  • Gone 410。
  • 不存在路径 404。
  • Asset MIME 和 Hash。
  • 查询路由未实现(?p= 类 URL 已登记为 excluded,预览不响应)。
  • Noindex Header。
  • 路径穿越返回 400/404。

人工抽检 20 条作为补充:

  • 10 条内容页。
  • 5 条分类/分页。
  • 3 条 Redirect。
  • 2 条 404/Gone。

14.6 真实数据验收

  • 完整 hesupipe WXR。
  • 完整授权媒体副本。
  • 本地 Sitemap。
  • 输入基数与 Import Report 对照。
  • 运行时间、峰值内存和产物大小记录。

15. 实施阶段

以下周期按单开发者配合 AI、旧 cms 只读复用估算。以阶段验收为准,不以日期强行结束。

阶段 0:输入授权、备份和基线(1–2 天)

任务:

  1. 记录当前 Git Commit。
  2. 确认试点数据使用授权和敏感信息范围。
  3. 备份旧 cms PostgreSQL、WXR、媒体和关键 Artifact 到第二存储。
  4. 验证 storage/、artifacts/、.env* 被 Git Ignore。
  5. 创建脱敏最小 WXR/媒体/Sitemap 夹具。
  6. 记录真实 WXR 的输入基数。

验收:

  • 原项目未被修改。
  • 备份有 Hash 和位置记录。
  • git status 不出现真实试点数据。

阶段 1:Workspace、数据库和契约(3–5 天)

任务:

  1. 建立 pnpm Workspace、TypeScript、Vitest 和 CI。
  2. 建立 PostgreSQL Compose。
  3. 建立 Drizzle Migration。
  4. 实现 §5 核心表和数据库约束。
  5. 冻结 UniversalRow、Route Manifest、Render Snapshot、Theme Manifest。
  6. 写契约测试和空库迁移测试。

验收:

  • 空环境一条命令启动数据库并迁移。
  • 所有核心约束有自动测试。
  • Revision/Snapshot 不可变测试通过。

阶段 2:Source Snapshot 与 WXR Adapter(4–7 天)

任务:

  1. source:register。
  2. Content-Addressed Snapshot。
  3. 流式 WXR 解析。
  4. UniversalRow 映射。
  5. Parser 限额。
  6. HTML 清洗。
  7. 未支持对象报告。

验收:

  • 脱敏夹具全绿。
  • 大文件不整文件读入内存。
  • 恶意/畸形输入安全失败。
  • 解析器不访问网络。

阶段 3:内容、分类、媒体和路由导入(5–8 天)

任务:

  1. Stable Source Identity。
  2. Content Entry/Revision 和 Current Pointer。
  3. Taxonomy/Term/Content Term。
  4. Media Registry/Usage 和安全校验。
  5. Route Registry Exact/Collision 模型。
  6. Import Job 和报告。
  7. 幂等重跑。

验收:

  • 同 Snapshot 二次导入 0 新 Revision。
  • Permalink 变化不创建重复 Content Entry。
  • 媒体歧义不自动匹配。
  • URL 冲突和不支持对象全部进入报告。

阶段 4:显式发布、Render Snapshot 和主题(5–8 天)

任务:

  1. Publish Dry Run/Confirm。
  2. Current/Published Pointer 状态测试。
  3. Build Input Snapshot。
  4. pilot-minimal Theme Manifest。
  5. 公共组件。
  6. Astro 内容、分类、分页、媒体和 SEO 页面。
  7. Sitemap/Robots。

验收:

  • 导入不改变 Published 页面。
  • 显式发布后新修订进入下次 Build。
  • Theme 无数据库/API/Seed 依赖。

阶段 5:Gate、Preview 与可复现性(4–7 天)

任务:

  1. Route/Asset/Build Manifest。
  2. 全量 Validation Engine。
  3. Preview Server。
  4. 全路由 HTTP 测试。
  5. 相同输入双构建测试。
  6. JSON/HTML 报告。

验收:

  • 全部自动 Gate 通过。
  • Preview 只读 Artifact。
  • 路径穿越和远程脚本测试通过。
  • 双构建业务 Hash 一致。

阶段 6:真实试点 L3 验收(2–5 天)

任务:

  1. 注册真实 Snapshot。
  2. 全量 Import。
  3. 修复或显式排除问题。
  4. 再次 Import 验证幂等。
  5. 显式 Publish。
  6. Build + Gate + Preview。
  7. 全量路由自动验证和 20 条人工抽检。
  8. 输出脱敏验收摘要。

验收:

  • §16 全部满足。

预计总周期:约 5–8 周(各阶段区间合计 24–42 个工作日)。真实数据异常、未知插件字段和媒体命名问题可能增加时间。


16. Definition of Done

切片只有同时满足以下条件才算完成。

16.1 输入与导入

  • Source Connection、Source Snapshot、Import Job 语义分离。
  • 真实输入有 Hash、大小、版本和授权记录。
  • 范围内所有对象均有明确结果。
  • 不支持对象有数量、类型和原因。
  • 第二次相同输入导入 0 新 Revision。
  • 导入不发布。
  • HTML 清洗和媒体安全报告可审计。

16.2 内容与发布

  • Revision 数据库级不可变。
  • Current/Published 双指针正确。
  • 已发布 A、导入 B 后公开仍为 A。
  • Publish B 后构建读取 B。
  • 发布操作可 Dry Run、显式确认并写审计。

16.3 URL 与媒体

  • Exact Route Key 不粗暴合并大小写、尾斜杠和编码语义。
  • Collision Key 正确报告跨平台风险。
  • 当前已知 URL 基线 unresolved=0。
  • 不允许无法映射路径默认跳首页。
  • 公开引用媒体无 Missing/Quarantined。
  • 查询路由未实现的事实(?p= 已登记 excluded)与未覆盖来源在报告中明确显示。

16.4 构建

  • Build Input 不可变且有完整 Hash。
  • Theme 只读 Render Snapshot。
  • 构建无网络、无数据库、无 API、无 Seed。
  • 相同 Input 两次构建业务 Hash 相同。
  • Artifact 结构统一并包含完整 Manifest。

16.5 门禁和预览

  • Route Manifest 全量 HTTP 自动验证通过。
  • HTML/Redirect/Asset/Gone/404 行为正确。
  • Dead Link=0。
  • Sitemap 双向一致。
  • Indexable HTML 的 Title、Description、Canonical、H1 通过。
  • Redirect 无环,目标存在。
  • 文件系统和路径安全检查通过。
  • Preview 使用同一 Build 的 Manifest,绑定回环地址并携带 Noindex Header。

16.6 工程质量

  • pnpm lint、typecheck、test、build 全绿。
  • 空数据库 Migration 测试通过。
  • 没有真实数据、Secret、Storage、Artifact 被提交。
  • 关键决策已有 ADR。
  • 生成脱敏 L3 验收摘要。

17. 风险与控制

风险 等级 控制
WXR 类型或字段超出范围 P1 Unsupported 清单、Extras、Adapter 夹具
Permalink 变化造成重复内容 P0 Stable Source ID 与 Source Record 映射
URL 规范化合并不同路径 P0 Exact Key 与 Collision Key 分离
WXR HTML 执行恶意脚本 P0 白名单清洗、CSP、负向测试
媒体模糊匹配错误 P0 完整路径优先、多候选不自动选
Windows 与 Linux 路径冲突 P0 Collision Gate、保留名称和 Unicode 检查
构建读到变化中数据 P0 不可变 Build Input Snapshot
Seed/默认数据进入产物 P0 无兜底代码、负向回归测试
进程中断造成假成功 P1 持久化 Job、Heartbeat、Stale 状态
真实数据误提交 Git P0 Ignore、Pre-commit/CI Secret 和大文件检查
报告只存在本机后丢失 P1 CI Artifact/受控存储 + 脱敏 Evidence 摘要
主题再次硬编码站点数据 P1 Theme Manifest、Render Contract、组件边界
L3 被误认为生产完成 P0 报告显式标记未部署、未完成 L4

18. 切片后的实施顺序

SP1  管理后台 + 平台级 super_admin + MFA
SP2  差量同步、删除检测、人工冲突队列
SP3  HTML 爬虫/Playwright + 日志/GSC URL 补全 + SEO 三方 Diff
SP4  正式 Block/Template/Theme 协议 + 第二主题
SP5  Build Worker/BullMQ + Release/发布锁/原子部署/回滚
SP6  Form API + Lead + 通知重试 + 隐私
SP7  备份自动化、监控、告警和生产 L4 验收

说明:

  • 手工备份、Source Snapshot 和本地 Evidence 保留从本切片开始执行。
  • SP7 是生产级自动备份和监控,不代表此前允许无备份开发。
  • 第一试点正式上线前,SP1、SP2、SP3、SP5 和 SP7 的上线阻断项必须完成。

19. 实施前最终确认项

  1. hesupipe WXR、媒体和 Sitemap 的准确位置及使用授权。
  2. 是否存在需要保留的 PDF、字体、视频或非图片附件。
  3. 试点 WXR 中实际 CPT、Taxonomy 和菜单类型清单。
  4. hesupipe 的生产 Origin 和旧 URL 是否区分大小写。
  5. 首页、产品列表和新闻列表的权威旧路径。
  6. Tag、日期、作者、Attachment 页面使用生成、301 还是 410(已定默认:Tag/日期/作者 → 301 主列表;Attachment 页按 Sitemap/WXR 状态在 html/redirect/gone 中逐条处置——如需偏离默认请提出)。
  7. 未支持 CPT 是否全部进入 Excluded,还是有必须纳入切片的类型。
  8. L3 Evidence 完整报告保存位置。

以上信息缺失时可以先完成框架、契约和脱敏夹具,但不得宣告真实试点 L3 验收完成。


20. 最终结论

本修订版将第一切片控制在可实施范围内,同时避免以下旧 cms 技术债再次进入 SiteForge:

  • Permalink 充当内容身份。
  • 导入后自动发布。
  • 路由塞进 Settings JSON。
  • 大小写和尾斜杠粗暴合并。
  • 实时查 API 构建和 Seed 兜底。
  • Preview 读取变化中的数据库。
  • 媒体按文件名静默猜测。
  • WordPress HTML 未清洗。
  • 主题页面继续硬编码站点数据。
  • Build/Import 状态只存在内存。

完成该切片后,项目将得到一个真正可复用的核心:

不可变来源快照
+ 幂等导入
+ 不可变内容修订
+ 显式发布指针
+ 精确 URL 注册表
+ 安全媒体清单
+ 冻结构建输入
+ 最小主题契约
+ 可复现 Astro 构建
+ 全量自动门禁
+ Artifact 自包含预览

这条主链达到 L3 后,再建设后台、差量同步、完整 SEO 扫描、第二主题和生产部署,不需要推翻第一切片的数据与契约。