Files
cms-website/docs/adr/ADR-018-主题与构建决策.md
T
浅念 87d6b9a777 docs: ADR-018/README/Plan 5 移交清单
Plan 4 Task 9 收尾(阶段 4 收官):

- ADR-018(主题与构建决策八条):Astro 5.18 选型与 MIT/依赖只进主题包/
  vitest 双版本(根 2.1.9 + 主题 3.2.7——astro vite6 兼容);渲染信任链
  bodyHtml 不二次清洗(三段接力);分页 V1 不做——路由登记义务随迁移移交;
  P0 发布门禁偏离(issue 无 DB 源,Plan 5 SEO diff 重审);编码路径四层
  字节精确 + 资产键 URL 线形归一(236f3dc);预览零依赖 + host 白名单
  收紧(回环纪律,比 spec §12.2 更严);CSP style-src 'unsafe-inline'
  (Astro scoped 样式内联);报告 HTML 先写两域对齐
- README:快速开始补 publish→build→preview 全链命令链示例(含
  artifacts/jobs/<jobId>/ 产物路径);目录结构补 themes/ 与
  apps/preview-server/;当前状态阶段 4 完成(真实 hesupipe 190 页可
  浏览器预览/408 测试/23 验收断言/6 截图);ADR-001~018 导航;下一步
  指向 Plan 4 文末移交清单
- Plan 4 文档:Task 9 勾选 + 验收节 ✅ 全过;文末新增"移交 Plan 5 清单"
  7 项(SEO 三方 diff 新输入源/完整门禁六件含可复现双构建与 footer 年份
  固定化/sitemap-robots 预览可达/og 元数据/836 nginx conf 输出/Task 0.2
  备份续传/度量基线 ~64s 首跑·~15s 复跑)

三绿全量:lint 过 / typecheck 五段过 / test 408(根 382 + 主题 26)全绿。
不 push——推送由用户执行。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 15:59:48 +08:00

