09. 程序化 SEO 与规模化页面供给 · 资料陈述 · 教学步骤 3

把页面拆成稳定内容模型

稳定结构让内容能够批量生成、局部更新、审核和重跑。

原知识点:Woy的CMS把内容拆成Page、Section、Content与Tag,分别承载公共页面、章节、结构化内容和分类关系。

执行步骤

  1. 先画实体关系:页面解决什么任务,Section承担哪些答案,Content保存哪些事实,Tag只用于哪些分类。
  2. 为每个字段规定类型、是否必填、允许空值、唯一键和校验规则。
  3. 为事实字段增加来源、获取时间、许可、有效期和审核状态。
  4. 写出一条完整示例记录并渲染成页面,检查缺值、冲突和转义。
  5. 给Schema标版本;字段变更先迁移预览数据再进入生产。

可复制工作表

# JSON Schema(复制后把__替换为实际值)
```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "__",
  "title": "RoutePage",
  "type": "object",
  "additionalProperties": false,
  "required": ["id", "slug", "originId", "destinationId", "facts", "status", "schemaVersion"],
  "properties": {
    "id": {"type": "string", "minLength": 1},
    "slug": {"type": "string", "pattern": "^[a-z0-9-]+{{INITIAL_CONTENT}}quot;},
    "originId": {"type": "string", "minLength": 1},
    "destinationId": {"type": "string", "minLength": 1},
    "facts": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["field", "value", "unit", "sourceUrl", "checkedAt", "licenseStatus"],
        "properties": {
          "field": {"enum": ["distanceKm", "durationMinutes", "fare"]},
          "value": {"type": "number", "minimum": 0},
          "unit": {"enum": ["km", "minute", "CNY"]},
          "sourceUrl": {"type": "string", "format": "uri", "pattern": "^https://"},
          "checkedAt": {"type": "string", "format": "date-time"},
          "licenseStatus": {"enum": ["approved", "restricted", "unknown"]}
        },
        "allOf": [
          {"if": {"properties": {"field": {"const": "distanceKm"}}, "required": ["field"]}, "then": {"properties": {"unit": {"const": "km"}}}},
          {"if": {"properties": {"field": {"const": "durationMinutes"}}, "required": ["field"]}, "then": {"properties": {"unit": {"const": "minute"}}}},
          {"if": {"properties": {"field": {"const": "fare"}}, "required": ["field"]}, "then": {"properties": {"unit": {"const": "CNY"}}}}
        ]
      }
    },
    "tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
    "status": {"enum": ["draft", "review", "published", "withdrawn"]},
    "schemaVersion": {"const": "__"}
  }
}
```

# 数据库Schema
```sql
CREATE TABLE route_page (
  id TEXT PRIMARY KEY,
  slug TEXT NOT NULL UNIQUE,
  origin_id TEXT NOT NULL,
  destination_id TEXT NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('draft','review','published','withdrawn')),
  schema_version TEXT NOT NULL,
  UNIQUE(origin_id, destination_id)
);
CREATE TABLE page_fact (
  page_id TEXT NOT NULL REFERENCES route_page(id),
  field TEXT NOT NULL CHECK (field IN ('distanceKm','durationMinutes','fare')),
  value_json TEXT NOT NULL CHECK (json_valid(value_json)) CHECK (json_type(value_json) IN ('integer','real')) CHECK (CAST(value_json AS REAL) >= 0),
  unit TEXT NOT NULL CHECK (
    (field='distanceKm' AND unit='km') OR
    (field='durationMinutes' AND unit='minute') OR
    (field='fare' AND unit='CNY')
  ),
  source_url TEXT NOT NULL CHECK (source_url LIKE 'https://%'),
  checked_at TEXT NOT NULL CHECK (
    checked_at GLOB '????-??-??T??:??:??Z' AND
    datetime(checked_at) IS NOT NULL AND
    strftime('%Y-%m-%dT%H:%M:%SZ', checked_at)=checked_at
  ),
  license_status TEXT NOT NULL CHECK (license_status IN ('approved','restricted','unknown')),
  PRIMARY KEY(page_id, field, source_url)
);
```

# 示例记录
```json
{"id":"__","slug":"__","originId":"__","destinationId":"__","facts":[{"field":"distanceKm","value":0,"unit":"km","sourceUrl":"https://__","checkedAt":"__","licenseStatus":"approved"}],"tags":["__"],"status":"draft","schemaVersion":"__"}
```

校验命令/工具:__
预期通过:__
预期失败记录及错误:__
迁移前版本/记录:__
迁移步骤:__
迁移后版本/记录:__
回滚命令/条件:__

