这是本节的多页打印视图。 .
设计与开发
- 1: 架构契约
- 2: 组件契约
- 3: 外壳与导航契约
- 4: 落地页契约
- 5: OINK 迁移边界
-
6: 设计决策
- 6.1: 警告与安全回退
- 6.2: 配置模型
- 6.3: Markdown 优先创作
- 6.4: 生成式配置 Schema
-
7: 设计研究
- 7.1: Goldmark 块属性实测
- 7.2: 消费站与迁移证据
- 7.3: OINK 全面审查(2026-08-26)
-
8: 设计提案与 PRD
- 8.1: 反向链接与知识图谱
- 8.2: 媒体收敛
- 8.3: Agent 批量索引
- 8.4: Book 出版链路
本专栏公开随 OINK 1.0.0 正式发布的维护者契约,兼容性下限为 Hugo Extended
0.160.1。持续测试只使用一个固定的 Hugo Extended 工具链,当前为 0.165.0;
兼容性下限不再单独作为矩阵测试项。唯一的中英文契约源文件位于本站仓库的
content/docs/design/。
本专栏是 OINK 可长期维护的设计记录。站内其它专栏按任务讲解如何搭建站点; 这里集中说明现行不变量、这些选择背后的理由、用于比较方案的证据,以及仍处于 候选阶段的工作。
如何阅读本专栏
| 层次 | 含义 |
|---|---|
| 契约 | 兼容实现必须保留的规范性行为 |
| 决策 | 用于解释现行行为的已接受理由与边界 |
| 研究 | 带日期且不具规范性的证据,必要时应重新验证 |
| 提案 | PRD 与 RFC 草案;公开在这里不代表已经实现 |
契约目录
| 契约 | 权威范围 |
|---|---|
| 架构契约 | 构建、配置、诊断、本地化、特色图片、输出、安全、无障碍与性能 |
| 组件契约 | 组件 API、Book 与发布原语、校验和输出降级 |
| 外壳与导航契约 | 导航、搜索、博客展示、操作、分类法与页尾组合 |
| 落地页契约 | 落地页数据、22 种区块注册表、运行时、无障碍与输出 |
| 迁移边界 | 从 0.4 到当前版本所支持的内容与配置迁移 |
设计记录
以后所有 OINK PRD 或 RFC 都必须以中英文页面对的形式放入
content/docs/design/proposals/,不得再在仓库中创建 plan/、plans/ 或
proposal/ 目录。提案被接受后,应同步更新实现、对应检查器与相关契约,把稳定
理由沉淀到“设计决策”,并通过 Git 历史与变更日志退出草案。
权威来源与维护
本目录同时管理英文与中文维护者设计文档。主题仓库管理可执行事实:hugo.yaml
管理公开默认值;对应的解析器与检查器定义可选结构;layouts/ 与 assets/
管理渲染行为;检查脚本与 tests/goldens/ 管理验收;VENDOR.json 管理内置
依赖的版本、许可证、文件与校验和。
公共行为发生变化时,必须在同一次交付中更新实现、对应检查器以及本目录下相关 契约的中英文版本。测试应验证行为和输出,不应只固定某段文字。
1 - 架构契约
这是随 OINK 1.0.0 正式发布的架构契约。本页是权威中文源文件,与英文版本
一同维护在 content/docs/design/。
仓库与装配
仓库根目录是一个完整的 Hugo 模块与主题,不是站点,也不是 npm workspace。
Hugo Extended 负责编译 SCSS 与模板。浏览器运行时与第三方资源都已提交到仓库,
因此普通构建不会访问网络。公开的双语文档、示例与浏览器测试位于同级的
oink.pgsty.com 仓库;主题仓库只在 tests/site/ 中保留范围明确的内部回归
夹具,不再维护独立的公开示例面。
生成的 public/ 与 resources/ 目录绝不是源文件。随主题内置的运行时、字体
家族与 Font Awesome 字形定义属于受支持的发行内容,并非待清理的死代码;
VENDOR.json 与 bin/check-vendor.py 固定其完整性。OINK 发布完整的受支持
Font Awesome 发行包,因为用户编写的内容可能使用主题模板本身没有引用的图标。
Font Awesome 官方编译 CSS 作为一份稳定、带指纹的 vendor 样式表发布,并排在由
主题与消费站 SCSS 编译出的指纹 main.css 之前。站点样式的普通修改不会再让图标
发行包失效,同时常规层叠顺序仍允许站点覆盖它。KaTeX、DocSearch、Swagger 与
Asciinema 等能力样式继续保持独立,只在实际使用时加载。内容指纹使不可变 URL 成为
可能;HTTP 缓存响应头属于部署宿主,而不是 Hugo 主题的职责。
Hugo 类型 docs、book、blog 与 swagger 选择阅读外壳;
params.ui.shell_types 可以增加类型。落地页使用 layout: landing。OINK 没有
article 类型或第二套博客外壳;沉浸式页面只是外壳契约
定义的一种博客展示方式。
layouts/_partials/shell/config.html 解析共享外壳事实。布局必须先通过
content/render.html 渲染,再执行 scripts.html,因为渲染钩子与 shortcode
会在 Page Store 中登记能力标志。覆盖时应选择范围最窄的 partial;若合并会改变
Hugo 的查找优先级,即使几个基础模板看起来相似,也应保持分离。
配置与诊断
主题策略位于 params.ui.*;comments.giscus、plantuml、drawio 等包含多项
设置的集成保留在顶层。布尔功能直接使用布尔值,除非它还包含多项设置。页面级
覆盖会去掉 ui. 前缀:params.ui.image_zoom 对应 image_zoom,front matter
中绝不嵌套 ui map。hugo.yaml 声明公开默认值;对应的解析器与检查器定义
任何可选配置的结构或范围。
无效输入遵循同一条规则:警告中写明输入值、允许的结构与安全回退,然后使用该
回退,或省略不安全的功能。普通 hugo server 因而仍可使用,而所有发布门禁都
使用 --panicOnWarning。主题绝不调用 errorf,check-params.py 会强制守住
这条边界。不要为无法到达的状态增加臆测式校验。
OINK 没有通用的键名重命名注册表。仍需给出迁移诊断的过渡,应在所属解析器中 添加针对性警告,并配严格的反向测试;已经移除的键绝不能作为兼容路径继续读取。
可能联网的功能必须显式启用,并以关闭方式降级。PlantUML 需要
plantuml.svg_image_url,Draw.io 需要 drawio.drawio_server,Algolia 需要
appId、apiKey 与 indexName;配置不完整时发出警告,而且不产生网络请求。
Draw.io 只在渲染内容含 PNG 或 SVG 候选图片时加载,并且每个不同的图片 URL
只检查一次。
界面本地化
此处语言扩展描述的是特性分支。在后续版本标签可以通过 Go Proxy 解析之前, 它还不是已发布模块的能力。
OINK 为
google/docsy@64f51c5
中现有的 31 个 locale 文件名提供原生界面文本,并额外保留通用 zh 作为简体中文
默认值:
这是一项兼容范围,不代表运行时依赖 Docsy,也不声称消费站点编写的正文已经翻译。 Docsy 以后增加的 locale 不会自动成为 OINK 支持项;它必须先补齐完整的 OINK 词条,并接受与现有语言相同的审校。
i18n/en.yaml 管理 192 键 schema。OINK 的 32 份语言包都必须拥有完全相同的键集
与原生界面文本;只有经过审查的产品名、标点、通行缩写或目标语言真实同形词可以
与英文保持相同,不再生成整段英文 fallback。zh 与 zh-cn 使用简体中文,
zh-tw 使用繁体中文。
在兼容下限 Hugo 0.160.x 上,如果同时存在地区化的中文语言包,作为非默认语言的
通用 zh 语言键必须显式设置具体的 locale: zh-CN;从 Hugo 0.161 起,该配置也能
解析裸 locale: zh。这项约束只影响语言配置,不改变语言包文件名 i18n/zh.yaml。
%s、{count}、{{ .Count }} 等运行时占位符可以移到符合目标语言语法的位置,
但字节内容必须保持不变。所有取值都是标量。语言包不得包含隐藏的双向文本控制符;
阿拉伯语、波斯语和希伯来语的方向仍由消费站点的语言设置(direction: rtl)
决定,不得把方向字符塞进译文。
bin/check-i18n.py 会检查 locale 集合、schema、取值类型、占位符、方向控制符,
以及少量已审查的英文本地同形词。因此增加可见字符串时,必须在同一变更中为每份
语言包提供译文,不能再运行 fallback 生成器。
特色图片
Hugo 的 images 是唯一的创作 API;params.images 只作为全站社交卡片回退。
| 来源 | 阅读列表缩略图 | 社交卡片 |
|---|---|---|
页面 images,或页面包中的 **featured*、*feature*、{*cover*,*thumbnail*} |
是 | 是 |
分区 cascade.images |
是 | 是 |
站点 params.images |
否 | 是 |
images: [] 会清除显式值或 cascade 继承值,但不会禁止发现页面包资源。只把解析
到的第一张图片作为代表图。Hugo 可以裁剪本地可处理的位图;SVG、static 与远程
资源仍然有效,只是不能执行 Hugo 图片操作。
featured-image-resolve.html 统一决定来源优先级与相对、绝对 URL。页面自己的
包资源优先于继承的 cascade 图片。列表缩略图、Open Graph/Twitter/schema
帮助模板、作者头像、Pinterest 图片与博客展示都消费同一个决定。
params.ui.featured_image 只用于博客,默认值为 none;页面或 cascade 可用
front matter 覆盖。banner 在单页标题上方渲染图片,wash 用图片给页头着色,
hero 在单页与分区索引上把图片绘制为外壳背景。缺少图片或使用非 HTML 输出时
不渲染图片。
输出与运行时
每个基础模板都会设置 Page.Store.tdOutputFormat:
| 输出 | 契约 |
|---|---|
| HTML | 完整的语义内容;只为实际用到的能力加载本地运行时 |
| 展开的内容;不含外壳导航、搜索或图片缩放运行时;共享操作层仍支持明确的打印控制 | |
| Markdown / LLMS | 保持源 Markdown 形态,不含 td- 组件标记 |
| LLMSFULL | 按顶层 section 选择启用:每个启用 section、每种语言一份 llms-full.txt,按阅读顺序拼接同一份 Markdown |
| RSS | 安全的静态摘要,或明确省略 |
| NAVJSON | 按站点选择启用:每种语言一份 navigation.json,序列化侧栏与 pager 已经在读的导航权威 |
| BookManifest | 选择启用、供出版打包器消费的有序 JSON 交接;绝不冒充 EPUB 或 PDF |
站点自行选择是否启用自定义输出;OINK 不会强制生成昂贵的整书聚合。HTML 加载 共享操作层、核心层,以及由页面 flag 选择的稳定第一方能力分片。需要模板化的能力 每种语言至多发布一份;flag 只决定引用哪些 script tag,绝不再生成新的组合 bundle。 Print 保留操作层,并且只加载渲染打印功能所需的运行时。大型第三方 UMD 文件保持 独立;未使用的功能运行时不会出现。
顶层 section 在自己 _index front matter 的 outputs 中列出 LLMSFULL 才会启用它,
主题绝不替站点把它加进输出集合。逐页 Markdown 与全文包由同一个渲染器产出,因此全文包
就是那份语义 Markdown(同样不含 td- 组件标记)按侧栏与 pager 的阅读顺序拼接。在顶层
之下启用会告警且不产出任何文件,普通构建仍然可用,而 --panicOnWarning 会拦住发布。
站点在 outputs.home 中启用 NAVJSON,为每种语言在语言根下发布一份 navigation.json。
它序列化侧栏与 pager 所读的同一条权威链:存在显式 data/docs_nav.json 树时用它,否则用
带 weight 的内容树。数组顺序就是契约,weight 绝不序列化,该输出标记为 notAlternative。
schema/nav.v1.schema.json 为该格式提供版本,它是手写的契约产物,随模板与检查器一同修改,
不受生成式配置 Schema 漂移门禁管辖。两种输出默认关闭,都不启用的站点构建结果逐字节不变;
bin/check-agent-indexes.py 是它们的归属检查器。
只有 Book 根在 outputs 中明确列出 BookManifest 时才会生成它。它引用该 Book
既有的逐页 Markdown,并记录派生出的页面顺序、标题、编号目标与 xref;主题不会在
其中猜测出版元数据,它也不是可分发的电子书。
主题仓库提供 bin/book-epub.py 与 bin/book-pdf.py 作为显式出版步骤,并用
bin/check-book-epub.py 与 bin/check-book-pdf.py 承担产物门禁。EPUB 打包器组合
BookManifest 与同一份整书 Print HTML,消费站另行传入出版 metadata;PDF runner
只在临时回环地址提供该 Print 产物,通过 script-src 'none' 内容安全策略调用显式指定的
Chrome/Chromium 二进制,输出带 CSS 页码的 A4 页面。两种工具都会拒绝缺失资源或越出构建树的资源;网络资源
与覆盖已有输出分别需要独立的显式开关。网络 opt-in 只允许被动 HTTP(S) 媒体,远程脚本与
本地文件协议仍属非法。EPUB metadata 文件中的相对资源以该文件所在目录为基准,不依赖
调用者的工作目录。普通 Hugo 构建不会执行出版工作;PDF 仍从 Print 派生,而不是另一种
模板输出。
性能规则如下:
- 若站点级资源或
partialCached结果可以承担工作,不要为每一页遍历.Site.Pages; .Content只渲染一次,完成后再读取 Page Store 标志;- 直接输出正确标记,不要扫描 DOM 后再修复;
- 浏览器工作按资源 URL 分组,而不是按 DOM 实例重复;
- 成本显著的普通输出应保持选择启用;
- 默认不输出 Speculation Rules:必须先由一个明确的生产消费站用可回滚的
moderate实验测量Sec-Purpose: prefetch请求、实际命中导航、传输字节与 CSP 影响; - 校验确实可达的作者输入,不校验假想的内部状态。
bin/measure-baseline.py 测量构建时间、输出体积、bundle 数量与 shortcode
密度。bin/sites/build-all.py 在隔离快照中构建维护范围内的消费站点。
信任边界、CSS 与无障碍
作者可以启用 Goldmark unsafe,但配置与组件参数不能视作原始 HTML。共享属性
策略使用允许清单、校验 class token、放行 data-* 与 aria-*,并在丢弃
style、srcdoc、on*、保留属性与未知属性时发出警告。需要本地 URL 或明确
绝对 URL 时,URL 帮助模板会拒绝危险协议与协议相对 URL。公开 API 承诺支持的
远程 URL 仍然可用,但构建时绝不抓取它们。
主题输出使用 td- class、data-td-* 属性与 --td-* 自定义属性;.steps、
.cards、.full-width 等作者标记保持无前缀。CSS 支持 RTL、打印、强制颜色、
减少动画、超长 token 与窄视口。主题拥有的装饰图标带 aria-hidden;只有包含
任务列表或原始 Font Awesome 元素的页面才加载作者内容无障碍修复。
字体角色为 ui、body、heading、code、display、meta 与 print,
通过 --td-*-font-family 暴露。ui 是主字体:body 经它解析,heading 又经
body 解析,因此赋一次值即同时移动界面、正文与标题。params.ui.typography
可取 technical 或 system;两者编译到同一份样式表,不加载运行时。旧
Bootstrap/Docsy Sass 变量继续为这些角色提供初值。
params.ui.fonts 让配置层触达同一组角色,供不愿挂载 SCSS 或新增样式表的站点
使用。它只写字体族名,绝不加载字体文件:所写字体族必须是读者已有的,或站点
自己用 @font-face 声明过的,这也让该键留在网络契约之外。取值只放行纯粹的
字体族语法,输出的 :root 块由匹配到的片段重新拼装;未知角色或不安全取值只
告警并单独丢弃。该块在样式表之后渲染,正是这一点让作者字体在同等优先级下压
过预设。外壳读站点的字体,不自带字体:Book 的编号与题注用正文字体,而非某种
技术字体。
强调色按角色拆开。强调文字(链接、外链、行内代码)跟随 Bootstrap 链接
族与 --bs-code-color,主题色永不重声明它们;行内代码是固定的胭脂红明暗对,
使一页密集的标识符读成「代码与正文」而非「代码与链接」。强调底(选中行、
指针划过导航行时那层更灰的底、hover 淡铺、目录药丸与轨道光点、徽章 hover、
卡片 hover 时的外边、分享按钮 hover 时的实心底、文本选中、焦点环)跟随
--td-accent、--td-accent-rgb 与 --td-accent-hover,它们默认取链接族,也是
params.ui.theme_color 唯一注入的属性。属于外壳而非正文的文字同样跟随它们:
视口正停在其上的目录锚点、以及指针或键盘焦点落在其上的 Book 章节小标题,
按分区颜色点亮,而不是链接蓝。theme_color 与 theme_color_dark
取 #rgb/#rrggbb;front matter 与分区 cascade 覆盖站点值。未配置的站点不注入
任何内容。解析失败的值告警并保留默认配色。解析成功但在主题自身画布上低于 4.5:1
的颜色,带可抑制 id 告警并照常生效:该检查是建议性的,只有解析失败才丢弃颜色。
亮色是主键:没有有效 theme_color 的 theme_color_dark 告警并被忽略,一页要么
两种模式都着色,要么都不着色。省略暗色一半时,向白按 4% 步进提亮,直到在暗色画布
上达到 4.5:1。注入的每个字节
都由解析出的整数通道格式化,绝不来自作者文本。同一个解析器同时回答 head 注入块
与侧栏根切换器的「这一页是什么颜色」。
发布状态
源码完成、本地验证、提交、打标签、推送、消费站点固定版本、部署与生产一致是彼此 独立的状态。一次本地 Hugo 构建只能证明本地验证通过。
2 - 组件契约
这是随 OINK 1.0.0 正式发布的组件契约。本页是权威中文源文件,与英文版本
一同维护在 content/docs/design/。
教程与完整示例位于面向读者的组件专栏。本页定义这些 指南所依赖的 API 与行为。
创作模型
一个区块加属性便能表达组件时,使用普通 Markdown;需要复合正文或 Markdown 无法携带的事实时,使用 shortcode。OINK 没有并行的组件注册表。原生形态要求:
只有 {{%/* steps */%}} 使用百分号分隔符,因为它的正文属于页面大纲;其它
shortcode 一律使用尖括号分隔符。复合正文通过 content/render-block.html
处理,并使用唯一的 ID 作用域。Shortcode 与组件参数中的 caption、label、title
和 name 是纯文本,Markdown 应放在正文里。落地页叙述字段遵循自己的契约。图标
由一对 Font Awesome class 表示。组件暴露安全的 class 与属性,不接受任意颜色
或内联样式。
公共 API
OINK 有 29 个 shortcode:
- 核心:
tabs、tab、steps、cards、card、fields、field、include、kbd、badge、param、comment、contributors、asciinema; - Book:
fig、tbl、eq、eg、xref、book-toc、book-figures、book-tables、book-equations、book-examples; - 发布:
release-card、release-assets、download; - OpenAPI:
swagger、redoc。
| 组件 | 原生形态 | Shortcode 形态 | HTML 运行时 |
|---|---|---|---|
| 提示块 | > [!TYPE]、折叠、{icon=} |
无 | 无 |
| 标签页 | 相邻围栏或表格加 {tab= group= value=} |
tabs / tab |
只在使用页加载 tabs |
| 步骤 | 有序列表加 {.steps} |
steps |
无 |
| 卡片 | 链接列表加 {.cards} |
cards / card |
无 |
| 参数表 | 表格加 {.fields} |
fields / field |
无 |
| FileTree | filetree 数据围栏 |
无 | 只有注释存在时加载分隔条运行时 |
| 画廊 | gallery 数据围栏 |
无 | 符合条件时共享图片缩放 |
| 图片 | Markdown 图片加块属性 | 无 | 符合条件时加载图片缩放 |
| 表格 | 属性、caption、编号或标签页 | 复合 Book 表格使用 tbl |
只有标签页表格加载 tabs |
| Book 目标 | 图片、表格、passthrough、围栏加 {num=} |
fig、tbl、eq、eg |
无 |
| 发布资产 | checksums 数据围栏 |
release-assets |
HTML 中加载复制功能 |
| 图表与数据 | mermaid、plantuml、markmap、math、chem、echarts、infographic 围栏 |
无 | 只加载选中的本地运行时 |
校验
无效的作者输入遵循架构契约:发出警告,使用文档
规定的安全回退或省略组件,再由 --panicOnWarning 在发布门禁中把同一条诊断
变为致命错误。命名参数与位置参数不能混用。Book 目标 ID 匹配
[A-Za-z][A-Za-z0-9_.:-]*,Book 编号匹配 [0-9A-Za-z.-]+,class 必须通过
token 校验。渲染钩子与 shortcode 目标共享同一个页面注册表,因此冲突不会生成
重复的输出 ID。
URL 使用 content/url.html。图片依次从页面资源、分区资源、全局 assets、static
或显式远程 URL 中解析。本地位图带固有尺寸;SVG、static 与远程来源仍然有效,
但不能执行 Hugo 图片操作。
组件行为
提示块与标签页
提示块类型包括 note、tip、important、warning、caution、success、
danger、question、example、quote 与 details;- 表示初始折叠,+
表示初始展开。未知类型会以中性提示块保持可见,不依赖 JavaScript。
只有连续且区块类型相同的相邻标签页才会分组。group 启用
#<group>-<value> hash 与 td-tabs:v1:<group> 存储键;未分组标签页两者都不用。
HTML 在 JavaScript 运行前暴露所有面板,打印输出展开面板,Markdown 保留作者
源文,RSS 接收渲染后的文本摘要。完整形态支持任意 Markdown;tab.label 必填,
父级存在 group 时 value 才严格必填,孤立的 tab 会警告且不渲染。
步骤、卡片、参数表与表格
原生步骤接受普通区块内容。只有某一步必须包含百分号容器时才使用 shortcode。
原生卡片是链接列表;完整形态增加正文、徽章、图标与图片。原生参数表把第一列
映射为名称、最后一列映射为描述,中间列由 meta= 或表头映射;完整形态允许
区块描述。card 与 field 只能放在各自的父容器中。
参数锚点为 field-<name>,名称转小写,连续标点折叠为连字符,因此
params.ui.typography 变成 field-params-ui-typography。重复锚点追加位置后缀。
表格渲染钩子负责响应式包装与 caption。.matrix 把第一列变为行表头;
.full-width 加宽普通表格或矩阵表格。.fields 不能与 matrix、full-width、
编号或标签页组合;编号与标签页也互斥。
图片、画廊、FileTree 与围栏
Markdown 图片钩子是普通图片 API。行内图片保持行内;块图片带 caption 或 num
时变为 figure。图片处理只属于这一原生形态:完整 fig 源形态是编号容器,其参数表
刻意不含 command/options,需要处理的编号图片写成带 num 的原生块图片。
允许的图片属性包括 id、num、caption、width、height、
link、command 与 options,以及共享安全属性。command 与 options 必须同时
出现,并对可处理的本地资源调用 Hugo Fit、Resize、Fill 或 Crop。普通
链接图片使用 Markdown 语法,因此 link 属性要求同时有 caption 或编号。链接
图片与装饰图片不加载缩放。
画廊每行接受一张 Markdown 图片,可带描述、链接与 class。FileTree 接受缩进、
- name、可选 /、注释,以及经过校验的 icon、tone、open、type 属性。Markdown
保留作者源文;打印输出渲染展开的静态图片与文件树。
所有代码高亮都使用 Chroma。通用围栏属性包括 title、copy、wrap、
collapse、label、id、行选项、标签页,以及 Book 的 num/caption。复制
操作返回作者源文。ECharts 输入是声明式 JSON/YAML;回调使用
window.OinkEchartsFunctions 中的 $fn:<name>,绝不执行嵌入脚本。
Book
book 类型扩展 docs 外壳,并遵循内容树或 data/docs_nav.json。book_number、
book_part、book_kind 与 book_status 是展示元数据,不改变 Hugo 发布状态。
带编号的类型为 fig、tbl、eq 与 eg,默认 ID 是 <kind>-<num>。eg
需要 caption;不带 num 的 eq 是无编号展示公式。xref 要么准确指定一种类型
并可附带 page/anchor,要么指定一个 anchor 和显式文字。带编号的示例是一个
完整的边框正文与 caption。
脚注属于页面文档。原生编号表格与围栏会让脚注留在页面里。Shortcode 正文是独立
的 Goldmark 文档,因此 tbl、eg、fig、card、tab、field 或 include
中的脚注引用会警告并保持字面形式;该检查忽略代码形态的文本。
book-toc 按 1–3 层导航顺序生成目录;四个 book-* 索引各自收集一种目标。
单页 Print 与普通 HTML 保持完全相同的普通标题与脚注 ID。只有多页分区
Print 与整书 Print 会改写跨页链接,并给这些页面局部标题与脚注增加命名
空间,避免聚合后冲突;显式目标 ID 保持不变。消费站点自行选择是否启用这些
潜在成本较高的聚合输出。
发布与下载
发布 front matter 使用一个
https://github.com/<owner>/<repo>/releases/tag/<tag> 形态的 release_url;owner、
项目与 tag 来自 URL,日期来自页面。构建不会抓取远程发布状态。已经移除的
release map、release_products 与 release_group_by_product 会警告并给出
替代项,它们不是兼容路径。分区索引列出所有页面;能解析时使用 project tag,
否则使用页面标题。
校验和可以接受规范行,也可以接受一个源资源,两者不能同时提供;文件名不能是 路径。HTML 增加本地复制功能,静态输出暴露完整 hash。
下载使用 data/download/<key>.yaml。channel 可取 rolling 或 pinned;只有
pinned URL 与命令会插值 ${version} 和 ${tag}。发布前,rolling channel 保持
可用,pinned channel 显示 pending。Markdown 渲染完整 channel 列表;RSS 省略
该组件。
验证
共享输出规则见架构契约,例外随各组件定义。 Markdown 与 RSS 不设置浏览器运行时标志;Print 只保留渲染打印功能需要的标志。 源码检查覆盖参数、渲染钩子策略、运行时隔离与迁移;输出检查比较 HTML、Print、 Markdown、RSS 与 LLMS golden;浏览器测试覆盖交互界面。迁移行为见 迁移边界。
3 - 外壳与导航契约
这是随 OINK 1.0.0 正式发布的外壳与导航契约。本页是权威中文源文件,与英文版本
一同维护在 content/docs/design/。
权威来源与导航
| 关注点 | 权威来源 |
|---|---|
| 全局导航 | Hugo menus.main |
| Docs / Book 侧栏与翻页 | 内容树或 data/docs_nav.json |
| 根栏目切换器 | 解析后的顶层内容根 |
| 内容发现 | 各语言的本地搜索索引 |
| 页面与命令面板操作 | 共享操作注册表 |
任何功能都不能引入另一套菜单或页面树。菜单只允许一层子项交互;更深层级会警告,
并平铺到带链接的分组标题下。外部链接使用
target="_blank" rel="noopener noreferrer";内部链接保持语言与子路径感知。
顶部导航栏的桌面视图与抽屉视图投影同一棵树,每个下拉面板都是一列宽度适中的
“图标 + 标题"行——mega 面板与其 columns 菜单参数已退役,配置 columns
会发出警告并保持单列。菜单描述只是配置数据,不再渲染。链接树在任何宽度都保持居中:
lg 以上是文字链接,之下收缩为图标链接。lg 与 md 之间,右端保留搜索、版本、
语言、主题与 GitHub,没有菜单按钮;md 以下这些工具移入底栏工具组,此时首页
与显式 Landing 页在搜索旁增加一枚抽屉入口,展开完整的带标签菜单树;其余宽度
与页面一律不渲染抽屉入口。语言链接指向页面译文,缺少译文时
指向对应语言首页;多个语言共享主机与 base path 时保持相对链接,只有语言拥有
独立 baseURL 时才变成绝对链接;hreflang 始终使用绝对链接。
navbar_autohide 从 768px 起只对精细指针生效,绝不作用于触控或抽屉宽度;
隐藏的导航栏不交还占位:两种状态下布局都保留导航栏横带,固定顶栏正好占满这条
横带、下边框画在带内,显现时原地淡入、不遮挡静止内容,hero 页面忽略该策略、
保留自己的叠加导航栏。首页与 hero 页面共用同一套柔和边界:导航栏不画下边框、
滚动时不投阴影,改由栏下一小段渐隐过渡收束边缘。
侧栏与翻页共享同一个根和顺序。manual_link、build.render: link、分隔行、
隐藏节点与占位节点保留各自已定义的语义。sidebar_icon_policy 可取默认的 all、
groups 或 none;图标是一对 Font Awesome class。无效策略遵循共享的警告与
回退契约。
沉浸式博客展示
OINK 没有 article 类型或第二套外壳。沉浸式阅读由普通博客外壳上的四个独立键 组成,可设在页面或分区 cascade 上;分区索引会重复它自己也需要的值:
博客外壳默认不渲染面包屑导航——文章应作为独立作品阅读——所以这份配置不需要
相应的键。breadcrumb 仍是普通键,页面或 cascade 可以在任何外壳上明确打开
或关闭它。
hero 在单页与分区索引上把共享特色图片用作装饰性的全出血背景。没有图片时
渲染普通开场;banner 与 wash 仍只用于单页。顶部导航栏以对比遮罩叠在 hero
上,并随页面一起滚动。
toc_style 可取 fixed 或 flow;flow 在文章旁放置更宽的导轨,并且只在滚动
之后固定。它的静止位置与文章信息行对齐;页面没有信息行时,与描述对齐。标题
换行数无法预知,因此由 docs-shell.js 测量偏移;没有 JavaScript 时,导轨从
文章起点开始。toc_taxonomies: false 移除术语云;导轨既无 TOC 又无术语云时
完全不渲染。notoc 仍是页面级 TOC 退出键。这些开关不改变署名、标签、系列、
翻页顺序、feed 或页尾组合;导轨在 xl 断点以下消失。
搜索、操作与运行时
params.offline_search 选择启用各语言的本地索引。启用后默认也在 hugo server
期间构建;大型编辑循环可以设置 offline_search_on_serve: false。HTML 搜索出现
在首页、外壳页面,以及启用 landing_search 的落地页上。其它非外壳页面与 Print
不包含对话框、Lunr 或命令面板。
搜索元数据包括 search_keywords、默认值为 1 的 search_boost,以及
search_exclude。索引携带 URL、标题、分类法、摘录、小标题、description、
正文或摘要、根、分区、类型、关键词、boost、面包屑导航与图标。夹具预算为原始
2 MiB、gzip 512 KiB。站点可以通过 hooks/search-keywords-extra.html 返回额外
字符串。
内置操作 ID 包括 copy_markdown、copy_link、open_chatgpt、open_claude、
view_markdown、view_history、edit_page、create_child_page、create_issue、
create_project_issue、print_section、print、switch_theme、
switch_language、switch_version 与 open_github。分享栏之外的 copy_link
只出现在命令面板中。站点通过
languages.<lang>.params.ui.command_palette.commands 配置的命令可以打开安全
URL,或调用内置 ID,绝不能注入 JavaScript。
命令面板有空状态、文本搜索状态与 > 命令状态;快捷链接来自导航。它没有历史、
语义搜索、个性化或远程回退。搜索查询留在浏览器内,默认不发送遥测。
OinkSurfaceCoordinator 协调命令面板、抽屉、根栏目、语言与版本菜单。各界面自行
管理焦点恢复与 Escape。键盘导航会忽略可编辑控件与模态框:/、\、f、c
打开搜索或命令;j/k 移动标题;q/e 翻页;h 改变展示方式;l/y、
t、r 分别打开语言、主题与根栏目选项。侧栏 WASD/方向键导航使用真实焦点,
不会改写 Tab 顺序。
页面大纲从同一套标题模型与滚动容器计算后的 scroll-padding-top 推导光标和可见
标题范围;SVG 线条与圆点共享同一组动画值,不会漂移。禁止增加臆测性的 DOM
修复遍历。
分享
params.ui.share 默认为空,可接受 16 个目标的任意有序子集:x、bluesky、
mastodon、facebook、linkedin、reddit、hackernews、telegram、
whatsapp、line、pinterest、weibo、chatgpt、claude、email、copy。
页面列表会替换继承列表;share: false 退出。未知项会警告并丢弃。只有普通页面
渲染分享栏;Print、Markdown 与 RSS 省略它。
目标是携带页面永久链接与标题的普通 intent 链接,外加本地 copy_link 按钮。
Pinterest 图片来自共享特色图片解析器。ChatGPT 与 Claude 接收构建期生成的永久
链接提示,与页面菜单里的助理操作相互独立。Discord 没有公共 intent 目标,因此
有意不提供。
分享栏不加载平台 SDK、iframe、脚本、样式表、计数器或 campaign 参数;只有读者
主动点击链接时才产生请求。它是一行带无障碍标签的字形。
share/items.html 解析目标,share/bar.html 负责渲染。
注记
页面注记在 annotation-items.html 中解析描述项,再通过
page-meta-lastmod.html 渲染;两者都可以做窄范围覆盖。各行顺序如下:
| 行 | 条件 |
|---|---|
| 最后修改 | 已设置 Lastmod |
| 上游 | front matter 中的 upstream_link 非空 |
| 翻译 | 配置的权威语言存在译文,而且本页包含作者正文 |
upstream_link 是页面级事实;cascade 有效,upstream_link: "" 表示退出。
其它上游事实按站点参数 → data/upstreams[upstream_source] → front matter 解析:
upstream_name、upstream_copyright、upstream_license、upstream_notice,
以及可选的 upstream_ref、upstream_modified。存在链接时,前四项必填。无效或
残缺的署名会警告,而且不渲染法律声明;不支持的 URL 会被拒绝。发布门禁通过
--panicOnWarning 拒绝这类警告。
upstream_modified 改变署名动词并链接提交历史,不增加新行。notice 页面承载
完整的许可证与免责声明。翻译说明通过 params.ui.translation_notice 选择启用,
以页面键 translation_notice 参与 cascade,跳过生成页面或无正文页面;以本语言
原创的页面可以用 translation_notice: false 关闭。
作者与系列
博客文章页头依次为标题、信息行、术语徽章、作者署名、系列条;description 在其后
引出正文。信息行 article-info.html 始终包含日期;启用 reading_time 后再增加
字数与分钟数。Front matter 的 upstream_link 与注记使用同一个页面级事实,
并在共享 URL 策略保护下增加本地化的原文链接。术语行只是裸徽章组,分类法名称
位于分组标签中,不显示前缀。术语徽章静止时是浅中性底与弱化文字,前置该分类法的
term 图标;可点击徽章在 hover 或 focus 时才取得当前分区的强调色淡铺、边框与文字。
图标词汇表由 taxonomy-icon.html 独家拥有——每个
分类法配一对图标:整体分类法一枚、单个术语一枚(folder-open/folder、
tags/tag、cubes/cube、users/user-pen、series 用
book-bookmark/book,其余用 shapes);params.ui.taxonomy_icons 可覆盖:
字符串同时作用于两个表面,taxonomy/term map 分别设置;无效输入警告并保留
内置。右栏词云只在云头戴整体图标:云 chip 与术语归档筛选条保持"文本 + 计数”——
分类法已经亮明身份,再在每个 chip 上重复图标只是噪声。作者署名只放人物——头像、姓名与个人资料的一行简介——
不带标签或日期。列表行、卡片与术语归档共享同一形态的元数据行:日期、一条本地化
的作者与分区短语,以及由同一个 reading_time 开关控制的字数和分钟数。句子下方
是独立成行、自动换行的徽章行,按分类法字母序列出页面在全部分类法下的词条,每枚
徽章佩戴各自的 term 图标;卡片排除 authors——其句中已具名。
只有声明 taxonomies: {author: authors} 才启用作者。作者 term 页面拥有显示名称、
摘要、正文与特色图片头像;没有 profile 时,回退到链接标题、首字母与归档。
authors-resolve.html 在文章页头、列表行中保留 front matter 顺序,并为每位作者
生成一个 RSS dc:creator。没有 authors 时,旧 author 保持原样;两者同时
存在时,authors 无警告胜出。自定义作者分类法复数名按普通分类法处理。
只有声明 taxonomies: {series: series} 才启用系列。Term 页面拥有引言;不新增
参数、数据文件、封面模型或运行时。页面使用 series: [name] 与可选的
series_weight。series-pages.html 先按 weight 排有权重成员,再按日期升序排
无权重成员,并用 Path 打破平局;系列条与 term 页面共享该顺序。第一个命名系列
得到一条 HTML/Print 系列条。面板是半透明加模糊,而不是一张不透明卡片:hero
文章会把题图铺在这一段背后,不透明底色等于在画面上挖个洞;普通文章上这层色调
就落回页面自身的底色,所以一种处理同时服务两种场景。summary 拥有整行与末端
箭头;系列名连同它的分类法图标,仍是 summary 的兄弟链接,覆盖在一份隐藏的等宽
占位文字上,避免 summary 内出现嵌套交互控件。展开后先划一条细线,再在同一层
表面上把成员阅读顺序放进一个保持 DOM 顺序的自适应网格。每个链接都把序号纳入
点击目标,序号贴在固定方格轨道的末端,因此无论多少篇,标题都对齐在同一条边上;
窄屏保持一栏,只有当每个标题仍有可读宽度时才增加等宽栏,因此桌面面板能用满自身
宽度,也不会把一条选中背景拖过整篇正文。悬停与读者所在位置直接借用侧栏导航
处理这两种状态的同两种底色,当前篇再加上填充序号与加粗标题,不靠颜色单独表意。打印时显示同一份展开
列表,收为单栏。单篇系列与非 HTML 输出省略它。编号、交叉引用与聚合输出仍属于 Book。
默认文章分类法徽章会排除保留的 authors 与 series,因为专属界面已经展示
它们。显式设置 params.taxonomy.page_header 可以恢复任意一项。
博客索引与页面组合
博客分区索引使用 params.ui.blog_index:默认的 list 与 cards 都是按最新优先
排列的一段扁平结果,共享 blog_index_size 分页;元数据行已经显示日期,所以不再
需要年份标题。table 把整个分区显示为日期、标题、标签行,不分页。卡片使用共享
首图、本地化日期/作者/分区元数据、标签与三行摘要。Term 与 taxonomy 页面保持
行列表。
params.ui.blog_index_toggle 为当前分页切片渲染三种形态,并允许读者循环切换。
配置值控制首次绘制,隐藏形态不加载图片。读者存储的选择只作用于发布了全部三种
形态的索引:切换器关闭的分区只发布一种形态,并始终显示它。Front matter 或
cascade 可为每个分区覆盖站点模式。没有切换器的 table 仍是完整且不分页的归档。
params.logo 始终是品牌标志;params.wordmark 或站点标题是紧凑宽度下隐藏的
文字部分。Docs、Book、Blog 与 Swagger 共享一个外壳模型。页尾顺序为分享、反馈、
注记、翻页、评论。Docs/Book 翻页遵循侧栏前序遍历;Blog 按 weight 后接日期倒序;
pager: false 退出。静态输出省略翻页 UI。
每一种实际渲染的页脚形态,都会在最底层栏右侧保留纯图标工具组,顺序为版本、
语言、主题、快捷键帮助。各菜单向上展开;版本触发器不直接显示当前分支或版本名。
胖页脚的折叠箭头排在工具组之后。低于 lg 时,底层栏放弃版权/居中/工具组的
三列布局,改为三行全宽居中堆叠,工具组在最后一行。这些全局控件不再出现在
侧栏底部;footer_style: none 会移除整条底栏。
OINK 没有归档外壳、任意深度飞出菜单、第二个导航权威、查询上传,也没有针对已
移除配置的浏览器兼容 shim。反馈只通过既有 gtag 发出 docs_feedback,在本地
保存选择,而且不替代 Giscus。
验证
bin/check-navigation-contract.py、bin/check-shell.py、JavaScript 测试、输出
golden 与消费站点浏览器套件覆盖导航、语言与子路径链接、博客变体、页尾顺序、
键盘行为、无障碍与响应式布局。
4 - 落地页契约
这是随 OINK 1.0.0 正式发布的落地页契约。本页是权威中文源文件,与英文版本
一同维护在 content/docs/design/。
外壳与数据
任何普通页面都可以声明 layout: landing。它渲染顶部导航栏、全宽画布与页脚,
不显示 docs 侧栏或 TOC 导轨。首页继续把 data/home/<lang>.yaml 作为兼容的创作
路径,并通过同一个渲染器处理。
非首页依次从内联 front matter、data/landing/<key>/<lang>.yaml、单个
data/landing/<key>.yaml 中精确匹配语言的条目,以及英文或无后缀本地数据中
解析 sections。落地页绝不抓取可变事实;星标数、价格、截图与头像必须在 Hugo
运行前提交或生成。
params.ui.landing_search 默认为 true,而且只有启用 offline_search 时才打开
既有本地命令面板。params.ui.github_stars 与 params.ui.alt_site 是可选的本地
界面事实。
区块注册表
注册表恰好有 22 种内置区块:
hero、metrics、capabilities、principles、cards、logo-wall、gallery、testimonials、contributors、faq、markdown、cta;pricing、pricing-compare、command-box、steps、timeline、code-plate、preview、case-study、download、bar-chart。
条目可以是类型字符串,也可以是包含 type、key、id、enabled、内联
data 或有意指定的本地 partial 的 map。作者提供唯一 ID,OINK 把它规范为
锚点安全值。未知类型遵循共享的警告与安全回退策略,绝不静默消失;发布时
--panicOnWarning 会拒绝它。内置区块由 landing/ partial 负责;已经移除的
home/ partial 名称不是 API。
preview 通过站点渲染钩子,把 Markdown source 放在 RenderString 输出旁,
因此其内容会登记与 docs 内容相同的运行时。源码面板使用 Chroma,并带默认值为
page.md 的 file 名称。Markdown 输出使用四个反引号包围的 markdown 围栏;
RSS 省略它。面板标签来自主题 i18n。
hero.align 可取 start 或 center。Center 只适用于文本;与图片组合时会警告,
并回退到 start,同时保留图片。download 消费与 shortcode 相同的
data/download/<key>.yaml 结构,不引入第二套 channel、版本、发布或插值模型。
语言、运行时与无障碍
叙述文件可以按语言拆分。共享事实字段依次解析 <field>_<exact language>——其中
- 规范为 _——再解析 <field>_<primary language>,最后解析无后缀字段。
不接受 camelCase 别名。叙述字段通过站点渲染钩子渲染行内或区块 Markdown;复用
为无障碍名称的值会转为纯文本。区块文案属于站点数据;只有主题控件使用 OINK
i18n。
交互式 HTML 设置 hasLanding,从而只按需添加 landing.js。运行时复用
OinkSurfaceCoordinator,负责出现动画、数字递增、复制、紧凑菜单与主题图片
增强。没有 JavaScript 时,服务端输出仍然完整。
跑马灯只用 CSS 复制;副本带 aria-hidden 与 inert,本地化复选框无需
JavaScript 也能持久保存暂停状态。减少动画会停用动画,强制颜色保留控件,主题
图片响应共享主题事件。顶部导航栏的 mega 面板与其 columns 参数已退役:仍然配置 columns 的菜单会告警并保持单列。紧凑菜单使用真实链接
与按钮,不捕获焦点,也不复制桌面导航树。
输出与兼容性
| 输出 | 契约 |
|---|---|
| HTML | 完整静态区块加渐进增强 |
| 静态网格与内容,移除控件 | |
| Markdown | 不带主题 class 的标题、正文、列表、表格与代码 |
| RSS | 省略落地页区块 |
非 HTML 输出不设置 Landing 标志或运行时。根相对链接与资源遵循部署子路径;普通 构建不下载图片。
已经移除的 0.4 组件形态属于迁移工具,不是并行的落地页实现。OINK 不增加价格 周期切换、远程事实 API、热点编辑器、可视化构建器或第二套注册表。既有首页数据 与显式自定义区块 partial 继续有效。
5 - OINK 迁移边界
这是随 OINK 1.0.0 正式发布的迁移契约。本页是权威中文源文件,与英文版本
一同维护在 content/docs/design/。
这是源码与配置指南,不是版本发布流水账。本地源码、提交、标签、推送、消费站点 固定版本、部署与生产一致仍是彼此独立的状态。面向读者的升级流程见 版本升级。
工具范围
bin/migrations/oink06.py 只扫描和自动改写站点内容目录下的 Markdown 文件,
包括受支持的 YAML front matter。它不改写 Hugo 配置、数据文件、布局、资源、
模块或生成输出。TOML/JSON front matter 与有歧义的 Markdown 会连同位置一起报告,
留给人工检查。
默认执行 dry-run;完成后的迁移具有幂等性:
代码围栏不会改写。book_figures.py 保留范围明确的 TPME、DDIA v1/v2 与
pg-internal profile;它不是通用解析器。
从 0.4 内容迁移到当前形态
| 已移除形态 | 当前形态 | 工具键 |
|---|---|---|
alert、details、pageinfo、原始 disclosure |
> [!TYPE] 提示块 |
callout |
tabpane、旧 tab、code-group、code-tab |
相邻 {tab=} 区块,或 tabs / tab |
tabs |
FileTree shortcode 或 {.filetree} 列表 |
filetree 围栏 |
filetree |
Gallery shortcode 或 {.gallery} 列表 |
gallery 围栏 |
gallery |
| ECharts / infographic shortcode | 同名数据围栏 | datafence |
| Docsy 卡片家族 | .cards 列表或 cards / card |
cards |
imgproc、image |
Markdown 图片加属性 | image |
readfile |
include |
include |
围栏 filename= |
title= |
fencetitle |
badge outline= |
移除 outline |
badge |
叶子 example、book-figures kind= |
eg、显式 book-* 索引 |
eg |
| 百分号分隔的 fields | 尖括号分隔的 fields / field |
fieldsdelim |
Docsy _param 占位符与 card header= 高亮 |
Font Awesome / badge / param 或提示块 |
param_placeholders |
| 不支持的旧 shortcode | 报告源码位置,人工检查 | reportonly |
配置与 front matter
以下配置改动需要手工处理;工具可以报告匹配的 front matter 键,但绝不编辑站点 配置。
| 旧配置 | 当前配置 |
|---|---|
offlineSearch* |
offline_search* |
disable_click2copy_chroma |
ui.code_copy,取反 |
content_width |
`reading_width: slim |
github_url |
github_repo |
ui.no_left_sidebar |
ui.sidebar_enabled,取反 |
| breadcrumb 别名 | ui.breadcrumb |
ui.scrollSpy |
ui.scroll_spy,取反 |
ui.showLightDarkModeMenu |
ui.dark_mode.show_menu |
ui.readingtime |
ui.reading_time |
ui.ul_show |
ui.sidebar_expand_levels |
ui.docs_root |
ui.docs_sidebar_root |
ui.pager |
ui.pager_types |
annotation/zoom/keyboard/reading 的 { enable: bool } map |
裸布尔值 |
ui.typography.preset |
ui.typography |
print.disable_toc |
print.toc,取反 |
Prism、rss_sections 与 algolia_docsearch 已移除。Chroma 是唯一高亮器;Algolia
配置为 search.algolia。页面级覆盖会去掉 ui. 前缀。旧 hide_feedback、
hide_readingtime、exclude_search、content_width、camelCase 手工链接与嵌套
front matter ui map 会连同替代项一起报告。
从 0.5 到 0.6
- 用
upstream_link加upstream_name、upstream_copyright、upstream_license、upstream_notice替代upstream_attribution;把downstream_modified改名为upstream_modified。 - 用一个 GitHub
release_url替代releasemap;从发布索引移除release_products与release_group_by_product。 - 博客与默认日期现在采用 ISO
2006-01-02;面向读者的日期继续显式保留time_format_blog或time_format_default。
已移除名称会警告,并采用文档规定的安全回退或不渲染;普通预览可以继续,严格
门禁通过 --panicOnWarning 拒绝它们。blog_index_toggle、
featured_image: hero、toc_style 与 toc_taxonomies 是增量选择启用项,不会
引入内容类型;沉浸式阅读仍使用普通博客外壳。
前置条件与验证
按照组件契约启用 Goldmark unsafe 渲染、块属性与
独立块图片。要使用 \(...\)、\[...\] 或 $$...$$,需要显式启用 passthrough;
Hugo 不会合并主题的 markup 配置。
针对改动的契约,使用固定的 Hugo Extended 0.165.0 工具链运行范围最小的源码与输出 检查;运行时变化时执行 JavaScript 测试,并严格构建根路径与子路径。对于维护范围 内的站点,在桌面与窄视口检查有代表性的 EN/ZH Docs 与 Blog 路由,再分别记录固定 版本、部署与线上一致状态。
6 - 设计决策
决策记录解释 OINK 为什么在多个兼容方案中选择了当前设计。上方五份契约仍是 现行行为的规范描述;实现与归属检查器仍是可执行事实。
OINK 过去把评审、PRD 与执行记录放在本地 plan/ 目录中。这样既不便发现有价值的
推理,也容易让已经放弃的设计看起来仍有权威。已经接受的理由现在统一进入这座双语、
版本化的文档站,与它所支撑的契约放在一起。
决策地图
| 决策 | 解决的问题 |
|---|---|
| 警告与安全回退 | 为什么普通预览能容忍错误输入,而发布仍保持严格 |
| 配置模型 | 配置放在哪里、页面如何覆盖,以及 OINK 为什么不另造配置命名空间 |
| Markdown 优先创作 | 为什么优先使用原生 Markdown,以及 Docs、Blog、Book、Landing 如何延长共享系统 |
| 生成式配置 Schema | 为什么编辑器 Schema 是生成的投影,以及漂移门禁如何阻止第三个配置权威出现 |
记录格式
一份已接受决策应记录背景、选择、后果,以及证明该选择仍然成立的证据。它不重复参数 参考或教程。每份决策都要链接到归属契约与验证面,中英文页面必须同步修改。
决策发生变化时,应在同一次交付中更新实现、检查器、受影响契约与决策记录。旧答案留在 Git 历史和版本变更记录中,不在导航树里并列保留两套“现行”答案。
相关
6.1 - 警告与安全回退
OINK 不调用 Hugo 的 errorf。作者或站点输入无效时,主题发出警告,并使用文档中
明确的安全回退,或者省略无效片段。版本发布与部署构建使用 --panicOnWarning,
因此同一条警告在发布门禁中仍会导致硬失败。
背景
Hugo 把整座站点作为一次事务构建。编辑一页时触发的 errorf 会让该次重建中的所有 URL
都返回错误,包括无关页面和首页。服务器进程仍然存在,修正输入后也会自动恢复,但多人共享
的预览在此期间完全不可用。
警告的开发成本不同。出错的值可以回退,站点其余部分仍可检查,作者也能看到准确消息。
发布构建则不会放过它,因为 OINK 的 CI 与集成门禁都会加上 --panicOnWarning。
决策
校验遵循四条规则:
- 点明无效键和值、允许的形状以及实际采用的回退值。
- 值来自页面 front matter 时带上页面位置;站点级错误不要在每一页重复刷屏。
- 不允许无效值继续参与后续运算。先校验,再用规范化后的值渲染。
- 没有诚实回退时,警告并且不渲染。不能为了继续构建而编造内容、发起网络请求或输出 不安全 URL。
枚举、布尔、CSS 长度与数字的共享校验形状位于
layouts/_partials/validate.html。领域 resolver 可以增加更窄的规则,但必须保留同一套
警告与回退契约。
安全边界
继续构建不等于继续输出危险内容。被拒绝的 CSS 长度要在进入 style 属性之前回退;远程服务
配置不完整时,要在浏览器可能发起请求之前省略组件;不安全的操作 URL 直接丢弃。真正的保护是
坏输出没有出现,而不是 Hugo 被终止。
这也把编辑与发布清晰分开:
| 阶段 | 无效输入的处理 |
|---|---|
hugo server 或普通本地构建 |
警告、回退或省略,其它页面继续可用 |
| CI、版本验收、部署 | 同一警告在 --panicOnWarning 下让构建以非零状态退出 |
后果
- 每个回退值都是公开契约的一部分,必须与主题声明的默认值一致。
- 从“失败”改成“回退”时,测试也必须改变。负向测试要同时证明普通构建存活、警告文案、 渲染后的回退,以及严格构建失败。
- 检查器必须直接验证被拒绝的输出。例如 URL 安全测试应断言危险 URL 没有进入产物,不能把 任意构建失败当作充分证据。
- 渲染产物负责 DOM、属性、顺序与已注入 token 的断言;浏览器套件负责计算后的颜色、尺寸、 间距、断点与交互结果。只要公开结果可以直接观察,检查器就不应冻结某一种 Sass 写法。
- 源码级检查仍用于
errorf等禁止构造,以及产物无法证明的少量拓扑不变量,例如唯一 authority、 唯一 resolver,或有意收窄的 caller set。
验证
本决策的归属参考包括
架构契约、
bin/check-params.py,以及主题夹具与本站的严格构建。
6.2 - 配置模型
OINK 保留 Hugo 原生键与仍有价值的 Docsy 兼容键,把主题呈现和行为放在
params.ui.* 下,并用同名的顶层 front matter 键提供页面覆盖。它不增加
params.oink.* 配置树,也不建立一套遮蔽 Hugo 配置模型的注册表。
背景
OINK 继承了成熟的配置面,又增加了阅读外壳、内容输出和本地交互。早期设计曾尝试把所有 主题自有键迁入一个新命名空间,并在每页一次性解析完整配置字典。这样会在 Hugo 原生键旁边 再造一种语言,使 section cascade 更复杂,迁移规模甚至超过它要控制的行为本身。
现行模型直接体现每一层的归属:
| 层次 | 职责 | 示例 |
|---|---|---|
| Hugo | 站点身份、语言、菜单、输出、分类法、markup、模块 | baseURL、languages、outputs |
| 站点事实与集成 | 仓库、版本、作者、本地搜索、评论、外部服务 | params.github_repo、params.version、params.comments |
| OINK 界面 | 外壳、导航、呈现与本地交互 | params.ui.sidebar_*、params.ui.typography、params.ui.share |
| 页面或栏目 | 对可覆盖站点默认值的局部调整 | sidebar_enabled、featured_image、share |
| 数据文件 | 不是开关的结构化事实与有序内容 | data/landing、data/download、data/docs_nav.json |
决策
配置 API 遵循以下规则:
- 站点事实保留在既有顶层;界面选择归入
params.ui.*。 - 页面覆盖去掉
ui.前缀,其余名称保持一致。section 的cascade可以把这个顶层键应用到后代。 - 一个布尔值足以表达完整政策时使用标量;只有真正存在下级设置时才使用 map。既有 map 可以接受 布尔速记。
- 名称采用正向、snake_case,并按功能分组。密切相关的设置共用前缀,不为此再建一层 resolver。
- 主题默认值声明在主题的
hugo.yaml中。只有静态值会抹掉刻意存在的外壳差异时,模板才可以 推导默认值。 - 每个功能族负责自己的规范化与校验。共享 helper 提供常见形状,但不存在一套悄悄重写任意旧键的 全局兼容注册表。
完整的现行键、类型与默认值统一放在配置参考中。本决策只记录 归属规则,不再维护第二张参数表。
兼容策略
公开键改名时,由归属 resolver 给出定向警告,同时提供迁移说明和负向测试。已移除或拼错的键 不构成永久别名层的理由。Hugo 与第三方原生 camelCase 键继续保留原样;OINK 自有新增使用 snake_case。
页面值通过 Hugo 普通的 front matter 与 cascade 模型解析。OINK 不要求作者在 front matter
里写嵌套 ui: 树,也不承诺合并任意嵌套页面 map。
后果
- 新增公开设置时,必须有声明或明确推导的默认值、归属 resolver、文档,以及正向和负向测试。
- 配置指南链接到唯一参考表,不在各处重复类型与默认值。
- 只有有序或重复事实才值得新增数据结构,不能只因为不想增加参数就造一个 data 文件。
- 无效标量值遵循警告与回退决策。
验证
bin/check-params.py 审计声明默认值、页面别名、警告行为与禁止 errorf 的不变量。公开参考及其
中文对页由集成站的双语和渲染链接检查覆盖。
6.3 - Markdown 优先创作
Goldmark 能保留目标语义时,优先提供原生 Markdown 形态。只有原生形态无法表达真实能力时, 才保留 shortcode。新增内容场景时延长既有外壳和数据模型,不另建一套并行渲染系统。
背景
OINK 同时服务短手册、大型参考文档、发布归档、落地页和书籍。对十一个消费站点、五千多篇 Markdown 的盘点呈现了两个极端:有些页面几乎不用主题语法,有些页面则由大量嵌套 shortcode 与站点自有 layout 拼成。
只为后一类优化的组件 API 会变成私有 DSL;只支持纯 Markdown 又会迫使书籍、富图、标签页和 结构化发布退回站点自有 HTML。真正有用的边界是能力,而不是语法看起来是否新颖。
决策
OINK 按以下顺序设计:
- 原生 Markdown 优先。 列表可以成为 Steps、Cards 或 FileTree 标记;表格可以成为 Fields 或矩阵;blockquote 可以成为 callout;代码围栏、图片与 passthrough 块通过渲染钩子携带属性。
- shortcode 只补能力。 CommonMark 缩进、嵌套容器、处理选项或跨页登记无法安全表达同一结果时, 才保留全量 shortcode 形态。
- 语义实现只有一套。 原生形态与全量形态进入同一组规范化 partial 和输出契约,不能只是两种 外观相似的组件。
- 沿一条系统延长。 新 Landing 区块进入 section 注册表;新 Blog 呈现仍是 Blog 变体;Book 编号接入内容原语与导航系统。OINK 不为一个功能再造第二套卡片、落地页、导航或 Article 外壳。
- 事实不藏在呈现字符串里。 版本、仓库、日期与有序记录来自 front matter、站点参数或数据文件。 shortcode 参数不能成为第二个事实来源。
输出契约
只有在每种已启用输出中都得到明确语义结果,一种创作形态才算完整:
| 输出 | 要求 |
|---|---|
| HTML | 服务器端先输出完整语义内容,JavaScript 只做增强 |
| 静态、展开,不包含依赖交互的控件 | |
| Markdown / LLMS | 保持源码形态的正文、链接、列表、表格与围栏,不泄漏组件 HTML |
| RSS | 安全的静态内容,或者明确省略 |
这一要求避免一个漂亮的 HTML-only 组件悄悄破坏 Agent 输出、订阅源或整书打印。
信任与呈现
渲染钩子与 shortcode 使用明确的属性白名单。不安全 URL scheme、内联事件处理器和任意 style 输入会被丢弃。只有在文档明确规定、下游站点 CSS 已属于既有创作契约的表面,才接受作者 class。 图标使用一对 Font Awesome class;OINK 不再发明第二种图标 ID 语言。
后果
- 提议新组件时,必须先说明 Markdown 加既有渲染钩子为什么不够。
- 保留全量 shortcode 时,必须点明它独有的能力,并测试两种形态进入相同的规范化输出。
- 外壳变体使用相互独立的呈现键,因此启用 Hero 或流式大纲不会改变分类法、订阅源、翻页顺序或 内容类型。
- 消费站证据是带日期的研究,不是永久冻结偶然语法的理由。当前公开面仍由 组件契约与外壳契约定义。
验证
主题的组件、Book、输出与 golden 检查器先验证创作契约,本站的双语示例与浏览器套件再完成集成 验收。原生形态背后的 Goldmark 事实记录在 块属性研究中。
6.4 - 生成式配置 Schema
schema/ 下的两份 JSON Schema 由 bin/generate-config-schema.py 从主题的
hugo.yaml 与模板读取点投影生成,手工编辑无法通过 CI。Schema 是既有权威的
只读投影,不是第三个配置权威。
背景
主题已有两个配置权威:hugo.yaml 在注释旁声明每个默认值;check-params.py
的读取点扫描知道模板实际消费的每一个键。编辑器对两者一无所知,作者只能凭记忆
敲 params.ui.* 和 front matter。
JSON Schema 能给编辑器补全与悬浮文档,风险在于 Schema 悄悄变成会漂移的第三个 权威。任何手工维护的 Schema 都终将与实现脱节,而脱节的补全比没有补全更危险。
决策
bin/generate-config-schema.py 在 schema/ 下生成两个文件:
site-params.schema.json 校验站点的 hugo.yaml(类型与默认值取自主题自己的
hugo.yaml,描述取自其注释块);front-matter.schema.json 校验页面 front
matter(模板作为创作面读取的全部键,描述继承自对应站点键)。仅为提示「已重命名
或已移除」而读取的键按名排除。
两个刻意的克制成为决策的一部分:
- front-matter Schema 不带类型约束。多个键在站点类型之外还接受裸布尔退出
(
share: false、theme_color: false);对合法输入画红线比没有提示更糟。 hugo.yaml读取器只解析该文件实际使用的形态——嵌套映射、标量、行内列表。 读不懂的构造是硬错误,超出能力时漂移门禁会大声失败而不是错误生成。
后果
改变 Schema 的唯一途径是修改 hugo.yaml 或扫描所读的模板:公开配置面变化时,
Schema 在同一次提交中随之再生,不存在需要单独记得维护的第二份清单。代价是
生成器与读取点扫描成为公开配置面的隐含门禁——新增参数键必须能被它们理解,
否则 CI 直接失败。
验证
python3 bin/generate-config-schema.py --check 在内存中重新生成,schema/
过期或缺失即失败;主题 CI 把它放在参数契约检查旁边运行。编辑器接入方法与
行为描述的规范位置是配置总览。
7 - 设计研究
研究记录测量了什么、使用了哪些输入与工具版本。它可以解释决策,但不能覆盖当前契约或实现。
只有其他维护者能够检查方法、理解边界并复现相关检查时,研究才适合进入公开 Design 内容树。 原始 Agent 对话、临时构建日志和本机绝对路径不符合这一标准。
研究地图
| 记录 | 证据 |
|---|---|
| Goldmark 块属性 | 支持的 Hugo 下限版本上,渲染钩子能看到什么,以及 CommonMark 容器的边界 |
| 消费站与迁移证据 | 带日期的语料盘点与确定性 Book 迁移结果 |
| 2026-08-26 全面审查 | 实现、配置、输出、安全、测试、性能与文档审查 |
发布规则
研究记录必须说明日期、输入、相关版本、方法、结果与已知边界。容易变化的数字明确标为快照。 涉及外部框架的比较,公开前要依据一手资料重新核验,并提炼成与 OINK 有关的结论,不能直接 复制成竞品目录。
7.1 - Goldmark 块属性实测
这些探针在 Hugo Extended 0.160.1 与 0.164.0 上得到字节一致的相关输出。它们解释 OINK 的原生组件形态;当前组件契约仍是权威。
方法
探针使用一个不带 OINK 模板的最小 Hugo 站点。渲染钩子把上下文字段与 .Attributes 输出为
可见标记。站点开启 Goldmark 块属性、行内与块级数学 passthrough 分隔符,以及为检查原始 HTML
而刻意启用的 unsafe 渲染,并设置 wrapStandAloneImageWithinParagraph: false。
每种源码形态分别用兼容下限版本和当时的当前 Hugo 版本渲染,再逐字节比较相关产物。以下结论 记录平台行为,不涉及视觉样式。
结论
| 源码形态 | 钩子结果 | 设计意义 |
|---|---|---|
含段落、围栏、callout、嵌套列表并以 {.steps} 结尾的有序列表 |
class 落在最外层 <ol>,列表项中的富块内容完整保留 |
Markdown 列表可以成为 Steps 原生形态 |
| 列表项内标题 | 标题保留在 <li> 内,并进入 .TableOfContents |
原生 Steps 可以携带可导航标题 |
以 {.filetree} 结尾的嵌套列表 |
class 落在最外层 <ul> |
FileTree 不需要只为保持层级再包 wrapper |
独占图片加 {#id num= caption= .class} |
render-image 收到 IsBlock=true 和全部属性 |
Book 图可以有原生图片形态 |
| 段落中的行内图片 | IsBlock=false,图片收不到块属性 |
行内图片不能使用块级 figure 契约 |
块级公式加 {#id num=} |
render-passthrough 收到 block 类型与属性 |
编号公式可以使用原生 passthrough 形态 |
表格加 {.fields #id num= caption=} |
render-table 收到 class 与命名属性 |
Fields、矩阵、题注和 Book 编号可以共享一个钩子 |
代码围栏加 {#id num= caption=} |
code-block 钩子收到属性 | 围栏本身可以成为编号示例 |
callout 加 {icon= tab=} |
blockquote 钩子同时收到 callout 元数据与属性 | 折叠、标题行内标记、图标和 tab 元数据可以共存 |
| 属性行与目标块之间隔一个空行 | 属性会静默消失 | 源码检查必须拒绝孤立属性行 |
两张相邻表分别带 tab= |
每个 table 钩子收到自己的 tab 标签 | 相邻块 tab 机制可以扩展到代码围栏之外 |
容器边界
Hugo 的 % shortcode delimiter 会把 .Inner 渲染成 Markdown,但模板必须在内部 Markdown
前后各输出一个空行。缺少任一空行时,后续列表可能被当作 HTML block 的字面内容,而不是 Markdown。
把多行 % 容器放进 CommonMark 列表项还有更硬的限制:生成的 HTML 不会随列表内容缩进,列表会在
容器之前闭合,并在容器之后重新开始。因此,当步骤中必须放另一个全量容器时,OINK 仍保留全量
Steps 形态。普通富块、围栏与 < shortcode 不受这一限制。
在相关收集器形态中,嵌套 % shortcode 收到的也是已经渲染好的内部 HTML。需要保留子项原始
Markdown 的收集器应使用 < delimiter,再通过共享的作用域块渲染器处理捕获到的正文。
属性归属
钩子能看到某个属性,并不等于它自动成为公开属性。每个钩子拥有文档明确的白名单。style 与内联
on* 处理器会被拒绝;携带 URL 的值必须经过共享 URL 策略。只有下游 CSS 已属于既有扩展机制的
表面,才保留站点 class。
实验还表明:gallery 列表项中的图片可以被视为块图,却仍不知道父列表带有什么 marker。因此运行时 要么依赖主题显式输出的标记,要么保留一条窄的结构兜底,不能假设图片钩子能看到任意祖先。
边界与验证
这些结果只覆盖 Hugo 0.160.1、0.164.0 与上述 Goldmark 设置。修改设置的站点或未来 Hugo 版本不在 承诺范围内。调整 Hugo 兼容下限时,应先重跑组件、Book、表格、gallery 与 Markdown 输出检查,再更新 这份快照。
7.2 - 消费站与迁移证据
这些计数描述 2026 年 8 月被检查的仓库。它们是设计选择的证据,不是实时产品指标或兼容承诺。
语料
创作语料盘点扫描了十一个 OINK 消费站点的 content/ 树:共 5,325 个 Markdown 文件,其中
5,293 个带 YAML front matter。样本同时包含单语言英文与中文参考站、双语产品站、发布归档、
自定义落地页,以及独立的 Book 消费站。
盘点刻意测量源码 Markdown,而不是生成后的 HTML。统计项包括 shortcode 调用、代码围栏属性、 callout、表格 marker、原始 HTML、front matter 键、内容类型与站点自有 layout。随后针对五个 长篇内容消费者又做了一轮 Book 专项盘点。
改变设计的结论
| 证据 | 形成的选择 |
|---|---|
| 内容从近乎纯 Markdown 到大量嵌套组件同时存在 | 原生 Markdown 是默认形态;只有明确能力缺口才保留全量形态 |
| 文档、Blog、Landing、发布与书籍反复在站点侧重做导航或卡片 | 延长共享外壳、注册表和内容原语,不增加并行系统 |
| 站点自有表格 class 很常见,匹配 canonical Fields 表头的表格却很少 | 钩子属性使用白名单,但保留文档明确的站点 class 扩展点;不能从任意二列表格猜测 Fields |
| Book 站各自拥有图、表、公式、示例和交叉引用约定 | 编号原语与迁移 profile 必须确定性分类、保留稳定 ID,并验证渲染目标 |
| 站点同时存在单语言、对页双语和生成式语言内容 | 必须明确语言权威与生成边界;迁移不能把未跟踪的生成树当作源码 |
| 富 HTML 页面仍要提供 Print、Markdown、订阅源和 Agent 输出 | 接受交互 HTML 之前,每个组件先声明所有输出中的降级行为 |
证据也否决了若干看起来诱人的新增项:文档站不足以支撑第二套 Landing 系统;Book 站不需要新封面 组件;连载归档不值得增加独立 shell type;远程 API 采集属于站点侧 CI,而不是承诺本地构建的 Hugo 主题。
块与表格证据
针对十一个站点与 Book 消费者的专项盘点共发现 11,484 张 pipe table。只有 11 张已经匹配严格的
Fields 表头词汇,约 874 张属于参考型表格,约 1,300 张属于兼容矩阵。因此 OINK 采用显式
.fields 与 .matrix marker,不按表格形状猜测语义。
同一轮盘点在十一个站点中发现 18 个 Steps 块,它们都使用带标题和富内容的全量形态。平台探针表明,
原生有序列表可以承载其中大多数内容,却不能在列表项内安全容纳另一个全量 % 容器。因此 OINK 保留
两种形态是为了技术能力边界,而不只是书写偏好。
确定性 Book 迁移
三个带日期的干跑 profile 用于证明迁移规则能解释每个被识别的来源,而不编造语义:
| Profile 快照 | 分类结果 | 人工边界 |
|---|---|---|
| DDIA v2 | 106 张图、3 张表、22 个代码示例,相关 304 条链接全部入账 | 1 条题注链接降级为可见文本,无未解释跳过项 |
| DDIA v1 | 90 张编号图与 203 条匹配引用 | 14 张装饰性或无编号图片刻意不处理 |
| TPME | 31 张图、10 张表、44 条编号引用与 1,018 条通用稳定引用 | 被识别项目零跳过 |
| 私有 Book profile | 119 张图、5 张表与 136 条编号引用 | 3 张歧义图片保留人工复核 |
每个 profile 都先干跑,只在歧义边界明确后写入;第二次执行变更数为零;随后以警告即失败的模式 构建,并通过渲染后的 kind、编号和锚点检查。公开迁移工具与当前 profile 边界见 创作书籍和 迁移契约。
边界
这些数字不能直接用于产品宣传,也不能当作当前站点清单。重做研究时,需要重新确定仓库清单并生成 新的带日期报告。本公开记录刻意排除了本机路径、未提交内容、私有仓库名称、原始 Agent 对话与生成 构建产物。
7.3 - OINK 全面审查(2026-08-26)
本文记录 2026-08-26 对 github.com/pgsty/oink 主线与本站集成面的审查证据。
它不会改变既有 API,也不表示文中建议已经实现。当前行为仍以 Design 契约、实现与 owning checker 为准。
其中一部分已被 OINK 0.7.1 取代。 F01–F06 这些代码问题已在该版本修复,见 0.7.1 发布说明。下面的发现应当读作促成修复的证据,而不是主题当前的状态。
审查结论
OINK 的主干质量明显高于一般 Hugo 主题:默认路径可构建、双语完整、组件测试广、输出与安全意识强,
真实站点在桌面、移动端、深浅色和无障碍主路径上没有发现普遍性崩坏。当前 main 与远端一致,
主题 CI 和本站 CI 都是绿色;本次重新执行的主题检查、迁移单测、浏览器单测、全站链接、
Playwright 与 axe 也全部通过。
但「全部绿色」不能等价为「契约全部成立」。本次审查发现 4 项 P1、9 项 P2、5 项 P3。 最重要的共同原因是:项目已经建立了一套很强的原则,却仍有若干早期/边缘实现没有接入这套原则; 而现有门禁主要证明已选中的正向场景不回归,不能系统发现配置空间、静态输出和公开文档的语义漂移。
建议在下一个版本标签前至少完成以下四项:
- 关闭 Swagger UI 默认在线 validator,并用非 localhost 的浏览器请求测试锁定「零隐式外联」;
- 把所有公开配置和 Landing 数据纳入统一的类型、范围、URL 与 CSS 值验证;
- 重做 Swagger、Redoc、Asciinema 的 HTML/Print/Markdown/RSS 降级和 runtime gate;
- 修复生成 Schema,并让公开配置/Front matter 参考重新与当前实现对齐。
基线与方法
审查基线
| 项目 | 快照 |
|---|---|
| 主题仓库 | main = fe439fdb1d7c2df745088c9bfcbb8c350403ee63,工作树干净,与 origin/main 一致 |
| 当前稳定标签 | v0.7.0 = cbb6f4e0bfe47e17ba7aa41d04b8651c943cf858 |
| 文档站仓库 | main = fd5fcde,工作树干净,公开 pin 为 github.com/pgsty/oink v0.7.0 |
| 本机工具 | Hugo Extended 0.164.0、Python 3.14.6、Node 26.4.0、npm 11.17.0 |
| 远端 CI | 主题 HEAD 的 GitHub Actions run 32792753866 成功 |
实际执行的验证
- 31 个主题 checker 全部通过;
- 85 个迁移单测全部通过;
- 38 个主题浏览器运行时单测全部通过;
- 40 个 HTML/Print/Markdown/RSS/LLMS golden 表面通过;
tests/site严格 Hugo 构建通过;- 真实双语站点的
npm test通过:121/121 中英页面配对、886 个标题 ID、24,860 个站内链接与 3,172 个 fragment 均通过; - 真实站点的完整 Playwright 套件通过:全站 sitemap axe 扫描、29 个无障碍场景、45 个响应式/ 导航场景、16 个键盘场景、10 个内容组件场景、18 个代码块场景、4 个 PRD5 场景与 5 个主题色场景;
- 额外在 320 CSS px 下人工检查 EN 首页、ZH 配置页、ZH Book 页、OpenAPI/Redoc 页,未发现页面级水平溢出;
npm audit对本站 79 个 npm 依赖报告 0 项漏洞;对VENDOR.json的 26 个精确 npm 版本调用 OSV Query API 未返回已知公告;measure-baseline.py assets --fixture-site的严格隔离构建通过。
判级
| 级别 | 含义 |
|---|---|
| P1 | 违反核心产品承诺、安全/隐私边界或普通编辑可用性;应在下一标签前修复 |
| P2 | 明显功能/契约/兼容性缺陷;短期内修复并增加行为门禁 |
| P3 | 维护性、性能、流程或文档治理债务;排入结构化改进 |
发现摘要
| ID | 级别 | 发现 | 默认站点是否受影响 |
|---|---|---|---|
| F01 | P1 | Swagger UI 在生产 URL 上默认启用在线 validator | 仅使用 swagger 的页面 |
| F02 | P1 | 多组非法配置会让普通 Hugo 直接失败或静默生成坏输出 | 取决于配置输入 |
| F03 | P1 | Swagger/Redoc/Asciinema 违反静态输出和 runtime 隔离契约 | 使用这些 shortcode 的页面 |
| F04 | P1 | Landing 将未验证数据送入 safeCSS,其它错误值静默通过 |
使用相关 Landing 字段的页面 |
| F05 | P2 | 自定义页面动作与归档版本 URL 绕过共享 URL 策略 | 配置这些可选项的站点 |
| F06 | P2 | 生成 JSON Schema 的默认值、类型、描述和候选键存在实质错误 | 使用编辑器 Schema 的作者 |
| F07 | P2 | 「完整」配置与 Front matter 参考大量落后于 v0.7 实现 | 全部维护者/消费站作者 |
| F08 | P2 | Design 契约与提案生命周期内部出现双重答案 | 维护者 |
| F09 | P2 | OpenAPI 无障碍缺口被测试排除,Redoc 推荐与实测不一致 | OpenAPI 页面读者 |
| F10 | P2 | 严格 CSP 文档没有覆盖主题自己的 inline script/style | 启用严格 CSP 的站点 |
| F11 | P2 | 浏览器兼容性没有公开基线,自动化只跑 Chromium | Firefox/Safari/RTL/强制色用户 |
| F12 | P2 | 输出安全与「Rendered Markdown」门禁存在系统盲区 | 依赖门禁判定安全/输出纯度的站点 |
| F13 | P2 | 跨仓库真实集成仍是人工、非原子的发布步骤 | 每次公共行为改动 |
| F14 | P3 | checker 体系重复且过度依赖源码字符串 | 维护者与并行工作树 |
| F15 | P3 | 全局 CSS/字体仍是首访主要负担 | 全部 HTML 页面 |
| F16 | P3 | vendor 完整性强,但漏洞/SBOM 与 CI 供应链门禁不足 | 发布维护者 |
| F17 | P3 | Changelog、已实现提案和无行为元数据造成治理噪音 | 维护者与升级读者 |
| F18 | P3 | Print isHTML 的 FIXME 已不能准确说明真实依赖 |
Print 模板维护者 |
详细发现
F01 — Swagger UI 会隐式联系在线 validator(P1)
证据。 layouts/_shortcodes/swagger.html 初始化 SwaggerUIBundle 时没有声明
validatorUrl: null。随主题内置的 swagger-ui-bundle.js 把默认值设为
https://validator.swagger.io/validator;它只对包含 localhost 或 127.0.0.1 的 spec URL
跳过在线校验。部署到真实域名后,Swagger UI 会创建在线 validator badge,请求参数包含 spec URL。
影响。 这违反「主题自有网络功能默认关闭」「本地优先」「同源 spec 在浏览器中不访问外部服务」三项承诺。 内网站点尤其会把内部主机名/spec 地址暴露给第三方。由于上游特意跳过 localhost,当前所有本地浏览器测试都看不到它。
建议。 初始化时显式写 validatorUrl: null。若未来允许在线 validator,应做成明确 opt-in 的 URL 配置,
走共享 URL 验证并在隐私/CSP 文档中说明。浏览器测试应使用一个非 localhost 的虚拟 origin,拦截全部请求,
断言同源 spec 页面只请求首方资源。
F02 — 非法配置没有统一 warn/fallback,甚至击穿普通预览(P1)
ui-param.html 明确写着「caller validates the type」,但多个 caller 没有验证。最小复现得到:
| 输入 | 实际结果 |
|---|---|
ui.blog_index_size: nope |
普通构建失败:.Paginate 要求正整数 |
ui.sidebar_expand_levels: nope |
普通构建失败:add 无法处理字符串 |
ui.sidebar_menu_truncate: nope |
普通构建失败:first 无法转成整数 |
offline_search_summary_length: nope |
普通构建失败:truncate 无法转成整数 |
ui.sidebar_width_min: "1; color: red" |
零告警成功,输出 --td-shell-sidebar-min: ZgotmplZpx |
ui.sidebar_width_min: -50 |
零告警成功,输出 -50px |
blog_index_columns: 2.5 / section_index_columns: 2.5 |
零告警成功,把 2.5 送入 CSS repeat() |
ui.sidebar_item_overflow: clip |
零告警成功,静默当成 ellipsis |
ui.sidebar_menu_foldable: definitely |
零告警成功,非布尔字符串按 truthy 启用 |
ui.blog_index_size: 0 |
被 Hugo default 静默吞掉,回到 12 |
Landing 的 marquee.rows、capabilities.columns 和 Asciinema 的数字参数也直接调用 int/float,
错误文本会终止模板执行。print.toc、offline_search_max_results 等错误类型则静默改变行为。
影响。 这是对 Diagnostics decision 的直接反例:普通 hugo server 可能整体不可用,而错误输入也可能在
--panicOnWarning 下零告警上线。
建议。 为整数、正整数、范围、成对范围和 CSS grid count 增加共享 validator;先归一化再参与运算或输出。
每个公开键至少需要四态用例:合法站点值、合法 page override、非法普通构建(warn+fallback)、非法严格构建(失败)。
对 min <= max、分页大小 >= 1、列数为合理整数等交叉约束加领域 resolver,不要依赖浏览器吞掉坏 CSS。
F03 — OpenAPI 与 Asciinema 仍是 HTML-only 岛(P1)
Architecture/Components 规定 Markdown/LLMS 不含 td-* 组件标记,Print 静态展开且不依赖交互,RSS 只保留安全静态内容或明确省略。
但当前实现与公开示例表明:
redoc在生成.md中原样输出<style>、<div class="td-redoc">与<redoc spec-url=...>;swagger把可执行 inline initializer 直接写在 shortcode 中;asciinema的.md输出包含整套td-asciinemaHTML 与 JSON script;- Asciinema 的 Print 仍加载约 185 KB 的 player JS/CSS,只能碰巧打印某一帧;
- Swagger/Redoc 在 Print 里留下空容器,并仍可能装载 1–2 MB runtime;
- 这些 shortcode 没有进入 Markdown/RSS/Print golden 矩阵。
影响。 Agent 输出被主题 HTML 污染;纸面/EPUB 读者拿到空壳;Print/PDF 负担无意义的大 runtime; Swagger inline script 也破坏 CSP。当前用户文档把这些缺陷写成「输出形态」,等于让 reader guide 与规范契约相互否定。
建议。 三者都先读取 tdOutputFormat:HTML 输出完整组件;Print/Markdown/RSS 输出一个有标题的静态链接、
spec/cast 地址与必要的文字说明,或者明确省略。只有交互 HTML 才设置 capability flag。Swagger initializer 应移入稳定 chunk,
Redoc 的样式移入 stylesheet,新增四输出 golden 与 runtime-absence 断言。
F04 — Landing 的 CSS/URL/数值入口没有同一安全边界(P1)
layouts/_partials/landing/sections/hero.html 对 title_size 做了 CSS 长度验证,却把
media.ratio 与 media.max_width 原样拼进字符串,再整体 safeCSS。最小输入:
普通和严格构建均零告警,输出:
Landing 允许把 sections 直接写进 front matter,因此这不是只属于仓库管理员的内部常量。
其它 section 的 columns、rules、宽高、style、icon 与 URL 也各自处理;非法 javascript: 通常被 Go template
变成 #ZgotmplZ,但没有 warning,严格门禁仍通过;字符串列数会变成 ZgotmplZ,某些 int 转换则直接终止构建。
建议。 为 Landing 建立一层 section schema/normalizer:所有类型共享 class、icon、URL、CSS length、grid count、
boolean、enum 解析;section partial 只消费规范化结果。hero.media.ratio 应是两个受限 track 值而不是任意 CSS 片段,
max_width 走 CSS length validator。所有 link/action 复用 content/url.html,并给每种 section 一个负向用例。
F05 — 两个配置 URL 面绕过共享策略(P2)
params.ui.page_context_menu.links 经 url-template.html 替换占位符后直接 safeURL;
url_latest_version 也被当作「trusted site configuration」直接 safeURL。它们没有检查 scheme、host、空白或 protocol-relative URL。
最小配置可零告警产出:
点击该 URL 会执行 JavaScript。站点配置本身是高信任输入,因此这不是默认远程攻击面,但它与公开的「safe URL」配置模型不一致, 也让复制来的配置片段拥有不必要的执行能力。
建议。 自定义动作只允许 http/https 与明确支持的站内相对 URL,并复用 content/url.html;
归档版本 URL 也应验证。浏览器 action registry 的二次检查值得保留,但 progressive-enhancement 的 <a> 不能绕过它。
F06 — 生成 Schema 与真实 YAML 不一致(P2)
generate-config-schema.py 的小型 YAML parser 不剥离行尾注释,至少 11 个默认值被生成成字符串,例如:
print.toc的默认值是字符串"true # ...",不是 booleantrue;print.section_break_wordcount、section_index_columns、blog_index_columns变成字符串;footer_style、blog_index、typography的 enum 默认值包含注释正文。
注释关联也会漂移:解释「breadcrumb 没有全站默认」的注释被挂到 section_index;解释 quick_links 的注释被挂到
sidebar_icon_policy;taxonomy icon 注释被挂到 pager_types;本地 chrome 注释被挂到 image_zoom。
Front matter Schema 还会把探测器读到的已移除键 release、upstream_attribution、downstream_modified 暴露给编辑器,
并把 navbar menu 的 Params.columns 误判成 page front matter。--check 只比较「同一个有 bug 的生成器」与已提交产物,
所以会稳定地保持错误。
建议。 不要继续扩展 ad-hoc YAML parser。使用能保留注释的正式 parser,或为默认值/描述建立显式机器元数据标记; scanner 需要区分 page、menu、shortcode 和 legacy detector 上下文。生成测试必须拿 Schema 默认值与 Hugo 实际解析值逐项比对, 并维护「禁止出现在补全中的已移除键」列表。
F07 — 配置与 Front matter 参考不是当前实现的完整参考(P2)
content/docs/customize/config.md 与 content/docs/write/frontmatter.md 都自称「每个主题实际读取的键的唯一完整参考」,
但当前存在多类实质错误:
- 日期默认仍写成长英文日期,而
hugo.yaml已是 ISO2006-01-02; - Blog 只写
none|banner|wash和list|cards,遗漏hero、table、toggle、size、toc_style、toc_taxonomies; - Front matter 仍把已移除的
releasemap、release_products、release_group_by_product当现行 API,遗漏release_url; images: []被写成「没有 featured image」,但契约明确 bundle resource discovery 仍继续;upstream_modified被写成新增一行,而现行契约是改变 credit verb,不新增行;- 大量页面说非法参数「直接失败」,与 warn/fallback decision 混在一起,普通预览与严格发布门禁没有说清;
- Book guide 仍说主题止于 Print HTML,而 v0.7 已发布 BookManifest、EPUB 与 PDF 工具;
- Asciinema/OpenAPI guide 将污染静态输出的现状写成产品契约;
- Features 页仍写 28 个 vendor 依赖,权威清单是 26 个。
中英文在这些旧答案上通常保持一致,所以 translation parity 不会报错。
建议。 先把配置参考与 Front matter 参考作为一次专门的契约迁移处理;从实现/Schema 生成一份可比对的 key inventory,
人工维护语义文字。发布门禁应检查:现行键全部出现、removed 键只出现在迁移章节、enum/default 与 hugo.yaml/resolver 一致。
F08 — Design 树出现互相冲突的权威和未退休提案(P2)
最直接的矛盾是:Shell 契约声明 navbar columns/mega panel 已退役、配置会 warning 并保持单列;
Landing 契约却仍声明「Navbar mega-menu columns accept 1–4」。实现与 checker 支持前者。
提案生命周期也没有按自己的规则执行:config-schema 已标记 implemented,仍位于 Active proposals;
Book publication 已把 manifest、EPUB、PDF 和 CI 做完大半,却仍以 Draft proposal 与正式 Architecture contract 重复描述;
media-convergence 把已实现里程碑和未完成 M4 混在一份原始设计记录中。
建议。 修正 Landing 契约;把已实现的 config-schema 稳定事实移到 Architecture/Decision 后退休提案; Book proposal 只保留尚未完成的 consumer migration 问题,或拆成新的窄提案。Active proposal 中不应存在第二份现行 API。
F09 — OpenAPI 无障碍承诺与测试排除项不一致(P2)
本站 axe 套件明确排除 .td-swagger-ui 和 .td-redoc。注释记录的已知问题包括 Swagger UI 的无名称 server select、
不可键盘访问的 scrollable version stamp,以及 Redoc operation description 的颜色对比度。
OpenAPI guide 却只公开 Swagger 的问题,并把「真正渲染的 Redoc」作为替代;这会让读者误以为 Redoc 满足本站的零违规门禁。
建议。 立即在 EN/ZH guide 中公开两者的真实边界。短期可通过主题 CSS 修复可修的 Redoc contrast, 对 Swagger 的可修 DOM 用 narrow post-render adapter;不能修的上游问题应有版本化 waiver、issue 链接和单独 axe 报告, 而不是把整块 DOM 排除后仍称全站零违规。
F10 — 当前主题不能直接配合严格 CSP(P2)
部署指南说同源资源使 strict CSP 可行,却只列作者 inline script、ECharts callback、analytics、远程 spec/diagram 和 Giscus。 主题自身在普通 Docs 页就输出两段可执行 inline script(颜色首绘与 shell prepaint)和 inline style;Markmap、Swagger、Algolia、 Google CSE 还增加主题自有 inline initializer。项目没有 nonce 参数、hash manifest 或完整的 CSP 示例。
影响。 script-src 'self' 会阻止主题自己的首绘与 shell 状态恢复;style-src 'self' 会阻止主题色、字体角色、Landing
和多个 inline custom property。站点只能加 'unsafe-inline'、自行维护 hash,或覆盖模板;当前文档没有说清。
建议。 把稳定初始化逻辑移到同源外部 chunk,以 data/JSON 传递页面配置;剩余必须 inline 的内容提供可生成的 CSP hash 清单, 或统一 nonce hook。文档应给出「最小核心」「带 Markmap/OpenAPI」「带第三方集成」三套策略,并明确 style-src 需求。
F11 — 浏览器兼容性承诺缺少基线与跨引擎证明(P2)
Playwright CI 只安装 Chromium;仓库和产品文档没有写最低 Chrome/Firefox/Safari 版本。
但实现依赖或增强使用 :has()、dialog、inert、color-mix()、@property、logical properties、
discrete display transition 等新能力。部分功能有 fallback,但没有一个浏览器矩阵证明它们。
RTL 主要依靠源码 marker、少量 JS 单测和一个临时给元素设置 dir=rtl 的几何测试;没有完整 RTL 语言站。
forced-colors 多数只检查 SCSS 中是否出现字符串,没有浏览器 computed-style/交互测试。
建议。 发布一个小而明确的支持矩阵,并至少对核心 shell/导航/内容/对话框跑 Chromium + Firefox + WebKit。
增加一条真正 languageDirection: rtl 的集成配置,以及 forced-colors、reduced-motion、320px、200% zoom 场景。
F12 — 输出安全和 Markdown 门禁没有检查自己宣称的全部表面(P2)
check-output-security.py 对 .md 只匹配 Markdown link 语法,不把其中 raw HTML 送入 HTML scanner;
因此 Redoc/Asciinema 的 <script>、spec-url 与 raw href 不会被发现。它也不检查 style 中的 url()、JSON config 中的 URL,
而 theme fixture 以全局 --third-party 运行,降低了第三方元素检查的区分度。
本站的 check-rendered-markdown.mjs 名字也容易误导:它扫描的是生成 HTML 的文本节点里是否残留 Markdown 标记,
并不读取生成 .md。真正的 md-output golden 只有 15 个页面,未覆盖 OpenAPI/Asciinema。
建议。 将门禁拆成三个明确工具:HTML trust、machine-output purity、rendered-text residue。
.md 中允许的 raw HTML 应有极窄 allowlist;CSS URL、form/action、JSON URL 与非可执行 JSON script 需要分别解析;
每个 public shortcode 至少进入一个 Markdown/Print/RSS 行为用例。
F13 — 两个仓库之间没有自动的候选提交集成门禁(P2)
主题 CI 只对 tests/site 合成夹具运行;文档站 CI 则只测试 go.mod 固定的公开标签。
主题 PR 的真实 EN/ZH/Playwright 验证依赖维护者本地执行 HUGO_MODULE_REPLACEMENTS,两个仓库的变更也无法原子提交。
这次的结果说明两边可以分别全绿,而公开参考仍与实现漂移。现有 release-state 文字区分是正确的,但自动化没有执行 「实现 + owning checker + EN/ZH contract」同一交付规则。
建议。 增加一个只读的跨仓库候选 workflow:主题 PR checkout 当前 SHA,同时 checkout 文档站指定 main SHA,
用临时 module replace 跑 npm test 与关键浏览器套件;反向也让 Design contract PR 指向待验证主题 SHA。
发布仍保持 tag/pin/deploy 分离,但候选提交应有一个可追溯的联合验证结果。
F14 — checker 维护成本和源码耦合过高(P3)
当前 checker 覆盖面值得肯定,但 34 个 check-*.py 中有 546 次 read_text();多数脚本重复实现 require、临时站点、
写文件、Hugo 命令和错误聚合。大量断言锁定模板/SCSS 的源码拼写、注释附近结构或整文件相等,而不是最终行为。
一部分 helper 又硬编码 theme: oink + --themesDir <repo-parent>,使 checkout/worktree 目录名成为隐藏前提。
项目没有统一的 Python lint/type gate。结果是新增 checker 很快,却更容易出现「门禁全绿但共同盲区没有人拥有」。
建议。 建立共享 fixture builder 和 assertion library;把负向 case 作为表驱动数据; 只给真正的 topology invariant 留源码检查,其余转到解析后的 HTML/JSON/computed style。 测试主题应通过显式 symlink/module replace 装载,不依赖仓库 basename。
F15 — runtime 拆分成功,但基础 CSS/字体仍占主要首访成本(P3)
严格隔离 fixture 基线:
| 指标 | 数值 |
|---|---|
| 冷/热构建 | 1.256 s / 1.273 s |
| 页面 | 249 |
| stable JS chunks | 18 |
| main + Font Awesome CSS | 549.8 KB raw / 91.1 KB gzip |
| 字体总量(其中 FA) | 999.7 KB raw / 248.5 KB gzip |
| Docs 页 JS 中位数 | 176.9 KB raw / 55.3 KB gzip |
| 生成 public | 26.2 MB |
| v0.7.0 Go module zip | 7.8 MB(展开约 20.5 MB、1,140 文件) |
第一方 capability chunk 已经消除了 2^N 组合包,这是正确方向;大第三方 runtime 也按页面隔离。
剩余主要成本来自所有页面都加载的 Bootstrap/主题/Landing CSS 与完整 Font Awesome 分发。
建议。 不要违背现有合同去按模板用量裁剪 Font Awesome。优先测量可独立缓存/按 surface 加载的 Landing、Book、Swagger CSS, 检查真实首访实际加载的 font subset,并给预算建立趋势报告而非武断阈值。
F16 — vendor 可复现,但漏洞与 CI 供应链仍靠人工(P3)
正面证据:VENDOR.json 精确记录 26 个包、56 个 artifact、31 个 license 文件和 tree hash,
check-vendor.py 通过;本次 OSV 与 npm audit 均未发现已知漏洞。
缺口:custom manifest 没有进入通用 SBOM/OSV gate,npm audit 也天然看不到这些 vendored 浏览器包;
文档站两个 workflow 通过 curl 下载 Hugo .deb 后直接 sudo dpkg -i,没有校验摘要;Actions 用可移动的 major tag,
主题 CI 的 Python 是浮动 3.x。
建议。 从 VENDOR.json 生成 CycloneDX/SPDX SBOM,增加定期 OSV 扫描;Hugo archive/deb 固定 SHA-256;
高信任 release workflow 的 action 固定 commit SHA;选择明确 Python 版本或建立版本矩阵。
F17 — 设计记录与发行文字的信噪比下降(P3)
CHANGELOG.md 已有 1,768 行,0.7.0 单节约 300 行;Unreleased 用约 20 行解释一次 checker retry。
这些叙事对工程复盘有价值,但升级读者很难快速找到 breaking change、迁移和行为差异。
同时,book_kind/book_part 被契约「认可」并出现在大量内容 front matter,却明确不被模板读取;
它们给作者增加了类似 API 的负担但没有行为。已实现提案仍留在 Active proposals 又放大了重复答案。
建议。 Changelog 保留用户可观察变化、breaking/migration 与修复摘要;长设计故事移到 Blog/Research,并从 changelog 链接。 没有行为的 metadata 要么定义消费者和 schema,要么从公共契约降级为站点自有字段。
F18 — Print isHTML FIXME 已经失真(P3)
hugo.yaml 说「等 Hugo 修复 #14381 前保持 isHTML 未设置」。该 Hugo issue 已于 2026-01-17 修复,
修复进入 OINK 兼容性下限之前的 Hugo 0.155 系列;OINK floor 是 0.160.1。
但在当前主题上简单启用 isHTML: true 仍会产生 page/section/landing print layout missing warnings,
严格构建失败。这说明真实依赖已经从「等待 Hugo alias fix」变成「当前 Print 模板命名依赖 non-HTML lookup 规则」。
建议。 不要直接删除 workaround。先为 HTML-classified Print 补齐 lookup matrix 与 alias/subpath 测试; 若继续保持 false,就更新注释说明当前真实原因,并增加一个测试防止未来维护者依据已关闭 issue 做错误清理。
做得好的地方
- 主题、文档站、发布标签和消费站 pin 被明确区分,没有把本地 replacement 当成发布;
- Hugo floor 0.160.1 与 0.164/0.165 的主题矩阵覆盖扎实;
- 大多数新组件已经遵循 warn/fallback、四输出、共享 URL/attribute policy 与 capability flag;
- 32 个 locale schema 一致,EN/ZH 真实页面、标题 ID、站内链接和窄屏导航有强门禁;
- 搜索、键盘、surface coordinator、页面动作和主题色测试既有单测也有浏览器行为测试;
- vendor license/hash、EPUB/PDF 的路径边界、PDF loopback+CSP 与不可覆盖默认值设计认真;
- 320px 人工复核未发现页面级水平溢出,当前核心视觉质量良好;
- 构建性能很好,第一方 JS 已从组合 bundle 迁移到稳定 capability chunk。
建议修复路线
阶段 0:下一个标签前
- Swagger 写死
validatorUrl: null,增加 production-origin no-network test; - 建立公开参数 inventory,为 F02/F04 中所有字段补 validator 与负向矩阵;
- 重做 Swagger/Redoc/Asciinema 四输出和 runtime gate;
- 修复自定义 action/归档版本 URL;
- 修复 Schema parser/scanner,并重新生成两份 Schema;
- 同步 EN/ZH Config、Front matter、OpenAPI、Asciinema、Book、Features 与 Landing contract。
阶段 1:契约门禁
- 为 29 个 shortcode 建立最小 HTML/Print/Markdown/RSS coverage map;
- 拆分并增强 output trust / machine-output purity 检查;
- 将 Landing section 输入统一归一化;
- 外部化 theme-owned inline initializer,发布 CSP 参考;
- 建立跨仓库候选提交 workflow。
阶段 2:兼容性与结构
- 加 Firefox/WebKit、真实 RTL、forced-colors、200% zoom;
- 收敛 Python checker harness 和源码字符串断言;
- 评估按 surface 拆 CSS 与字体实际请求;
- 生成 SBOM、定期 OSV、固定 CI 下载摘要;
- 退休已实现提案并精简 Changelog。
完成判据
- 使用同源 Swagger spec 的生产 origin 除首方资源外无请求;
- 每个公开配置错误在普通构建中 warn+fallback/omit,在严格构建中失败,且不出现 Go template
ZgotmplZ; - 生成
.md不含td-*、theme<script>/<style>或空交互容器; - Print 不加载 Swagger/Redoc/Asciinema runtime,并给读者可理解的静态替代;
- Schema 默认值类型与 Hugo 实际解析完全一致,removed key 不出现在补全中;
- EN/ZH 配置和 Front matter 参考的 key/enum/default 与实现 inventory 一致;
- 核心 Playwright 在 Chromium、Firefox、WebKit 通过,真实 RTL 与 forced-colors 有行为断言;
- 主题候选 SHA 有一条可追溯的真实文档站联合验证记录。
审查边界
本次没有逐一审查全部消费站仓库、真实生产响应头/CDN 缓存、Firefox/Safari 实机、读屏器, 也没有人工逆向 13 MB minified 第三方源代码。漏洞查询是 2026-08-26 的快照,之后可能变化。 DDIA/TPME 的 EPUB/PDF 真实消费站结果引用现有 CI/契约,本次没有重新发布或部署任何站点。
8 - 设计提案与 PRD
提案描述的行为可能尚不存在。当前行为由契约、已接受决策、实现与归属检查器定义。不能把提案 当作配置参考。
本栏目是 OINK 产品需求文档、RFC 风格设计与未决维护者提案的唯一正本位置。不要在主题仓库或
文档仓库中另建本地 plan/、plans/、proposal/ 或其它并行设计树。
当前提案
| 提案 | 当前边界 |
|---|---|
| 反向链接与知识图谱 | G1(静态反向链接)已接受,已在主题 main 分支实现,随 OINK 0.8.0 发布;局部与全站图谱(G2/G3)保持草案 |
| 媒体收敛 | 部分已实现;media-result 契约与 Landing 资源元数据已交付,M3 决议为原生图片处理,退役(M4)保持开放 |
| Agent 批量索引 | 已接受(2026-08-27);两类输出都已在主题 main 分支实现,随 OINK 0.8.0 发布,之后本提案退役 |
| Book 出版链路 | manifest 与 EPUB/PDF 工具已随版本发布,见架构;本页只剩消费站迁移未决 |
生成式配置 Schema 提案已按生命周期退役:行为的规范位置是配置总览, 长期理由进入生成式配置 Schema 决策,草案原文由 Git 历史保存。
新 PRD 放在哪里
创建一份英文主页面及其简体中文对页:
两份文件都使用显式、稳定的英文标题 ID。中文页面中的代码、键、路径、版本与 API 名称保持原样。 提案开头要有可见的草案状态,并包含:
- 状态、负责人、日期和受影响契约面;
- 背景与证据;
- 目标与明确非目标;
- 提议行为,以及输出、无障碍、安全边界;
- 兼容与迁移影响;
- 实现与归属检查器计划;
- 验收标准与待决问题;
- 记录提案自身变化的决策日志。
大型实验可以在 ../research/ 下增加带日期的页面;临时日志与生成
产物不进入 Hugo 内容,也不进入 Git。
生命周期
提案被接受后不会自动成为第二份契约。稳定行为进入归属契约,稳定理由进入 Decisions,用户步骤进入 相关指南,然后把提案退出活动导航。本地构建、提交、tag、公开模块、消费站 pin 与部署仍是相互独立 的完成状态。
评审门禁
实施前,评审者确认提案没有重复已有外壳、resolver、组件族或数据权威。实施期间,如果设计改变, 先更新这份双语提案,不能让代码悄悄漂移。验收至少覆盖主题的最窄归属检查、真实文档站、渲染后的 中英文、相关输出、无障碍与响应式检查。
8.1 - 反向链接与知识图谱
2026-08-27 决议 G1 的全部待决问题并接受 G1(静态反向链接)。它已在主题 main 分支实现,随 OINK 0.8.0 发布。局部与全站图谱(G2/G3)保持草案状态,等待 G1 的 真实使用证据;它们的名称和配置在被接受之前不是公开 API。
前提
反向导航与页面连接视图是链接图的属性,不是 [[wikilink]] 拼写的属性。Hugo 已经接受普通
Markdown 链接和 ref / relref。OINK 可以从作者已经在写的内容中派生图谱,无需增加解析器、
Goldmark 扩展或并行创作语法。
首要价值是反向链接,而不是可视化。静态入链列表不需要 JavaScript,在 Print 与 Markdown 中也能 降级。交互图谱应当只是完整列表之上的可选增强。
目标与非目标
目标:
- 每次构建为每种语言派生一份链接索引;
- 在页面上显示确定性的入链;
- 可选显示有界的局部邻接图;
- 可选发布全站视图与机器可读图数据;
- 编辑链接暂时陈旧或不完整时,普通预览仍然可用。
非目标:
- 引入
[[wikilink]]语法; - 索引外链、
mailto:、同页锚点或自链接; - 用 JavaScript 发现正文中已经存在的链接;
- 把可视化变成唯一导航方式;
- 承诺从任意 shortcode 参数或原始 HTML 中完整提取语义图。
交付阶段
| 阶段 | 交付物 | 运行时 | 独立价值 |
|---|---|---|---|
| G1 | 语言内链接索引与反向链接列表 | 无 | HTML、Print、Markdown 中的反向导航 |
| G2 | 当前页面周围的局部图谱 | 既有 ECharts 加一个小型本地运行时 | 以 G1 为无障碍兜底的空间视图 |
| G3 | 全站图谱页与图数据输出 | 同一运行时 | 全站探索与机器可读边 |
每个阶段单独验收。G1 不等待 G2,G2 也不会强迫每一页加载图谱代码。
提取契约
提议的索引按语言扫描源码一次,每对来源与目标只记录一条边。它先剥离代码围栏和行内代码,再提取
普通 Markdown 链接与 ref / relref;随后只解析站内页面,去掉 fragment 以确定页面身份,
排除自链接,并合并重复引用。
实现至少要测试:
- 同一目标的重复链接合并为一条边;
- 围栏与行内代码不产生边;
- 外链、protocol-relative URL、邮件、同页锚点与自链接被排除;
ref与relref被纳入;- 每种语言生成相互独立的图;
- 无法解析的派生边由警告或专项检查报告,但不会让普通
hugo server不可用。
扫描原始源码存在已知遗漏。自定义 shortcode 参数或原始 <a href> 中的 URL 可能不会进入图谱。
必须明确记录这种遗漏,不能声称得到完整语义图。
反向链接输出
G1 在右栏输出一个 aside 组,与目录、分类标签云并列:目录讲这一页写了什么,反向链接
讲哪些页面指向这一页。该组默认展开,先显示前八条,其余折进原生 disclosure,避免被
大量引用的页面把右栏撑满。开关是站点键 params.ui.backlinks(裸布尔,默认关闭),页面用同名去前缀的
front matter 键 backlinks 覆盖,section 可以 cascade。排序必须确定:按稳定页面路径
排序——它与语言无关、与导航自然同组,且不需要第二个排序权威。该组使用普通链接;没有
入链时不渲染。
无法解析的派生边被静默丢弃并作为已知遗漏记录在案:G1 是本地导航增强,不是链接检查器, 让它替站点报告断链只会制造重复告警。
Print 与 Markdown 保留可读列表。除非后续 feed 研究证明反向链接能改善文章订阅而不是制造站点导航 噪音,否则 RSS 省略它。
交互图谱边界
G2 复用本地内置的 ECharts graph series。当前页面是中心,直接入链与出链邻居组成默认深度。硬性 节点上限防止视图不可读或成本失控。键盘焦点、文字替代、reduced motion、forced colors、窄屏和 Print 都是验收要求,不是后续润色。
JavaScript 或 ECharts 不可用时,G1 仍然完整可见。运行时只在真正渲染图谱的页面加载,并进入既有 feature bundle key,避免不同特性页面在资产缓存中撞车。
全站输出
G3 可以新增专用图谱页与 opt-in JSON 输出。JSON schema 包含版本、语言、节点和带稳定 URL 的有向边, 不暴露本机文件路径或未发布页面。它必须和 G1、G2 使用同一索引,避免三种表示各自漂移。
兼容与迁移
普通 Markdown 写法不变,因此无需内容迁移。配置名称继续待定,直到原型证明最小公开面。所有交互 与全站输出默认关闭;静态反向链接列表可以单独讨论,因为它只是本地导航,不涉及网络与浏览器状态。
验收标准
验收需要专项 graph 检查器、提取夹具、HTML/Print/Markdown golden、严格构建负向用例、浏览器无障碍 与响应式测试,以及真实双语站构建。性能在有代表性的大站上测量,但带日期的原型耗时不能自动成为 永久预算。
待决问题
G1 的问题已全部决议(见决策日志)。仍然开放、属于 G2/G3 的问题:
- 局部图只暴露一层,还是允许严格限额的第二层?
- 哪些页面元数据值得进入 graph JSON?
- 在 G1、G2 获得生产证据前,G3 是否值得新增输出格式?
决策日志
- 2026-08-19:起草三阶段设计。
- 2026-08-27:决议 G1 并接受,排入 OINK 0.8.0。G1 是 opt-in:站点键
params.ui.backlinks裸布尔默认关闭,页面覆盖键backlinks,不按 shell type 区分——策略归站点与页面,不归外壳。排序简化为稳定页面路径单键排序,删去 「section → weight → 标题」的三级链:单一确定性权威已经满足反向导航,多级排序 等于第二个导航权威。无法解析的边静默丢弃并记录为已知遗漏,不产生告警。 G2/G3 与图数据输出继续等待生产证据。 - 2026-08-27:设计评审把这一块从页尾移到右栏。反向链接是页面元数据,与目录成对; 页尾是读者的收尾区——分享、反馈、出处、翻页、评论。右栏这一组同时引入八条上限, 其余收进原生 disclosure。
8.2 - 媒体收敛
M1(共享 media-result 契约)与 M2(Landing 资源元数据)已在主题 main 分支实现;
M3 已决议为方案 2:图片处理只属于原生 Markdown 图片形态,完整 fig 源形态保持
容器语义,其参数表刻意不含 command/options。M4(兼容退役)在完成消费方盘点
之前保持开放。以下各节为原始设计记录。
当前基线
正文图片钩子、编号 fig、卡片与 gallery 统一通过 content/image-resolve.html 解析页面资源、
section 资源、全局资产、static 文件与显式远程 URL。栅格资源可以提供固有尺寸与处理后派生图。
HTML Zoom 资格使用 data-td-image-zoom 标记;构建期检测只查找主题自己输出的标记。
独占 Markdown 图片已经可以把题注或 Book 编号与图片处理、链接组合起来。编号图片 figure 共用
td-figure 与 td-book-figure 语义。Landing 媒体经过共享 URL 信任策略;代表图片则刻意使用
排序 resolver,因为它的职责是选择代表图片,而不是渲染一个显式来源。
剩余问题
共享安全边界已经比共享媒体模型更成熟。Landing 媒体仍然拿不到与正文图片相同的页面资源元数据和
处理结果;代表图片选择与显式图片解析返回不同结果形状;部分兼容 class 仍保留在标记中;Book 的
全量 fig 形态也不能表达原生图片钩子的所有处理选项。
因此问题已经不再是“替换七种图片入口”,而是:能否在不抹掉各自语义差异的前提下,让剩余表面共享 一份小型结果契约。
目标与非目标
目标:
- 为 URL、原始 URL、尺寸、替代文字、署名、可处理状态与外部状态定义一个规范化媒体结果形状;
- 在来源语义重合处,让显式正文图片、Landing 媒体与代表图片复用这个形状;
- 继续让 figure 标记与 Zoom 资格分别只有一个归属实现;
- 决定全量
fig是否需要处理能力,还是要求处理过的编号图使用原生图片形态; - 只有在完成消费站证据与 release note 后才退役兼容标记。
非目标:
- 增加第三方 lightbox 或远程图片服务;
- 意外把 image Zoom 从 opt-in 改成站点政策;
- 给 gallery 新增题注、序列或轮播模型;
- 把表格、公式、示例等非图片 Book 目标合并进只适用于图片的基类;
- 强迫代表图片排序与显式图片解析完全相同。
提议阶段
M1 — 结果契约
记录正文 resolver 与代表图片 resolver 的返回字段,再把交集提取成一份内部媒体结果契约。代表图片 继续负责来源排序,正文 resolver 继续负责显式来源解析。这是要求字节输出不变的内部重构。
M2 — Landing 资源元数据
允许 Landing 条目中的合格本地资源通过媒体契约解析,获得固有尺寸与相同 URL/安全结论。Landing 数据中显式给出的宽高继续优先。远程与 static 来源仍然合法,但不能伪装成拥有可处理资源元数据。
M3 — 全量 figure 能力决策
从两个答案中明确选择一个:
- 为全量
fig的来源形态增加处理参数,并通过同一处理 helper 规范化;或者 - 处理能力只属于原生 Markdown 图片,把全量
fig明确定义为任意编号块内容的容器。
实现不能让两个答案各完成一半。两种形态的 Markdown/LLMS 输出必须一致地链接到文档规定的原图 或派生图。
M4 — 兼容标记退役
移除旧图片元素 class 或属性之前,先盘点下游 CSS 与 JavaScript。兼容名称仍被使用时,要么保留一个 明确的版本窗口,要么在同一 release train 中迁移归属站点。
安全、输出与无障碍
- 图片 URL 继续遵守共享 scheme 与远程主机策略。
- 缺少必需替代文字时发出警告,且只在现行契约允许处渲染装饰性回退。
- 宽高不能声称 SVG、static 文件或远程来源没有提供的元数据。
- 带链接的图片不是 Zoom 目标;运行时保留 dialog 焦点、键盘关闭、reduced motion 与窄屏约束。
- Print、Markdown、RSS 与 LLMS 去掉交互标记,同时保留目标图片、题注、署名、编号与链接。
验收标准
每个阶段分别拥有 HTML 与 Markdown 字节级证据、正文与 Landing resolver 测试、URL/安全检查、图片处理 测试、Book 目标、gallery/Zoom 浏览器测试,以及真实站中英文窄屏审查。只有 M3 的能力选择明确后, 提案才能被接受。
待决问题
- 一份共享结果结构是否足够,还是共享更底层的 URL/资源记录会让 resolver 归属更清晰?
- Landing 应消费资源署名,还是只消费尺寸与 URL?
- 原生图片已经能组合编号、题注、链接和处理后,全量
fig处理能力是否仍有真实消费需求? - 哪些输出兼容名称仍被真实消费站使用?
8.3 - Agent 批量索引
2026-08-27 决议全部待决问题后接受本提案。两类输出——按顶层 section 分包的
LLMSFULL 与语言根下的 NAVJSON 导航树——都已在主题 main 分支实现,随 OINK
0.8.0 发布,之后本提案退役;发布后的行为由架构契约
承担。OINK 已经支持每页 Markdown、语言内 llms.txt、HTML discovery link 与
Copy Markdown;本页只覆盖新增的两类。
当前基线
站点可以为 page 与 section 启用 Hugo 的 Markdown 输出,并为 home 启用生成 llms.txt 的 LLMS
输出。OINK 把 shortcode 渲染成语义化 Markdown,保留源码 URL 和语言内 LLMS 索引发现信息,Copy
Markdown 也读取同一个 alternative output URL。主题声明输出格式,但不强迫站点选择哪些 outputs。
导航已经存在权威链:有显式 data/docs_nav.json 树时使用它,否则使用内容树与 weight。侧栏、
pager 与已声明 section index 共用这一权威。机器导航输出必须从同一棵树派生,不能再造排序。
目标与非目标
目标:
- 为显式启用的顶层 section 可选装配语言内全文包;
- 可选发布带版本的导航 JSON,供 Agent 与外部工具使用;
- 复用人工站点的同一 Markdown 页面渲染器、页面纳入规则与导航权威;
- 所有输出仍通过 Hugo output 配置 opt-in;
- 验证链接、语言隔离、media type 与确定性顺序。
非目标:
- 替换每页 Markdown 或
llms.txt; - 新建
params.oink.*配置树; - 在 Hugo 构建期间抓取生成好的
public/文件; - 嵌入私有源码路径、草稿页面或跨语言回退;
- 承诺一个巨型全文包适合所有模型上下文。
全文包
llms-full.txt 输出拼接每页输出所用的同一份语义化 Markdown。页面之间使用稳定、可见的
分隔符与来源 URL。第 1 版只实现按顶层 section 分包:每个在自身 _index front matter 的
outputs 中显式启用该格式的顶层 section,在语言内得到一个文件。整站单文件形态被推迟,
待真实站点证据表明按 section 分包不够用时再议——巨型单文件既容易超出模型上下文,又会
把所有 section 的更新耦合到一个产物上。
由 Hugo output 配置决定哪些 section 获得该格式,而不是由主题参数决定。主题提供检查器, 报告意图与实际输出不一致,但不能修改站点输出集合。
全文包在 Hugo 内部通过共享页面渲染 partial 组装,不读取 public/ 中的兄弟产物,也不依赖输出
构建顺序。文件大小作为证据报告;任意阈值不能通过警告让 --panicOnWarning 拒绝原本合法的发布。
导航 JSON
导航 JSON 是 home output,与 llms.txt 同级:每种语言在语言根下一个文件。内容包含
schema 版本、语言、根节点与递归有序节点。页面节点包含稳定 ID(语言内 permalink 路径)、
标题、HTML URL、启用时的 Markdown URL、kind 与 children;有 description 时一并携带。
显式外部导航节点只包含标签、URL 与 external kind。
节点不序列化 weight:数组顺序就是契约,weight 是派生顺序的私有机制,公开它会诱导
消费者重新排序。输出遵循渲染侧栏相同的可见性与排序规则,排除 draft、headless resource、
隐藏导航项与当前语言不可用页面,永不序列化本机文件名。
该格式拥有自己的 JSON Schema(schema/nav.v1.schema.json,手工编写的版本化契约产物,
不属于生成式配置 Schema 的漂移门禁)与 golden 夹具,并标记为 notAlternative,避免 Hugo
把它广告为页面级 alternate。
发现信息与输出边界
llms.txt 默认列出已经启用的全文包与导航 JSON——发现信息属于索引文件,这正是它存在的
理由。HTML head 继续发现每页 Markdown 和语言内 LLMS 索引,不把每个批量产物塞进每一页。
shortcode、Landing section、Book 目标与交互组件继续使用当前 Markdown 降级。新输出无权增加组件 HTML、脚本、评论、反馈控件或导航 chrome。
兼容与迁移影响
两种输出都默认关闭,未启用的站点字节不变。启用是站点侧的 Hugo outputs 配置,没有新的
params 键,没有重命名,没有迁移步骤。关闭输出即完全退出,不留残余。
实现与归属检查器计划
- 输出格式:
LLMSFULL(text/plain、baseName: llms-full、notAlternative、 section 级)与NAVJSON(application/json、notAlternative、home 级), 与既有MARKDOWN/LLMS定义并列声明。 - 模板:section 的
llms-full布局复用每页 Markdown 输出的共享渲染 partial 按导航 顺序拼接;home 的导航 JSON 布局走既有导航权威 partial,不引入第二套树遍历。 - 归属检查器:新增
bin/check-agent-indexes.py,在tests/site夹具上验证语言隔离、 链接可解析、顺序与侧栏一致、schema 合规、字节稳定重建,并报告每个包的字节数与页数 (只报告,不设上限门禁)。 - Golden:
check-goldens.py矩阵增加 llms-full 与导航 JSON 夹具。 - 文档:站点新增双语 Agent 索引指南;
llms.txt发现行为并入既有 LLMS 文档; 本提案按生命周期退役。
验收标准
- EN 与 ZH 输出只包含各自语言的页面和 URL。
- 每个列出的 Markdown URL 都存在;每个导航 URL 都可解析,或明确标记为外部节点。
- 同一根下的顺序与渲染侧栏、pager 一致。
- 导航 JSON 通过
schema/nav.v1.schema.json校验。 - 固定 Hugo 版本与输入时,相同源码重建得到字节稳定输出。
- 新格式关闭时,HTML、Markdown、Print、RSS 与 LLMS golden 均无回归。
- 大站夹具能证明按顶层 section 分包,而不是为每个嵌套 section 都生成文件。
决策日志
- 2026-08-20:起草;全文包给出全站与按 section 两种形态,导航 JSON 位置未定。
- 2026-08-27:决议五个待决问题并接受提案。全文包第 1 版只做按顶层 section 分包,
整站单文件推迟到有真实证据;导航 JSON 定为 home output;schema v1 节点元数据取
最小集(稳定 ID、标题、HTML URL、Markdown URL、kind、children、可选 description),
不序列化
weight;llms.txt默认列出已启用的两类产物;检查器只报告体积证据, 不执行任何模型上下文上限。
8.4 - Book 出版链路
选择启用的 BookManifest、通用 EPUB/PDF runner 与产物校验都已随版本发布,
其规范性描述在架构。没有任何一次构建会自己产出这两种文件。
本提案仍然未决、且只在本提案未决的,是消费站迁移。
背景与证据
OINK 已经负责 Book 导航顺序、编号图表公式示例、交叉引用、整书 Print HTML、标题与 脚注命名空间,以及逐页 Markdown 降级。缺失的是一份机器可读的整书交接产物,供 通用打包器消费。
DDIA 当前保留了一份规模不小的 EPUB 预处理器,持续追踪 OINK 的编号原语、跨页链接、 脚注、图片路径与 Book 顺序。TPME 则留有一份更早的导出脚本,它依赖的历史根目录文件 已经不再匹配当前 Hugo 内容树。前者证明出版需求真实存在,后者证明消费站自有配方会 悄悄过期。
EPUB 不是一份模板渲染结果。它是一个 ZIP 容器,包含出版元数据、资源 manifest、 spine、导航、内容文档、样式与媒体。Hugo 可以渲染中间输出,但最终文件必须由打包工具 生成并校验。
目标
- 让每一种 Book 原语在出版场景中只有一份主题拥有的语义结果。
- 发布选择启用的整书中间产物,确定性记录页面顺序、稳定目标与交叉引用;只在显式打包时 从语义 Print 文档解析本地资源。
- 提供通用、带版本的 EPUB 打包器,以及版本固定的 Print-to-PDF runner;消费站只传入 自己的出版事实。
- 用两个结构不同的公开 Book 消费站证明边界成立。
非目标
- 不为每个站点或分区默认启用昂贵的聚合输出。
- 不把 Markdown、JSON 或 HTML 中间产物称为 EPUB。
- 不猜测书名、作者、封面、ISBN、版本、权利或发布策略。
- 普通 Hugo 构建不抓取远程图片或服务。
- 不增加第二套 Book 外壳、第二个导航权威或通用出版配置命名空间。
- 不承诺不同浏览器引擎产生逐像素相同的 PDF 分页。
所有权边界
| OINK 负责 | 消费站负责 |
|---|---|
| 从现有导航权威导出的 Book 顺序 | 要发布的语言、版本与 Book 根 |
fig、tbl、eq、eg、xref、标题与脚注的语义降级 |
书名、作者、标识符、封面、权利与出版者信息 |
| 稳定中间 schema 与通用打包器行为 | 可选章节排除,以及出版专用的前后置内容 |
| EPUB 结构/链接校验与 Print-to-PDF runner | 发布自动化、签名、分发与法律批准 |
| 主题夹具与兼容检查 | 内容正确性与最终产物批准 |
消费站传递事实,不补丁 OINK 标记;OINK 提供语义,不决定一本书是否可以分发。
提议行为
第一步实现是一份选择启用的 Book manifest,不是最终电子书。它引用已经发布的逐页 Markdown,只记录主题能够诚实推导的事实:
- schema 版本与语言;
- Book 根与拍平后的页面顺序;
- 页面标题、可选 Book 编号、HTML URL 与 Markdown URL;
- 稳定标题目标与编号对象目标;
- 跨页引用。
只有明确启用该输出的 Book 根才生成 manifest。普通 HTML、Print、Markdown、RSS、 搜索与导航构建不受这项 opt-in 影响,保持彼此独立。
OINK EPUB 打包器消费 manifest 与既有整书 Print HTML;后者已经包含命名空间化标题与脚注、 编号目标、作者原始锚点、MathML 与静态交互降级。打包器只改写出版 URL,调用固定版本的 Pandoc 3.10 profile,再用 EPUBCheck 与 OINK 自有内部目标检查器验证。消费站提供一份小型 metadata 文件与封面。逐页 Markdown 仍作为可审计的源码形态输出记录在 manifest 中, 但不成为第二条语义转换路径。
本地资源必须存在于生成的 public/ 树下,默认打包完全不访问网络。如果消费站明确保留了
远程图片,必须显式传入 --allow-remote-resources;它只允许被动 HTTP(S) 媒体,绝不放行
远程脚本或本地文件协议。除非明确传入 --force,工具也不会覆盖已有 EPUB。
PDF 来自同一份整书 Print HTML。Runner 在带 script-src 'none' 的临时回环服务中提供构建产物,
默认阻止外部资源,调用显式指定的 Chrome/Chromium 二进制,并拒绝隐式覆盖。Print CSS 负责 A4 纸张、纸面代码
换行、占满版心的编号公式与页码。Checker 通过 Poppler 校验 PDF 结构、A4 几何、可提取的
Book 标题与抽样页码;最终批准仍包含渲染页面复核。
参考工作流保持四个显式步骤,不增加 Hugo 模式:
输出、无障碍与安全
- HTML 与现有输出不加载 exporter,也不增加浏览器运行时。
- 中间产物保留文档语言、标题层级、替代文本、表头、链接文本与源码顺序。
- 交互控件沿用既有静态 Markdown/Print 降级。
- 资源路径必须解析到构建产物内部,或明确作为外部链接;打包过程绝不跟随作者内容中的 任意本地路径。
- 任何消费站值在未经现有输出同等级的校验与规范化之前,都不能成为原始 HTML、CSS、 命令参数或文件系统路径。
兼容与迁移
这是一项增量、选择启用的能力。现有 Book 站保留当前输出与脚本。只有当主题中间产物能 解释 DDIA 当前校验的每一章、编号对象、脚注、图片与内部链接后,DDIA pilot 才删除消费站 侧的转换。TPME 是第二消费站门禁;任何 DDIA 专用路由、标签或章节清单都不能进入通用 schema。
原型证据
第一版 manifest 在两个消费站隔离快照中选择启用,未修改任一消费站工作树,结果如下:
| 消费站 | 有序页面 | 标题 | Markdown 原始锚点 | 编号目标 | Xref | 未解析 |
|---|---|---|---|---|---|---|
| DDIA | 23 | 597 | 33 | 131(106 图、3 表、22 示例) | 292 | 0 |
| TPME | 18 | 295 | 946 | 41(31 图、10 表) | 1,062 | 0 |
两份 manifest 都没有重复编号目标 ID。本机样本中,严格构建额外耗时约为 DDIA 0.28 秒、 TPME 0.22 秒。TPME 大量保留原始锚点这一事实很关键:打包器必须消费仍保留这些显式锚点的 渲染输出。整书 Print HTML 已经同时携带命名空间化标题、脚注、编号目标与 MathML,因此 manifest 不应再复制整棵文档树。
随后,两份隔离快照通过同一个通用命令完成打包:
| 消费站 | EPUB 章节 | 带类型目标 | 包大小 | OINK 包/链接检查 | EPUBCheck 5.3.0 |
|---|---|---|---|---|---|
| DDIA | 23 | 131 | 22.9 MB | 0 错误 | 0 错误、0 警告 |
| TPME | 18 | 41 | 2.2 MB | 0 错误 | 0 错误、0 警告 |
DDIA 唯一的远程海报图使用了显式网络资源开关;TPME 完全由本地产物打包。通用 checker
按 BookManifest 校验每个页面锚点,以及每个目标的 kind 与 num,不再依赖 DDIA
旧预处理器的包装类。主题夹具同样通过了 Hugo minify 后的打包检查。出版 CI 把 Pandoc
3.10 与 EPUBCheck 5.3.0 的版本及归档摘要固定下来,并与 Hugo 兼容矩阵分为独立 job。
Print-to-PDF 试点使用 Chrome for Testing headless shell 151.0.7922.34,并在同一出版 CI job 按归档摘要固定:
| 消费站 | Book 页面 | PDF 页数 | 包大小 | 结构/文本/页码检查 | 渲染复核 |
|---|---|---|---|---|---|
| 主题夹具 | 5 | 23 | 1.1 MB | 0 错误 | 封面、表格、代码、公式、脚注 |
| DDIA | 23 | 527 | 60.3 MB | 0 错误 | CJK、表格、图片、代码、参考文献、后置内容 |
| TPME | 18 | 197 | 8.4 MB | 0 错误 | CJK、宽表、代码、callout、后置内容 |
三份 PDF 均为 Tagged、未加密的 A4 文档。真实消费站 checker 找到了全部 manifest 页面标题,
以及首页、中页和末页的 CSS 页码。视觉复核还暴露并修复了三项既有 Print 缺陷:子页面数学
能力未把 KaTeX 样式传播给整书聚合;过宽的 Bootstrap 列重置误命中 KaTeX col-align-*
内部类;pre > code 覆盖了纸面换行。它们都是范围明确的 Print 修复,不是 exporter 专用
DOM 改写。
实施计划
- 已完成:把 Print 页面序列提取成一份共享 Book partial,不改变 Print 产物。
- 已完成:增加默认关闭的 manifest 与夹具 checker。
- 已完成:用同一条通用 EPUB 链路打包 DDIA 与 TPME 隔离快照,再校验带类型目标、 内部链接与 EPUB 3.3 合规性。
- 已完成:用同一份固定 Chrome runner 渲染主题夹具与 DDIA/TPME 隔离快照,校验并 目视复核代表页面。
- 下一项消费站迁移:只有在 DDIA 仓库独立接受新出版门禁后,才用 metadata 加一次 调用替代它的语义预处理器。
验收标准
- 默认站点不发布新聚合文件,也没有显著构建成本。
- 固定的 Hugo Extended 0.165.0 工具链能在 warning 即失败模式下构建 opt-in 夹具。
- 现有 HTML、Print、Markdown、RSS、导航、搜索与浏览器测试全部通过。
- DDIA pilot 保留 23 章以及全部 106 图、3 表、22 示例与内部链接;这些都作为带类型的 语义目标存在,未解析目标为零。
- TPME 通过同一份 schema 与打包器生成产物。
- 消费站脚本不再包含 OINK 原语专用正则表达式。
- EPUBCheck 与 OINK 包/链接检查器通过;PDF 结构/文本/页码 checker 与代表页面渲染复核 全部通过。
待决问题
- 消费站迁移应发生在下一个 OINK release tag 之前还是之后?
决策记录
- 2026-08-24:起草主题/消费站所有权边界;先选择 opt-in 语义中间产物,不提前承诺最终 EPUB API 或实现。
- 2026-08-24:DDIA/TPME 隔离试点解决了第一项格式决策:保留一份引用既有逐页 Markdown 的 JSON manifest,并消费既有整书 Print HTML;不增加生成式整书 Markdown 或另一条 语义降级路径。
- 2026-08-24:打包器消费整书 Print HTML,因此 manifest 同样使用既有
no_print排除。 这样出版顺序只有一份,也不需要第二个输出专用排除键。 - 2026-08-24:实现通用 EPUB 链路,并在出版 CI 固定 Pandoc 3.10 与 EPUBCheck 5.3.0。 DDIA 与 TPME 隔离包同时通过带类型目标/内部链接检查与官方 EPUB 3.3 校验。
- 2026-08-24:远程出版资源继续默认拒绝。DDIA 的历史远程海报图通过显式 opt-in 验证, 没有因此放松默认值,也没有加入 DDIA 专用改写。
- 2026-08-24:增加回环 Print-to-PDF runner,并按归档摘要固定 Chrome for Testing headless shell 151.0.7922.34。主题、DDIA 与 TPME PDF 均通过结构、文本、A4、页码检查 与渲染复核。
- 2026-08-24:PDF 复核只修复 owning Print 契约:聚合数学能力传播、Bootstrap 列选择器 范围、代码换行、编号公式单列布局与 CSS 页边距。