44 lines
8.6 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.
# ADR-018:主题与构建决策(Astro 选型/渲染信任链/分页不做/P0 发布门禁偏离/编码路径贯穿/预览回环与 CSP/报告写序)
- **状态**:已采纳(Plan 4 Task 9 收尾,阶段 4;实现锚点 `themes/pilot-minimal/` · `apps/api/src/modules/build/` · `apps/api/src/modules/publish/publish.service.ts` · `apps/preview-server/server.mjs`;验收证据 `docs/evidence/phase4-preview.md`——真实 hesupipe 190 页浏览器预览,23 断言 + 6 截图全过)
## 决策一:Astro 5.18 选型与依赖布局(MIT)
1. **主题引擎 = Astro 5(锁定 5.18.x minor,实装 5.18.2)**:静态优先(零客户端运行时)、`[...slug].astro` 单入口 + `getStaticPaths` 读输入目录的路由模型与"route-manifest 是唯一权威"的落盘规则天然咬合、文件级布局组件(BaseLayout/SeoHead/RichText)贴合六类页面分派。**许可证 MIT**(前置事实 6 的版本纪律复查通过),依赖只进 `themes/pilot-minimal/package.json`(根 workspace 不引入——构建工具不污染 API/测试面)。真实数据规模实测:190 页纯 astro 构建 4.9-7.5s。
2. **vitest 双版本并存(根 2.1.9 / 主题 3.2.7)**:主题测试要 `import .astro` 必须挂 astro 的 vite 插件(`getViteConfig`),astro 5.18 携 vite 6——根 vitest 2.x 系不兼容,故主题包独立 devDependency `vitest@^3.2`。两套 runner 各跑各的 package(根 `vitest.config.ts` 不含 themes,主题自有 `vitest.config.ts`),`pnpm test` 串联(382 + 26 = 408)。**不把根升 3.x**:根栈(pg 临时库 globalSetup/长时集成用例)在 2.1.9 上稳定,为一个子包升根属无收益风险面。
3. **主题五禁的结构保证**:渲染数据唯一入口 `src/lib/load-render-data.ts`(只读 `SF_BUILD_INPUT` 目录)——不查库/不调 API/不读源快照/无 seed/构建期断网由入口单一性 + 集成测试五禁断言(含 DATABASE_URL 置空、fetch/net stub)共同锁定。
## 决策二:渲染信任链——bodyHtml 不二次清洗
4. **主题端 `set:html` 直出,不清洗**(`RichText.astro`):bodyHtml 的信任链是三段接力——① Plan 2 适配器 HTML 白名单清洗(ADR-009,入库存的就是清洗产物);② Plan 4 Task 2 快照物化期 URL 重写(`rewriteBodyHtml`——仅做媒体 URL 到根相对 public_path 的映射改写,命中面以 media ready 集合为界,未命中保留原样 + issue);③ 主题经 load-render-data 唯一入口消费。三段之外**无第四个写方**,主题端再清洗是冗余面(且会破坏与 source_rendered 的逐字节对拍——Plan 5 SEO 三方 diff 的比较基准)。**禁"顺手加固"**:任何在主题层加清洗/改写的行为都属破坏信任链,须先升版本评审。
## 决策三:分页 V1 一律不做——路由登记义务随迁移移交
5. **列表页单页全列**(/news/ 129 条、taxonomy 列表同;`Pagination.astro` 保留为禁用态接口组件):分页路由(/news/page/2/ 等)不在 route_registry——未登记路由即不可达,硬做分页会**自破门禁①死链**(分页链接指向无路由路径)。V1 不做的完整语义:主题不做、管线不造、门禁不查。**移交义务**:Plan 5 起做分页必须**先扩路由登记**(route_registry 造行 → routes.json → dist → manifest 全链),路由登记是分页的前置义务而非主题层决定——该义务随迁移移交 Plan 5+,触发时连门禁死链白名单一并定。
## 决策四:P0 发布门禁偏离(前置事实 8②)
6. **V1 publish 不读导入报告文件做 P0 阻断**(`publish.service.ts` 头注记):spec §8 语义是发布前拒绝未解决 P0 issue,但 import issue 只落报告文件(artifacts/)、**无 DB 查询源**——publish 要阻断只能重读报告 JSON,而真实数据 206 条 issue 现状下严格门禁会**反堵发布**(其中大量是源数据固有形态而非阻断级问题)。真实防线不缺位:missing 媒体等在 build/gate 层有实物防线(artifact-completeness/死链/SEO 门禁 + validation_runs 落库)。**Plan 5 SEO 三方 diff 时重审**——届时 issue 分级(P0-P3)落地,publish 门禁改读 DB 化的 issue 源。
## 决策五:编码路径贯穿规则 + 资产键编码归一(236f3dc 修补)
7. **全程编码原样(字节精确)**:routes.json → dist 文件路径 → route-manifest → preview 匹配,四层对百分号编码路径(真实 54 条如 `/%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac/`)**不解码不归一**。理由:解码归一是歧义面(编码大小写/二次编码/非法序列),V1 无消费方需要解码形态;Windows/NTFS 下 '%' 是合法文件名字符,编码原样落盘无障碍。解码归一归 SP3 验证器(届时作为校验规则而非数据改写)。
8. **资产键例外——URL 线形归一**(验收内实证修补 236f3dc):route-manifest 资产行 exact_key 若用文件名本体(裸全角括号/顿号/间隔号),浏览器对非 ASCII **一律百分号编码后发请求**,裸键永不命中 → 预览破图(生产面 nginx 先解码不受影响)。修补:`pipeline.toManifestRoute` 把 exact_key 归一为 URL 线形(可打印 ASCII 原样、其余 UTF-8 百分号编码大写 hex),target_file 仍盘上裸名、DB/渲染快照域不变。配套不变量:manifest 全键纯 ASCII(e2e 断言 ⑦c)。同源修补:预览 Location 头 ASCII 化(`asciiLocation()`——RFC 9110 头域是 ASCII,裸全角会 ERR_INVALID_CHAR 崩 500)。
## 决策六:预览零依赖 + host 白名单收紧(比 spec 更严)
9. **零新依赖**:预览服务器 `apps/preview-server/server.mjs` 纯 node:http(.mjs 不进 TS 链,类型面由相邻 server.d.mts 供给)——匹配器(exact_key + query_key)、MIME 表、流式文件服务(createReadStream 直 pipe,64MB RSS 平坦实测)全手写;route-manifest 是唯一输入。
10. **host 白名单比 spec §12.2 更严**:spec 允许"除非显式指定"其他地址,实现收紧为**只允许 127.0.0.1/localhost**——库级在服务构造点即拒(先于监听),CLI 级先于一切(含 DB 访问前)拒绝。理由:预览产物带 `X-Robots-Tag: noindex` 但无鉴权,回环纪律是唯一防线;"显式指定对外地址"没有正当用例,留口子只会被误用。要对外 = 走部署器(Plan 7 的领域)。
11. **安全面全套**:穿越预检先于 URL 解析(new URL 会规范化点段——实测洞)、realpath + lexical 双检防 junction 逃逸、目录即 404 禁列表、仅 GET/HEAD、全响应 noindex。
## 决策七:CSP style-src 'unsafe-inline'(Astro scoped 样式兼容)
12. **预览 CSP = `default-src 'self'; style-src 'self' 'unsafe-inline'`**:Astro 5 静态产物把 scoped 样式**内联为 `<style>` 元素**(无外部样式表),`default-src 'self'` 会拦掉全部内联样式——页面在浏览器裸奔(HTTP 级测试看不见,浏览器时刻才显影,Task 8 实证)。只放开样式面:脚本/连接/图片等仍走 default-src 'self'。**不换 Astro 配置规避**(external stylesheet 会改变产物形态与可复现面),接受这一条最小放行并在此记档。
## 决策八:报告 HTML 先写——两域对齐
13. **build 报告与 import 报告同序:HTML 先写**(`report-html.ts` + build.service 落盘点):build.json 在则 build.html 必在——HTML 渲染失败时磁盘不留"JSON 新终态 + HTML 旧终态"的窄窗口(Plan 3 移交项⑤的兑现,两个域从此同纪律)。HTML 渲染面全动态文本转义、静态骨架无 script(Plan 2 Task 6 模板纪律)。
- **理由**:主题层"最薄可信渲染"(信任链三段接力 + 唯一入口)、预览层"最窄暴露面"(零依赖 + 回环 + 只读两文件)、门禁层"实物防线优先于台账阻断"(P0 偏离的真实防线论证)——三条线索共同把复杂度压在数据侧(快照冻结),消费侧全部薄层。
- **权衡/陷阱**:① 分页不做意味着 /news/ 129 条单页——真实站可浏览性达标(验收过),但内容量再涨一个量级前 Plan 5 必须落路由登记义务;② P0 发布门禁偏离是**有意欠账**——issue DB 化前 publish 无阻断,靠 build/gate 层兜底,Plan 5 重审时若引入 publish 阻断须先做 issue 分级落库;③ 编码路径"原样"与资产键"归一"并存不矛盾——前者是四层贯穿的不解码纪律,后者是键的**构造规则**(进 manifest 前就写成 URL 线形),排查时先分清键在哪一层;④ vitest 双版本是过渡态——根升 3.x 的时机是 vitest 2.x 出安全分支停维护或根需要 vite 6 特性时,届时一次性迁移并复跑全套。