# Oink 0.5.0 — 组件 API v5 与收敛后的配置

> Oink 0.5.0 用原生 Markdown 形态取代大多数 shortcode，把全部配置键与 front matter 键收敛到三条规则上，删除 0.x 兼容层，并附带把 0.4 站点改写到位的迁移工具。 每一个旧键、旧形态、旧 shortcode 都会让构建失败并指出替代写法，而不是被静默忽略。

---

LLMS 索引： [llms.txt](/zh/llms.txt)

---

Oink 0.5.0 是 API 冻结版本。它包含 1.0 线将要冻结的全部变更：组件 API v5
（原生 Markdown 形态优先，29 个 shortcode 作为完整形态）、按三条规则收敛的配置键与
front matter 键、主题产出物统一的命名空间、0.x 兼容层与无人使用的 Docsy 遗留物的
删除，以及能把 0.4 站点改写到位的迁移工具。任何被退役的键、形态或 shortcode
都会让构建失败，并在错误信息里给出替代写法。

对每一个 0.4 站点来说这都是破坏性升级。请先看[概览](#at-a-glance)，
再看[迁移指南](#migration)；中间的参考章节逐项列出每一处变化的新旧形态。

**v0\.5\.0 · 2026-08-18**
- [查看发布](https://github.com/pgsty/oink/releases/tag/v0.5.0)
- [源码 · tar\.gz](https://github.com/pgsty/oink/archive/refs/tags/v0.5.0.tar.gz)
- [源码 · zip](https://github.com/pgsty/oink/archive/refs/tags/v0.5.0.zip)
- [pgsty\/oink](https://github.com/pgsty/oink)

## 概览 {#at-a-glance}

- **内容**：大多数组件直接用 Markdown 写——`> [!TYPE]` 提示块、`{.steps}` /
  `{.cards}` 列表、`{.fields}` / `{.matrix}` / `{caption=}` / `{#id num=}` /
  `{tab=}` 表格、```` ```filetree ```` / ```` ```gallery ```` / ```` ```echarts ```` /
  ```` ```infographic ```` / ```` ```checksums ```` 数据围栏、相邻代码围栏成标签页、
  带属性行的 Markdown 图片。0.4.2 的 53 个 shortcode 中 32 个删除或改名、8 个新增，
  剩下 29 个作为完整形态。`scripts/migrations/oink06.py` 负责改写内容。
- **配置**：三条规则——开关就是裸的特性名、单键 map 压平、front matter 键 =
  站点键去掉 `ui.`。约四十个键改名或改形，每个旧键都会让构建失败并指出替代。
  主题的全部默认值都声明在主题的 `hugo.yaml` 里。
- **Front matter**：不再有 `ui:` 块；页面覆盖用裸键（`section_index: cards`），
  `page_context_menu` 与站点 map 同形，`manualLink*` 改为 `manual_link*`，
  `hide_*` / `exclude_search` 删除。
- **命名空间**：主题 class 一律 `td-*`、data 属性 `data-td-*`、自定义属性 `--td-*`、
  JS 全局 `Oink*`；`oink-*` 一族和 Docsy 遗留（`leaf`、`has-child`、`nav-*` …）
  消失。提示块文案改为 `callout_*` i18n 键。
- **删除**：`home/**` 适配 partial、`outputformat.html`、`td/render-heading.html`、
  Docsy community 页面与 `params.links`、`td/code-dark` / `td/color-adjustments-dark` /
  `td/gcs-search-dark` / `td/extra` 这些 Sass 文件、`.td-box*` 与 `-bg-*` 调色 class、
  Prism、Open Sans、`click-to-copy.js`、`swaggerui`（改名 `swagger`）。
- **行为**：标题带自链接；print 内容每次构建只渲染一次（修了一个真实的竞态）；
  三个可缓存的 JS bundle；print 页面加载 8 KB JS 而不是 100 KB；shell 动效在结构上
  遵守 reduced-motion；giscus 调色板随主题发布并且只在渲染评论的页面加载。
- **迁移**：内容与 front matter 走 `oink06.py report → migrate --write → check`，
  然后构建——报错就是配置清单。
- **发布加固**：API 冻结前进行了两轮对抗性评审，修复了客户端命名空间迁移、
  动作注册表加载顺序、迁移输入 fail-closed、多实例 OpenAPI 嵌入、通用属性与图片
  URL 策略，以及消费站配置预检。

## 组件 API v5 {#components}

### 原生形态优先 {#native-forms}

v5 的原则：Markdown 块能表达的组件就用 Markdown 写；shortcode 只为块表达不了的
情形存在。渲染钩子识别原生形态，所有钩子共用一套属性策略。

**组件：原生形态与完整形态**

- `Callout 提示块` — `native`

  `> [!NOTE] Title` 引用块；`[!TYPE]-` 折叠 / `[!TYPE]+` 展开；可选 `{icon="fa-solid fa-x"}`；类型 `note tip important warning caution success danger question example quote details`。没有 shortcode。

- `Tabs 标签页` — `native + shortcode`

  原生：相邻围栏（或表格）加 `{tab= group= value=}`。

  Shortcode：`tabs group= default= label=` `tab label= value=` … `/tab` `/tabs`。

- `Steps 步骤` — `native + shortcode`

  原生：`1.` 列表 + `{.steps}`。

  Shortcode：`steps` 加标题——唯一用 `%` 分隔符书写的 shortcode（正文是页面级 Markdown）；步骤里的标题会进入页内目录。

- `Cards 卡片` — `native + shortcode`

  原生：链接列表 + `{.cards}`。

  Shortcode：`cards` `card title= link= icon= badge= image= image_alt=|decorative=` 正文 `/card` `/cards`。

- `Fields 字段` — `native + shortcode`

  原生：表格 + `{.fields [caption=] [id=] [meta="type required default -"]}`——第一列是名称，最后一列是说明，中间列是元数据 chip。

  Shortcode：`fields label= id= class=` `field name= type= required= default=` 正文 `/field` `/fields`——用于块级说明（本列表就是）。两种形态渲染同样的 chip；每个条目有 `#field-<name>` 锚点。

- `FileTree 文件树` — `native`

  ```` ```filetree {title=} ```` 围栏，每个条目一行 `- name[/]  # comment {icon= tone= open= type=}`；2/4 空格、tab 或 `tree` 缩进。CSS + 原生 `<details>`；注释列在构建期对齐。没有 shortcode。

- `Gallery 画廊` — `native`

  ```` ```gallery ```` 围栏，每张图一行 `![alt](src) # description {link= class=}`；alt 必填，条目可缩放。没有 shortcode。

- `Image 图片` — `native`

  `![alt](src "title")` 加属性行 `{#id num= caption= width= height= link= command= options=}`，承担图注、编号、链接与 Hugo 图片处理。`imgproc` 退役；没有图片 shortcode。

- `表格族` — `native`

  `{.full-width}` `{.fields}` `{.matrix}` `{caption=}` `{#id}` `{#id num= caption=}` `{tab= group= value=}`；站点 class 透传。互斥：fields ⟂ matrix / full-width / num；num ⟂ tab。

- `Fig / Tbl / Eq / Eg` — `native + shortcode`

  原生：图片 / 表格 / `$$` 块 / 围栏 + `{#id num= caption=}`（默认 id `fig-`、`tbl-`、`eq-`、`eg-<num>`）。

  Shortcode：`fig` `tbl` `eq` `eg`（`eg` 必须有图注）。

- `Xref 交叉引用` — `native + shortcode`

  原生：普通 Markdown 链接（不带 kind）。

  Shortcode：`xref fig|tbl|eq|eg="…" [page=] [anchor=]`。

- `Book 索引` — `shortcode`

  `book-toc` `book-figures` `book-tables` `book-equations` `book-examples`——没有 `kind=` 参数。

- `代码围栏` — `native`

  围栏属性 `{title copy wrap collapse label id tab group value num caption lineNos hl_lines lineNoStart anchorLineNos tabWidth}`；只有 Chroma。

- `数据围栏` — `native`

  `mermaid plantuml markmap math chem echarts infographic checksums filetree gallery`；`echarts` 只做声明式配置，`$fn:<name>` 回调来自 `window.OinkEchartsFunctions`。

- `叶子组件` — `shortcode`

  `kbd` `badge` `param` `include` `comment` `contributors` `asciinema`（原生 `<kbd>` 同样可用）；`badge` 没有 `outline`，`param` 只接受标量。

- `Release / OpenAPI` — `shortcode`

  `release-card` `release-assets` `download` / `swagger` `redoc`；`checksums` 围栏是发布信息的原生形态。

29 个 shortcode：核心 14（`tabs tab steps cards card fields field include kbd
badge param comment contributors asciinema`）、Book 10（`fig tbl eq eg xref
book-toc book-figures book-tables book-equations book-examples`）、Release 3、
OpenAPI 2。嵌套名（`tab`、`card`、`field`）只在父级里合法；每个 shortcode 都校验
参数，未知参数导致构建失败（0.4 里 `asciinema`、`redoc`、`swagger`、`param`、
`comment`、`steps` 会静默接受任何参数）。

### 删除的 shortcode 与替代 {#removed-shortcodes}

每个条目上的 chip 是迁移工具键（`scripts/migrations/oink06.py migrate --only <key>`）；
`manual` 表示报告会列出、需要人工修改。

**删除的 shortcode 与替代**

- `alert · details · td-page-notice` — `callout`

  0.4：`alert color=… title=…`、`details`、`td-page-notice`（都是 `%` shortcode）、原生 `<details><summary>`。

  0.5.0：`> [!TYPE] title` 提示块，折叠块用 `> [!DETAILS]-`。

- `tabpane · tab · code-group · code-tab` — `tabs`

  0.4：`tabpane` 配 `tab header=…`（都是 `%` shortcode）、`code-group` 配 `code-tab`。

  0.5.0：相邻围栏加 `{tab= group= value=}`（纯代码面板），或 `tabs` 配 `tab`（混合内容）。

- `filetree · filetree/folder · filetree/file` — `filetree`

  0.4：`filetree` 与 `filetree/folder`、`filetree/file`；过渡期的 `{.filetree}` 列表标记。

  0.5.0：```` ```filetree ```` 围栏——`label` 改为 `title`；`open`、`icon`、`color`、`comment`、`link` 保留。

- `gallery · gallery/image` — `gallery`

  0.4：`gallery` 与 `gallery/image`；图片列表 + `{.gallery}`。

  0.5.0：```` ```gallery ```` 围栏。

- `echarts · infographic` — `datafence`

  0.4：`echarts`、`infographic` shortcode。

  0.5.0：同名数据围栏；`$fn:` 回调不变，`js` 子围栏改到 `window.OinkEchartsFunctions`。

- `doc-cards · doc-card · nav-cards · nav-card · card · cardpane · doc-carousel` — `cards`

  0.4：Docsy 的卡片族与 OINK 的 `doc-cards` / `nav-cards` 包装。

  0.5.0：`cards` 配 `card`，或链接列表 + `{.cards}`。`card` 作为 `cards` 的子元素保留名字，契约不同。

- `imgproc` — `image`

  0.4：`imgproc …`（以及发布前短暂存在的 `image …`）。

  0.5.0：`![alt](src)` + 属性行 `{command= options= caption=}`。

- `readfile` — `include`

  0.4：`readfile file=…`。

  0.5.0：`include file=… [code=true lang=…]`——依次查页面资源、assets、content 相对路径。

- `围栏 filename=` — `fencetitle`

  0.4：围栏上的 `{filename="x"}`。

  0.5.0：`{title="x"}`。

- `badge outline=` — `badge`

  0.4：`badge … outline=…`。

  0.5.0：去掉 `outline`——只有一种徽章外观。

- `example · book-figures kind=` — `eg`

  0.4：自闭合的 `example …` + 围栏；`book-figures kind="tbl"`。

  0.5.0：`eg` … `/eg`；`book-tables`、`book-equations`、`book-examples`。

- `fields · field（百分号形态）` — `fieldsdelim`

  0.4：用 `%` 分隔符书写的 `fields` / `field`（从未发布）。

  0.5.0：`fields` / `field`。

- `_param · iframe · conditional-text · netlify · 不带 kind 的 xref` — `reportonly`

  带 `file:line` 报告，人工处理；`_param` 占位符由 `param_placeholders` 变换处理。

- `blocks/cover · blocks/feature · blocks/lead · blocks/link-down · blocks/section` — `reportonly`

  0.5.0：`layout: landing` + `sections`（数据文件或内联 front matter）。只报告，不改写。

- `swaggerui` — `manual`

  改名为 `swagger`；改调用名即可。

- `pageinfo` — `manual`

  改写为 `> [!NOTE]` 提示块。

- `td/site-build-info/netlify.md` — `manual`

  删除，无替代。

相对 0.4.2 新增：`tabs`、`cards`、`include`、`eg`、`book-tables`、`book-equations`、
`book-examples`，以及改名而来的 `swagger`。`card` 与 `tab` 名字未变，但现在是
`cards` / `tabs` 的子元素，契约不同。

没有图片 shortcode：渲染钩子统一为 Markdown 图片、`fig` 与配置里的图片来源解析页面
资源、栏目资源、全局 assets 以及 static / 远程路径，并把图注、编号、链接与 Hugo
图片处理（`command`、`options`）都放在属性行上——`imgproc` 能做的它都能做。

### 块属性策略 {#attribute-policy}

所有渲染钩子（表格、图片、代码块、passthrough、引用块、标题）共用一套策略：
白名单键由钩子消费，`class` 经 token 校验后透传，`data-*` 与 `aria-*` 透传，
`style`、`on*` 与任何未知键让构建失败。内容上的站点 CSS class 仍然合法；
内联样式和事件处理器永远到不了输出。

### 代码围栏 {#code-fences}

- `{filename="x"}` 改为 `{title="x"}`；同一围栏上 `title` 与 `filename` 互斥。
- Prism 路径删除。`params.prism_syntax_highlighting`、`static/js/prism.js`、
  `static/css/prism.css` 不复存在；Chroma 加 `params.highlight_classes`（默认
  `true`）是唯一的高亮器。Prism 无法与 `tab`、`group`、`value`、`num`、`caption`
  共存，任何用了标签页或编号示例的 0.4 站点一开启它就已经构建失败。
- 复制控件依次遵循围栏上的 `copy=all|command|true|false`、会话类 lexer 默认值
  （`console`、`shell-session` → `command`）、再是 `all`。`params.ui.code_copy: false`
  只改站点默认值；显式写了 `copy` 的围栏仍按自己的写法。旧键
  `disable_click2copy_chroma` 会静默压过作者的显式值。
- Docsy 的 `click-to-copy.js`（0.3 起从未加载）及其 `.td-click-to-copy` 样式删除。

## 配置 {#configuration}

### 三条规则 {#config-rules}

1. 布尔开关就是裸的特性名：`ui.annotation: true`，不是 `ui.annotation.enable`，
   也不是 `ui.annotation_enabled`。仅存的 `_enabled` 后缀是 `ui.navbar_enabled`、
   `ui.sidebar_enabled`、`ui.sidebar_root_enabled`——它们的裸名会与同族兄弟键冲突。
2. 单键 map 压平成标量。只有拥有多个子设置的特性才保留 map——`comments`、
   `ui.feedback`、`ui.page_context_menu`、`ui.dark_mode`、`ui.command_palette`、
   `ui.alt_site`、`taxonomy`、`print`、`search`、`plantuml`、`drawio`、`mermaid`、
   `copyright`、`ui.taxonomy_icons`——其中开关型的同时接受裸布尔
   （`comments: false`、`plantuml: false`、`dark_mode: true`、`feedback: true`、
   `page_context_menu: false`）。
3. front matter 键 = 站点键去掉 `ui.` 前缀，没有例外（见 [Front matter](#front-matter)）。

键名一律 snake_case、正向、按用途命名。camelCase 只在原样透传给外部运行时的地方
保留（`comments.giscus.*` 是 giscus 自己的属性名，`mermaid.*` 交给
`mermaid.initialize()`）。

任何旧键或旧形态都会让构建失败并指名替代——站点配置由
`layouts/_partials/config-legacy.html` 负责，页面由
`layouts/_partials/front-matter-legacy.html` 负责——升级就是按报错逐条替换。
没有任何东西被静默忽略。

### 改名与改形的站点键 {#config-renames}

| 0.4 | 0.5.0 | 说明 |
| --- | --- | --- |
| `offlineSearch`、`offlineSearchIndex`、`offlineSearchMaxResults`、`offlineSearchOnServe`、`offlineSearchSummaryLength` | `offline_search`、`offline_search_index`、`offline_search_max_results`、`offline_search_on_serve`、`offline_search_summary_length` | 环境变量覆盖写 `HUGOxPARAMSxOFFLINE_SEARCH_ON_SERVE=true`（Hugo 的备用分隔符 `x`；`_` 无法定位含下划线的键） |
| `ui.showLightDarkModeMenu`（`true` / `false` / `"enable-only (experimental)"`） | `ui.dark_mode`——`true`，或 `{ enable, show_menu }` | `show_menu: true` 隐含 `enable` |
| `ui.scrollSpy.disable` | `ui.scroll_spy` | 取反；默认 `false` |
| `ui.no_left_sidebar` | `ui.sidebar_enabled` | 取反 |
| `ui.breadcrumb_disable` | `ui.breadcrumb` | 取反；默认 `true` |
| `print.disable_toc` | `print.toc` | 取反；默认 `true` |
| `disable_click2copy_chroma` | `ui.code_copy` | 取反；只设默认值 |
| `ui.readingtime.enable` | `ui.reading_time` | 裸布尔 |
| `ui.ul_show` | `ui.sidebar_expand_levels` | 默认 2 |
| `Taxonomy.taxonomyCloud`、`.taxonomyCloudTitle`、`.taxonomyPageHeader` | `taxonomy.cloud`、`.cloud_title`、`.page_header` | 一个小写 map |
| `ui.annotation.enable`、`ui.image_zoom.enable`、`ui.keyboard_nav.enable` | `ui.annotation`、`ui.image_zoom`、`ui.keyboard_nav` | 裸布尔 |
| `ui.typography.preset` | `ui.typography` | `technical` \| `system`；环境变量覆盖 `HUGO_PARAMS_UI_TYPOGRAPHY=system` |
| `ui.pager.types` | `ui.pager_types` | `[docs, book, blog]` |
| `markmap.enable` | `markmap` | 裸布尔 |
| `content_width`（`slim` \| `norm` \| `wide`） | `reading_width`（`slim` \| `normal` \| `wide`） | Book 正文测量；body class `td-book-content--normal`，令牌 `--td-book-content-normal` |
| `ui.docs_root` | `ui.docs_sidebar_root` | `section` \| `home` |
| `github_url` | `github_repo` | 编辑、历史、Issue 链接由仓库推导 |
| `algolia_docsearch` | `search.algolia` 加 `appId`、`apiKey`、`indexName` | 构建失败 |
| `rss_sections` | 删除 | 从未被读取 |
| `params.links.user[]` / `.developer[]` | 删除 | Docsy community 页面已删 |
| `plantuml.enable`、`drawio.enable` | 不变，且 map 接受 `plantuml: false` / `drawio: false` | |
| `comments.enable` | 不变，且接受 `comments: false` | |
| `comments.giscus.lightTheme` / `darkTheme` | 默认不设 | 默认使用主题自带调色板（见[样式与资源](#styles-assets)） |

主题的全部默认值现在都在主题 `hugo.yaml` 里声明并附取值域注释。以前只是模板回退、
现在正式声明的有：`offline_search: false`、`offline_search_summary_length: 70`、
`ui.breadcrumb: true`、`ui.reading_time: false`、`ui.dark_mode: false`、
`ui.docs_sidebar_root: section`、`ui.sidebar_icon_policy: all`、
`ui.section_index_columns: 2`、`ui.code_copy: true`、`print.toc: true`、
`print.section_break_wordcount: 50`、`markmap: false`、`plantuml.enable: false`、
`drawio.enable: false`、`github_branch: main`。两个默认值保持派生并如此记录：
`ui.quick_links`（来自 `docs_section` 与 `blog_section`）和 `ui.taxonomy_icons`
（内置 `categories`/`tags` 图标）。`ui.sidebar_expand_levels`（2）与
`ui.sidebar_menu_truncate`（2000）的模板回退与声明值一致。

保持不变、继续可用的 Docsy 键：`github_repo`、`github_project_repo`、`github_branch`、
`github_subdir`、`path_base_for_github_subdir`、`time_format_blog`、
`time_format_default`、`version`、`versions`、`version_menu`、
`version_menu_pagelinks`、`archived_version`、`url_latest_version`、`copyright`、
`description`、`author`、`gcs_engine_id`、`search.algolia.*`、`mermaid`、
`plantuml.*`、`drawio.*`、`ui.sidebar_menu_compact`、`ui.sidebar_menu_foldable`、
`ui.sidebar_menu_truncate`、`ui.sidebar_cache_limit`、`ui.sidebar_root_enabled`、
`ui.feedback.{enable,reasons}`。

### 大声失败，而非静默 {#fail-loud}

同时配置多个搜索后端（`offline_search`、`gcs_engine_id`、`search.algolia`）现在会让
构建失败（以前只警告）。PlantUML 不设 `plantuml.svg_image_url`、Diagrams.net 不设
`drawio.drawio_server`、Algolia 缺任一凭据，与 0.4 一样构建失败。构建错误遵循同一
形状——`<component>: <subject> <expectation>; got <value> at <position>`——全小写、
位置只用一个介词、配置错误给出完整的 `params.` 路径；不再指向文档 URL。

## Front matter {#front-matter}

页面键 = 站点键去掉 `ui.` 前缀，front matter 里不再有 `ui:` 块。栏目 cascade 同理
（`cascade: { params: { section_index: cards } }` 或直接裸键）。一个解析器
（`ui-param.html`）为每个可按页覆盖的 `params.ui.*` 设置先读页面值、再读站点值：
`sidebar_menu_compact`、`sidebar_menu_foldable`、`sidebar_expand_levels`、
`sidebar_width_min`、`sidebar_width_max`、`sidebar_item_overflow`、
`sidebar_headings`、`sidebar_enabled`、`section_index`、`section_index_columns`、
`lastmod_commit`、`breadcrumb`、`scroll_spy`、`code_copy`、`keyboard_nav`、
`book_draft_banner`；再加上显式页面键 `navbar_enabled`、`navbar_autohide`、
`footer_style`、`annotation`、`feedback`、`image_zoom`、`reading_time`、
`page_context_menu`、`comments`、`page_width`、`reading_width`。

| 0.4 front matter | 0.5.0 |
| --- | --- |
| `params: { ui: { <key>: … } }`（任何键） | 顶层（或 `params:` 下）的 `<key>: …` |
| `params.ui.image_zoom.enable` | `image_zoom: true \| false` |
| `params.ui.keyboard_nav.enable`、`params.ui.annotation.enable` | `keyboard_nav`、`annotation`（裸布尔） |
| `annotation: { enable: … }` | `annotation: true \| false` |
| `context_menu` | `page_context_menu`（`true \| false`，或 `{ enable, assistant_links }`） |
| `assistant_links`（顶层） | `page_context_menu: { assistant_links: false }`——页面只能收窄站点策略 |
| `hide_readingtime: true` | `reading_time: false` |
| `hide_feedback: true` | `feedback: false` |
| `exclude_search`、`excludeSearch` | `search_exclude` |
| `content_width: norm` | `reading_width: normal` |
| `manualLink`、`manualLinkTitle`、`manualLinkTarget`、`manualLinkRelref` | `manual_link`、`manual_link_title`、`manual_link_target`、`manual_link_relref` |
| `body_class: td-no-left-sidebar` | `sidebar_enabled: false` |
| `contributingUrl` | 随 community 页面删除 |
| `Icon` | `icon`（Hugo 不区分大小写；主题按小写读取） |

未变的页面键：`toc_hide`、`toc_root`、`notoc`、`no_print`、`no_list`、`simple_list`、
`hide_summary`、`sidebar_root_for`、`sidebar_divider`、`sidebar_expanded`、
`sidebar_root_menu`、`sidebar_root_link_self`、`search_keywords`、`search_boost`、
`pager`、`landing`、`sections`、`book_number`、`book_status`、`release`、
`release_products`、`release_group_by_product`、`upstream_attribution`、
`downstream_modified`、`byline`、`author`、`body_class`。

`scripts/migrations/oink06.py migrate --only frontmatter` 会改写以上全部页面键，
包括 `cascade:` map 与列表里的。

## 模板、partial 与布局 {#templates}

删除项，以及复制或调用过它们的站点应改用什么：

| 0.4 | 0.5.0 |
| --- | --- |
| `_partials/home/**`（18 个适配器）、`_partials/home-data.html` | `_partials/landing/**`、`landing/home-data.html` |
| `_partials/outputformat.html` | `.Store.Get "tdOutputFormat"`（`html` \| `print` \| `markdown` \| `rss`，每个 base 模板都会设置） |
| `_partials/td/render-heading.html` 以及调用它的站点侧 `_markup/render-heading.html` | 主题自己的 `_markup/render-heading.html`——删掉站点覆盖 |
| `layouts/community/list.html`、`layouts/docs/community.html`、`_partials/community_links.html` | 无——Docsy community 页面已删 |
| `_partials/taxonomy_terms_article.html`、`taxonomy_terms_article_wrapper.html`、`taxonomy_terms_cloud.html` | `taxonomy-terms-article.html`、`taxonomy-terms-article-wrapper.html`、`taxonomy-terms-cloud.html` |
| `_partials/taxonomy_terms_clouds.html`、`code/markdown-escape.html` | 0.4 里就已是死文件；活的是 `shell/taxonomy-terms-clouds.html`、`content/markdown-escape.html` |
| `_shortcodes/swaggerui.html` | `_shortcodes/swagger.html` |
| 从 0.4 复制的 `layouts/_default/_markup/render-*` | 保留任何覆盖前先与 0.5.0 对比——每个钩子都变了 |

有覆盖的站点还应知道的模板层变化：

- 侧栏两个来源——内容树与显式的 `data/docs_nav.json`——每一行都经
  `shell/sidebar-node.html` 渲染。`shell/config.html` 仍是品牌、logo、栏目配置的
  唯一解析器。
- 每个渲染内容的布局都调用 `content/render.html` 而不是 `.Content`
  （图片缩放候选扫描在那里进行）。
- Print：`print/page-content.html` 通过 `partialCached` 让每页的 print 内容在一次
  构建里只渲染一次；`print/render.html`、`print/content.html`、`book/print.html`
  与各 `single.print.html` 布局都读它。复制过 0.4 print 模板的站点应删掉副本——
  0.4 的流水线在"本身是 section 的章节被父级再次聚合"时会在页面 store 上竞态。
- 标题渲染钩子归主题所有。每个标题带 id 与悬停显示的自链接
  （`.td-heading-self-link`，文案 `ui_heading_self_link`）；print 与 RSS 会剥掉链接。
- DocSearch 容器只有一个 `#td-docsearch`；写死的 `#docsearch-0/1` 两个 id 没有了。

## 样式与资源 {#styles-assets}

### 一个命名空间 {#namespace}

主题产出的一切都有前缀，`scripts/check-namespace.py` 守着这条线。站点里挂在旧名字
上的 CSS 或 JS 必须迁移：

| 类别 | 0.4 | 0.5.0 |
| --- | --- | --- |
| class | `oink-*`（landing 子系统）、`leaf`、`has-child`、`active-path`、`is-open`、`is-active`、`is-hidden`、`is-disabled`、`landing-header`、`landing-nav`、`landing-container`、`article-meta`、`pageinfo`、`nav-*`、`taxonomy-*`、`ul-N` | 全部 `td-*`；站点页头与导航是 `td-site-header`、`td-site-nav`、`td-site-container` |
| data 属性 | `data-oink-*` | `data-td-*` |
| 自定义属性 | `--oink-*`、`--term-*` | `--td-*` |
| JS 全局 | `oink*` / `echartsFunctions` | `window.OinkActions`、`OinkEchartsFunctions`、`OinkLanding`、`OinkSearchEngine`、`OinkSurfaceCoordinator` |
| 作者标记（无前缀，不变） | — | `{.steps}` `{.cards}` `{.fields}` `{.matrix}` `{.full-width}` |

### Sass 与令牌 {#sass}

删除的 Sass 文件（站点 `_styles_project.scss` 若仍 import 会编译失败）：
`td/code-dark`、`td/color-adjustments-dark`、`td/gcs-search-dark`、`td/extra`、
`td/extra/bs-defaults`、`td/extra/buttons`、`td/extra/main-container`、
`td/extra/navbar`、`td/boxes`（`.td-box`、`.td-box--<color>`、`.td-box--height-*`）、
`td/colors`（`.-bg-<name>`、`.-text-<name>`）。删除的变量：`$td-box-colors`、
`$td-print-font-name`、`$td-enable-webfonts`。

改名或新增的令牌：`--td-book-content-norm` → `--td-book-content-normal`
（`.td-book-content--norm` → `--normal`）；`--td-print-font-family` 角色保留但在
两种预设下都跟随 `--td-body-font-family`；新增 `--td-motion-duration-fast`
（100 ms）、`--td-motion-duration`（150 ms）、`--td-motion-duration-slow`（250 ms），
shell 的每个过渡都引用它们，`prefers-reduced-motion: reduce` 把它们置零。

排版：UI 与正文使用 Inter（可变字重，Latin/Latin-ext/西里尔/希腊/越南语子集按
`unicode-range` 提供；CJK 与 emoji 回落到平台字体栈）、无边框行内代码、安静的代码
卡片加悬停显示的复制控件、Mintlify 风格字段行、页末两个文本链接的翻页器、卡片式
栏目索引上方的分隔线。Open Sans（18 个 woff2 子集、652 KB，为一个仅打印用的字体
发布到每个站点）已删除；想在纸面上换字体的站点自己在样式表里设
`--td-print-font-family`。`system` 预设依旧不请求任何品牌字体。

shell 图标改为由 `shell/icon.html` 分发的 Font Awesome class 对
（`<i class="td-shell-icon td-shell-icon--<name> fa-solid fa-…">`）而不是内联
SVG；`--td-shell-icon-size` 设置尺寸盒。

### 发布的资源 {#published-assets}

- 三个 JavaScript bundle 取代按特性组合生成的单个 bundle：`js/actions.js` 与
  `js/core.js` 在每一页字节相同、可以缓存；只有一个小的 `js/page-<hash>.js`
  随页面变化。ECharts 单独一个 `<script>`。print 输出加载 7.9 KB 而不是 100 KB。
- `static/css/giscus-oink-{light,dark}.css` 没有了。调色板以
  `assets/css/giscus-{light,dark}.css` 随主题提供，只在渲染评论的页面发布，
  并且是 `comments.giscus.lightTheme` / `darkTheme` 的默认值；指向旧路径的站点
  删掉那两行即可（或写 giscus 内置主题名 / 自己的样式表 URL）。
- 删除：`static/js/prism.js`、`static/css/prism.css`、`static/webfonts/open-sans/`、
  `assets/js/click-to-copy.js`。`VENDOR.json` 与 vendor 目录哈希已重新生成。

## i18n {#i18n}

- 提示块文案改为带命名空间的键：`callout_note`、`callout_tip`、`callout_important`、
  `callout_warning`、`callout_caution`、`callout_success`、`callout_danger`、
  `callout_question`、`callout_example`、`callout_quote`、`callout_details`。
  主题不再占用 `note`、`example`、`quote` 这类裸顶层键；在自己 `i18n/` 里覆盖过
  这些键的站点需要改名。
- 删除：`community_join`、`community_introduce`、`community_learn`、
  `community_using`、`community_develop`、`community_contribute`、
  `community_how_to`、`community_guideline`。
- 新增：`ui_heading_self_link` 与 `ui_field_self_link`（各语言英文兜底；中文变体已审校）。
- 32 个语言文件保持键完全一致（174 个键）。

## 数据文件 {#data}

- `data/home/<lang>.yaml`（或 `data/home.yaml`）必须列出 `sections`；隐式的
  `hero → metrics → capabilities → principles → cta` 顺序没有了，缺失会构建失败。
- 胖页脚只读 `data/footer/<lang>.yaml`（或 `data/footer.yaml`）。`data/home` 里的
  `footer` 键会构建失败并指出新位置。
- `data/landing/<key>/<lang>.yaml`、`data/docs_nav.json`、`data/download/<key>.yaml`、
  `data/brand.yaml` 不变。

## 行为与输出变化 {#behavior}

- 标题带悬停显示的自链接；print 与 RSS 输出剥掉锚点，Markdown 输出
  （`RenderShortcodes`）不受影响。
- print 聚合每次构建对每页内容只渲染一次。0.4 里"本身是 section 的章节"会被自己的
  print 输出和父级的 print 输出并发渲染两次，两次渲染在页面 store 上竞态——可见
  症状是 `_print/` 里偶发重复的 `td-code-…` id。
- `<main>` 不再带 `role="main"`；侧栏 `<aside>` 不再重复内层 `<nav>` 的
  "Section navigation" 标签。
- `ui.dark_mode: true` 同时开启暗色调色板与 System / Light / Dark 菜单；
  单独的 `show_menu: true` 隐含 `enable`。
- `ui.code_copy: false` 只设默认值（见[代码围栏](#code-fences)）。
- 首页渲染导航栏；提示块标题满足对比度；Gallery 条目与其它图片同等享有缩放；
  标签页运行时保住运行边界、唯一同伴 id 与 print 标题；FileTree 与整个 shell 遵守
  `prefers-reduced-motion`。
- 表格渲染钩子也在 print 与 RSS 输出里运行，表格在交互式 HTML 之外也保留图注、
  编号与滚动容器；两种形态的 `fields` 产出同一渲染，每个条目都有 `#field-<name>` 锚点。
- `llms.txt` 读取 `params.ui.docs_section`，并带描述列出文档页面。
- 图片解析器的错误按调用方标注（Markdown 图片是 `image:`，`fig` 是 shortcode 名），
  配置里的图片来源与内容遵循同一 URL 策略。

## 发布候选加固 {#release-hardening}

最终评审发现了一个系统性迁移缺口：模板已经输出新的 `data-td-*` 契约，若干运行时与
测试 mock 却仍读取旧 dataset 名称；动作清单还位于同步动作注册 bundle 之后，注册表
可能以空状态初始化。这两项现已修复，并新增结构检查阻止回归。页面动作、命令面板搜索、
代码复制与折叠、反馈页面标识、折叠控件文案、Giscus 主题、图片缩放标签与 Asciinema
计时标签，现在会在测试与真实渲染 DOM 中使用同一组属性。

同一轮加固还完成了以下修正：

- `report`、`migrate` 与 `check` 遇到不存在、空、不可读或非 UTF-8 的目标时直接失败，
  不再给出误导性的“无残留”结果；JSON front matter 改用 JSON 解码器解析；
- Markdown、RSS 与聚合打印输出都会执行旧 front matter 检查，页面评论等覆盖项严格
  校验布尔值与 map 形态；
- 数据围栏与提示块会保留允许的 `data-*` / `aria-*` 属性，同时图表布尔参数保持严格；
- 每个 Swagger 与 ReDoc 嵌入都有唯一实例，不再覆盖 `window.onload` 或发布 `window.ui`；
- shell Logo、Wordmark 与配置型 Featured Image 统一使用共享 URL 策略；
- 新增 `scripts/check-site-markup.py`，从消费站解析后的配置中检查原生形态必需的三项
  Goldmark 设置。

## 迁移指南 {#migration}

顺序很重要：先内容（工具默认 dry-run 且幂等），再让构建错误驱动配置与布局的修改。

改写内容前，先确认消费站能够渲染原生形态：

```sh
python3 path/to/oink/scripts/check-site-markup.py --site ~/pgsty/example.com
```

### 1. 盘点 {#migration-inventory}

```sh
python3 scripts/migrations/oink06.py report --sites ~/pgsty/example.com --md report.md --json report.json
```

报告逐站列出工具会改写的每一个 0.4 结构、不会碰的（带 `file:line` 与原因），
以及改写之后仍会被标记的残留。

### 2. 内容与 front matter {#migration-content}

```sh
python3 scripts/migrations/oink06.py migrate --site ~/pgsty/example.com          # dry run：diff + 计数
python3 scripts/migrations/oink06.py migrate --site ~/pgsty/example.com --write  # 原子改写
python3 scripts/migrations/oink06.py migrate --site ~/pgsty/example.com --write  # 第二次：changed 0
python3 scripts/migrations/oink06.py check   --site ~/pgsty/example.com          # 残留旧语法 → exit 1
```

变换按应用顺序：`frontmatter`（页面键，含 `cascade:`）、`callout`、
`param_placeholders`、`tabs`、`filetree`、`gallery`、`datafence`、`cards`、
`fieldsdelim`、`image`、`include`、`fencetitle`、`badge`、`eg`、`reportonly`。
`--only <key>` 选子集。围栏内的文本永远不改写；TOML/JSON front matter 只报告不改写。
在 11 个自有站点上，front matter 变换触及 628 个文件、零 finding。

报告会列出需要人工处理的项：`swaggerui` → `swagger`、`pageinfo` → 提示块、
`_param` 占位符、`iframe`/`conditional-text`/`blocks/*`、不带 kind 的 `xref`，
以及必须改成 `window.OinkEchartsFunctions` 条目的 `echarts` `js` 子围栏。

### 3. 配置 {#migration-config}

构建站点。每个旧键都会带着替代写法失败：

```text
ERROR params.offlineSearch was renamed: use params.offline_search
ERROR params.ui.typography.preset was flattened: use params.ui.typography: technical | system
ERROR params.ui.showLightDarkModeMenu was renamed: use params.ui.dark_mode.show_menu
ERROR params.print.disable_toc was renamed: use params.print.toc (inverted)
ERROR params.rss_sections was removed: the key was never read; delete it
```

一份典型的 0.4 `hugo.yaml` 变成：

```yaml
params:
  offline_search: true
  offline_search_on_serve: true
  offline_search_index: summary
  offline_search_summary_length: 70
  offline_search_max_results: 10
  reading_width: normal            # 原 content_width: norm
  markmap: true                    # 原 markmap: { enable: true }
  print:
    toc: true                      # 原 disable_toc: false
  comments:
    enable: true
    type: giscus
    giscus:
      repo: …                      # lightTheme / darkTheme 两行删除
  ui:
    typography: technical          # 原 typography: { preset: technical }
    dark_mode: true                # 原 showLightDarkModeMenu: true
    sidebar_expand_levels: 2       # 原 ul_show: 2
    scroll_spy: false              # 原 scrollSpy: { disable: true }
    reading_time: false            # 原 readingtime: { enable: false }
    image_zoom: true               # 原 image_zoom: { enable: true }
    keyboard_nav: true             # 原 keyboard_nav: { enable: true }
    annotation: true               # 原 annotation: { enable: true }
    pager_types: [docs, book, blog] # 原 pager: { types: [...] }
    docs_sidebar_root: section     # 原 docs_root
    breadcrumb: true               # 原 breadcrumb_disable: false
    sidebar_enabled: true          # 原 no_left_sidebar: false
    code_copy: true                # 原 disable_click2copy_chroma: false（顶层）
```

删除 `params.links`、`prism_syntax_highlighting`、`rss_sections`、`github_url`
（改用 `github_repo`）、`algolia_docsearch`，以及 giscus 的 `lightTheme` /
`darkTheme` URL。

### 4. cascade 与栏目索引 {#migration-cascades}

设置过 `params.ui.*` 的 `cascade` 改为裸键——`_index.md` 里的由变换处理，
`hugo.yaml` 里手写的 cascade 要自己检查：

```yaml
cascade:
  type: blog
  params:
    sidebar_menu_compact: false    # 原 params.ui.sidebar_menu_compact
    sidebar_expand_levels: 3       # 原 params.ui.ul_show
```

### 5. Sass、布局与站点脚本 {#migration-site-code}

- `assets/scss/_styles_project.scss`：删掉 `@import 'td/color-adjustments-dark'`、
  `'td/code-dark'`、`'td/extra'`、`'td/extra/bs-defaults'`、`'td/gcs-search-dark'`；
  去掉针对 `.td-navbar-cover`、`.td-navbar-transparent`、`.td-box*`、`-bg-*`、
  `oink-*`、`--oink-*` 的规则。
- `assets/scss/_variables_project.scss`：去掉 `$td-print-font-name`、
  `$td-enable-webfonts`、`$td-box-colors`。
- `layouts/`：`partial "home-data.html"` / `"home/section.html"` 换成 `landing/…`；
  `partial "outputformat.html"` 换成 `.Store.Get "tdOutputFormat"`；删掉调用
  `td/render-heading.html` 的 `_markup/render-heading.html`；`taxonomy_terms_*`
  调用改名；其它复制过的 partial 或钩子在保留前先与 0.5.0 对比。
- 站点 JS 与测试：`oink-*` id 与 `data-oink-*` 属性改为 `td-*` / `data-td-*`；
  动作清单是 `#td-action-manifest`；每页 bundle 是 `js/page-<hash>.js`，旁边是
  `js/actions.js` 与 `js/core.js`。
- 站点 `i18n/` 覆盖：`note`、`tip`… 改名为 `callout_note`、`callout_tip`…。

### 6. 数据 {#migration-data}

把 `footer:` map 从 `data/home/<lang>.yaml` 移到 `data/footer/<lang>.yaml`；
确认 `data/home/<lang>.yaml` 列出了 `sections`。

### 7. 校验 {#migration-verify}

```sh
hugo --printPathWarnings --panicOnWarning
python3 scripts/check-output-security.py --public public --base-url https://example.com/
```

然后检查变化最大的几个面：一个有代码标签页与提示块的文档页、一个有图片的页面
（缩放开与关）、一个 Book 章节及其 `_print/` 聚合、`index.md` Markdown 输出、
一个 RSS feed、首页 landing、暗色调色板。`v0.5.0` 标签推送后再固定它：

```sh
hugo mod get github.com/pgsty/oink@v0.5.0
hugo mod tidy
```

## 兼容性 {#compatibility}

- Hugo Extended 0.160.1 仍是最低版本；CI 跑 0.160.1 与 0.164.0，并新增以
  Hugo Module 模式构建消费站点。
- 模块路径仍是 `github.com/pgsty/oink`；消费站点仍不需要 Node.js、CDN 或构建期下载。
- 对 0.4 没有兼容层：改名的键、形态、shortcode、partial、class 要么构建失败要么
  直接消失，这是有意的。旧键报错就是迁移指南；其中源自 Docsy 的条目同样服务
  从 Docsy 迁来的站点。
- 合理的 Docsy 键保持不变（见[配置](#config-renames)下的清单）；`sidebar_*` 一族名字未动。
- 交互特性仍然默认关闭：`offline_search`、`ui.image_zoom`、`comments`、
  `ui.feedback`、`ui.dark_mode`、`page_context_menu.assistant_links` 需要站点主动开启。

## 验证 {#verification}

主题 CI：34 个检查脚本（i18n 键一致、分类法、字体令牌、导航 / 组件 / 内容原语 /
Book 契约、运行时隔离、侧栏图标、搜索、动作、命令面板、阅读、发布资产、下载、
landing、Book 迁移、共享场景、键盘、shell、命名空间、参数、vendor 清单、输出结构
与安全、30 个面的四态 goldens、代码块、内容与媒体原语、图片缩放、Gallery、组件）、
浏览器运行时单测、迁移工具测试（85）、在 Hugo 0.160.1 与 0.164.0 上无警告构建的
fixture 站点、`system` 排版预设、遗留 Sass 覆盖、非法预设构建失败，以及新增的
Module 模式消费站点构建。`scripts/check-params.py` 为每个退役键各构建一个站点
（32 个站点键、14 个页面键），断言每个都失败并指名替代。

本站按上述迁移后在 0.5.0 上无警告构建。最终门禁在 Hugo 0.160.1 与 0.164.0 上各跑
一遍完整矩阵；媒体断言允许各支持版本使用不同的不透明派生缓存哈希，同时仍严格检查
渲染 URL 结构、尺寸、alt 语义与 Zoom 排除。源码校验、本地附注标签、远端标签发布、
消费站固定版本与部署仍是可独立审计的门禁。

## 完整变更集 {#full-change-set}

完整源码差异见 [v0.4.2 到 v0.5.0](https://github.com/pgsty/oink/compare/v0.4.2...v0.5.0)
与主题的 `CHANGELOG.md`。