完整示例

示例(ID、命令与数字仅演示可复现填法):
Schema采用模板中的RoutePage,$id=https://schemas.example.test/route-page-2.0.0.json,schemaVersion=2.0.0;数据库迁移为db/migrations/021_route_page_v2.sql。JSON Schema把distanceKm、durationMinutes、fare都限定为非负数字,并用条件规则绑定km、minute、CNY;SQLite同时校验合法JSON、非负JSON数值、字段—单位、HTTPS来源和UTC真实日历日期;日期解析后必须格式化回原字符串,不能让SQLite把2月30日静默归一成3月2日。

完整通过记录:
{"id":"r-sh-hz","slug":"shanghai-to-hangzhou","originId":"sh","destinationId":"hz","facts":[{"field":"distanceKm","value":176,"unit":"km","sourceUrl":"https://data.example.gov/route/sh-hz","checkedAt":"2026-07-31T08:00:00Z","licenseStatus":"approved"}],"tags":["rail"],"status":"draft","schemaVersion":"2.0.0"}

校验命令与结果:

  1. npx ajv-cli validate -s schemas/route-page-2.0.0.json -d fixtures/route-valid.json --spec=draft2020 → valid。
  2. sqlite3 qa/route-v2.db < db/migrations/021_route_page_v2.sql,再导入route-valid.json → 1条写入且唯一、JSON类型、单位、来源和日期约束全部通过。
  3. npm test -- route-page-render.test.mjs route-page-constraints.test.mjs → 有效记录与9类负例共10/10通过。

列较多时可左右滚动;首列会固定,便于逐行比较。

失败fixture/测试注入错误预期错误实际结果发布门禁
route-missing-source.json删除facts[0].sourceUrlmissing required property sourceUrlAJV在facts[0]报同一错误保持draft
route-wrong-value-type.json把distanceKm的value改成字符串“176”value must be numberAJV在facts[0].value报同一错误拒绝导入
route-wrong-unit.jsondistanceKm使用unit=minuteunit must be equal to constant km条件Schema在facts[0].unit报同一错误拒绝导入
route-invalid-json.sql直接写入value_json='not-json'CHECK json_valid(value_json)SQLite CHECK constraint failed: json_valid拒绝写入
route-invalid-date.sqlchecked_at写2026-02-30T08:00:00Zstrftime往返结果必须仍等于原UTC字符串SQLite把它解析为03-02,往返相等CHECK失败拒绝写入
route-negative-value.sqlvalue_json写-1数值必须>=0SQLite CHECK constraint failed: CAST(value_json AS REAL)>=0拒绝写入
route-duplicate-pair.json用不同slug重复sh→hzUNIQUE(origin_id,destination_id)SQLite UNIQUE constraint failed拒绝导入
route-conflict.json两个approved来源给distanceKm 176与181FACT_CONFLICT,需人工选择渲染器返回409并列出两个sourceUrl不生成生产HTML
route-escape.jsontags[0]含<script>alert(1)</script>文本转义且脚本不执行快照为&lt;script&gt;,DOM中script节点0允许预览

迁移前快照route-v1-20260731.jsonl有120条,旧字段distance为“176 km”。执行node scripts/migrate-route-v1-to-v2.mjs --input route-v1-20260731.jsonl --output route-v2.jsonl --report migration-v2.csv:116条拆成数字value与匹配unit并通过,4条不可解析进入review,0条静默丢弃。随后运行node scripts/validate-route-v2.mjs route-v2.jsonl与上述数据库/渲染测试,116/116通过;只有签字后的记录才从draft转published。回滚制品为快照route-v1-20260731.jsonl和reader-v1镜像;任一丢记录、负数/真实日历日期约束失效、冲突静默覆盖或渲染测试失败即执行deploy rollback reader-v1并恢复只读快照。负责人为数据工程师吴,复查日2026-08-02。

完成清单

  • JSON Schema和数据库都约束非负事实值、字段—单位、必填、唯一、枚举、来源与真实日历日期。
  • 包含完整通过记录,以及错误类型、负数、非法JSON和无效日历日期等具有预期错误的失败记录。
  • 迁移前后结构、数据转换、验证命令和回滚条件齐全。
  • 约束与渲染测试能证明缺值、类型、数值范围、日期、冲突和转义行为符合发布门禁。

适用条件

页面类型和章节类型需有稳定语义,不能退化成无约束富文本。

来源:SEO友好的AI原生 CMS 的思考和实践