使用 OINK Starter,在定制前建立本地预览基线。
这是本节的多页打印视图。 .
OINK 文档
- 1: OINK 是什么
-
2: 快速上手
- 2.1: 使用 OINK Starter
- 2.2: Starter 仓库导览
- 2.3: 从零建站与其它安装方式
- 3: 创作内容
- 4: 组件总览
- 5: 定制站点
- 6: 维护管理
-
7: 设计与开发
- 7.1: 架构契约
- 7.2: 组件契约
- 7.3: 外壳与导航契约
- 7.4: 落地页契约
- 7.5: OINK 迁移边界
-
7.6: 设计决策
- 7.6.1: 警告与安全回退
- 7.6.2: 配置模型
- 7.6.3: Markdown 优先创作
- 7.6.4: 生成式配置 Schema
-
7.7: 设计研究
- 7.7.1: Goldmark 块属性实测
- 7.7.2: 消费站与迁移证据
- 7.7.3: OINK 全面审查(2026-08-26)
- 7.8: 设计提案与 PRD
OINK 是一款技术文档 Hugo 主题。组件是 Markdown 语法的一部分,不是另一套模板语言;浏览器需要的字体、图标、搜索与图表运行时随主题分发;构建依赖只有一个 Hugo Extended 二进制,不需要 Node.js,不请求 CDN。当前发布版本 v1.0.0。
五条入口
- 快速上手 — 创建 OINK Starter 仓库,建立本地基线,分层定制并部署。
- 组件总览 — 每个组件一页,先给源码再给渲染效果。
- 使用 OINK 创作优美的内容 — 从第一次预览到持续维护发布物的实战教程。
- 案例 — 把生产站点拆解成可复用的设计与迁移模式。
- 设计与开发 — 面向 OINK 维护者的契约、已接受决策、研究证据与候选提案。
按任务导航
| 你要做的事 | 去哪 |
|---|---|
| 判断是否适用 | OINK 是什么 |
| 安装并预览 | 快速上手 |
| 写一页文档 | 编写页面 |
| 把目录树变成侧栏 | 组织内容 |
| 查组件写法 | 组件总览 |
| 改站名、Logo、配色与字体 | 品牌外观 |
| 查某个配置键的默认值 | 配置总览 |
| 做双语或多语言站 | 多语言 |
| 从头到尾掌握 OINK | 使用 OINK 创作优美的内容 |
| 研究生产环境实现 | 案例 |
| 部署到线上 | 发布上线 |
| 升级版本或从 Docsy 迁移 | 版本升级 |
| 维护主题、审查契约或编写 PRD | 设计与开发 |
Docs 的七个栏目按阅读顺序排列:了解、上手、写内容、查组件、改站点、管发布,最后理解并维护其背后的契约与设计记录。
1 - OINK 是什么
OINK 是一款独立的 Hugo 主题,用于搭建中大型技术文档站。它从 Docsy 演化而来:保留 Docsy 的内容模型与多语言行为,替换外壳、导航、搜索与内容组件。
消费站点的构建依赖只有一个 Hugo Extended 二进制,不需要 Node.js、npm 或 PostCSS,也不请求 CDN。Bootstrap、Font Awesome、字体、本地搜索、图表与 API 文档运行时都提交在主题仓库里,只在页面用到时下发。
组件不是另一套模板语言:> [!NOTE] 是提示块,表格加一行 {.fields} 是参数表,图片下面加 {caption=} 就有图注。当前有十五个生产站点在用它,本站是其中之一。

主题的职责
- 文档与博客外壳:导航、侧栏树、目录、面包屑、翻页、深色模式、打印视图与无障碍交互。
- 多语言框架:译文路由、缺译回退、语言权重、RTL,以及 32 份完整界面语言包。
- 本地运行时:Mermaid、KaTeX、Markmap、Swagger UI、Redoc、Asciinema、ECharts、Infographic 与本地全文检索。
- 内容组件:提示块、标签页、步骤、卡片、参数表、文件树、画廊、徽章、按键等,多数有 Markdown 原生形态。
- 内容类型:普通文档之外,还内置书籍编号与交叉引用、发布与下载页、数据驱动的 Landing 首页、OpenAPI 文档页。
主题不负责源码托管与部署:站点可以放在 GitHub、GitLab 或私有 Git 上,Hugo 生成的静态文件可用任何托管平台发布。站点自己的内容、品牌与业务组件仍归站点管理,主题只提供通用外壳与可复用组件。
适用范围
| 这些情况适合 | 这些情况不适合 |
|---|---|
| 页面多、内容类型杂:文档、博客、书、发布页与 API 参考共处一个站点 | 只有一两页内容、不需要结构化导航;README 或更轻的 Hugo 主题更简单 |
| 需要完整的多语言,而不是给英文站挂一个翻译入口 | 站点主体是应用界面而不是文档:可以用 OINK 承载文档部分,业务组件留在站点层 |
| 对可复现构建与网络隔离有要求,构建机不能出网 | 需要在正文里写交互组件(React / MDX) |
| 多个站点共享同一套外壳,不必复制布局与 shortcode | 想用一个开关换成另一套视觉:主题没有品牌开关,改外观要走 CSS token 与 partial 覆盖 |
| 团队没有前端,也不维护 Node 工具链 | 需要主题内置内容管理后台或所见即所得编辑器 |
与其它文档方案的差别
下表只列结构性差别,且只写能从各项目自身文档与仓库确认的部分。各项目的版本会变动,选型前以其当前文档为准。
| 维度 | OINK | Docsy | Hextra | Docusaurus |
|---|---|---|---|---|
| 构建工具 | Hugo Extended,单个二进制 | Hugo Extended + Node/npm | Hugo | Node.js 工具链 |
| 消费站点要不要 npm | 不要 | 要:Bootstrap 与 Font Awesome 从 node_modules/ 挂载 |
不要 | 要 |
| 前端资源从哪来 | 全部提交在主题仓库,VENDOR.json 记录版本、来源、许可与校验值 |
每页无条件加载 CDN 上的 jQuery;Mermaid、KaTeX 等还会在构建期请求 CDN | 预编译产物提交在仓库 | npm 依赖 |
| 组件写法 | Markdown 原生属性与围栏为主,29 个 shortcode 兜底 | shortcode(19 个) | shortcode(29 个)为主,提示块有 > [!NOTE] 原生形态 |
MDX(React 组件) |
| 多语言 | Hugo 多语言 + 32 份完整界面语言包 | Hugo 多语言 + 31 个界面 locale 文件 | Hugo 多语言 + 21 个界面语言包 | 内置 i18n 框架 |
| 书籍编号与交叉引用 / 发布下载页 / 数据驱动落地页 | 主题内置 | 无 | 无 | 需自建或找插件 |
两点补充。每页 Markdown 输出与 llms.txt 不是 OINK 独有的能力,Docsy 与 Hextra 也有,三者都要站点在 outputs 里显式打开。表格最后一行的三项只有 OINK 内置,它们来自 PGSTY 自己的生产站点,不是通用文档站的必需品。主题的交互功能默认关闭,搜索、缩放、评论与反馈都要站点显式打开。
OINK 不是叠在 Docsy 上的皮肤,而是 fork 之后独立演化的主题。Docsy 的源码历史、Apache-2.0 义务与署名完整保留,细节见开源许可与致谢。
入口
亮点特性按能力逐条列出主题提供的东西,每条链接到讲它的指南页。
1.1 - 亮点特性
本页逐条列出 OINK 与普通 Hugo 主题的差别,每条末尾给出讲它的指南页。要立即安装,见十分钟上手。
组件写在 Markdown 里
提示块是 > [!NOTE] 块引用(十种语义类型加一个中性折叠块),参数表是表格加一行 {.fields},步骤与卡片是列表加 {.steps} / {.cards},图注是图片下面一行 {caption="…"}。标签页是几个相邻围栏各带一个 {tab="…"};文件树、画廊、Mermaid、ECharts 是以语言命名的数据围栏。这些写法在 GitHub 或普通 Markdown 阅读器中退化为块引用、表格、列表与代码块,内容不丢失。
29 个 shortcode 覆盖原生形态表达不了的场景:卡片带图标与图片、参数表条目正文是多段 Markdown。
→ 组件总览
只要一个 Hugo 二进制
消费站点的全部构建依赖是 Hugo Extended 0.160.1 或更新版本。SCSS 由 Hugo 内置的 Sass 转译器编译,主题不调用 postCSS;没有 npm、没有 webpack、没有构建期下载。用 Hugo Module 方式安装主题时需要本机有 Go 来解析模块,用离线归档或 submodule 则不需要。
「仅依赖 Hugo」指的是构建依赖。界面交互仍在浏览器中执行 JavaScript:搜索、命令面板、图表、标签页都是页面脚本,区别在于这些脚本随主题分发、按页面用到的功能下发。
→ 十分钟上手
本地优先
浏览器需要的资源全部提交在主题仓库里:Bootstrap、Font Awesome、四款字体、Lunr、Mermaid、KaTeX、Markmap、Swagger UI、Redoc、Asciinema、ECharts、Infographic。VENDOR.json 逐项记录 26 个依赖的版本、来源、许可证文件与 SHA-256 校验值,更新某个运行时要同时更新产物、许可证与校验值。
对可能引起网络请求的功能,主题让它保持关闭而不是静默连出去:PlantUML 缺 params.plantuml.svg_image_url、Diagrams.net 缺 params.drawio.drawio_server、Algolia 缺 appId / apiKey / indexName,都会告警并保持禁用;带 --panicOnWarning 的发布关卡会把这条告警变成失败。
本地优先不覆盖作者自己添加的内容。以下都是显式的网络选择:外部链接、远程图片与视频、iframe、远程 API 规范;Algolia、Google 自定义搜索这类托管搜索;分析、评论与其它 SaaS 集成;作者主动配置远程渲染器的 PlantUML 与 Diagrams.net。用到它们的页面仍然是有效页面,但站点不应再宣称这些页面可以完全离线使用。
一份内容,四种输出
每个组件在四种输出下都有确定的形态:交互式 HTML;去掉缩放与复制控件、折叠块完全展开的打印页;纯 Markdown;RSS。打印视图按栏目整份生成(本栏目是 /zh/_print/docs/about/),Markdown 版本是同一页面地址加 index.md。
站点在 outputs 里显式选择需要哪几种,主题不替站点决定。
双语与 32 个界面语言
多语言走 Hugo 原生机制:译文路由、按权重排序的语言选择器、缺译回退、RTL,
以及 canonical 与 alternate 元数据。界面文案有 32 份语言包,共用 192 键
schema:Docsy 支持的 31 个 locale 文件名,再加通用 zh。每份语言包都使用目标
语言覆盖完整 OINK 界面,不再保留英文占位块;zh 与 zh-cn 使用简体中文,
zh-tw 使用繁体中文。
→ 多语言
全文检索不出站
打开 params.offline_search 后,Hugo 为每种语言生成一份索引,浏览器用本地 Lunr 检索拉丁文字、用子串回退检索中日韩文本,查询内容不发给任何第三方。页面可以用 search_boost 调权重、用 search_keywords 补同义词。
→ 全文检索
命令面板
Cmd/Ctrl + K 打开命令面板;裸按 / 进入搜索态,裸按 \ 进入纯命令态。面板里同时有页面、命令与页面动作(切换语言、切换主题、复制 Markdown 等),搜索与操作共用一个入口。
→ 命令面板
键盘导航
默认开启,可按站点或按栏目关闭。w s 在侧栏树上下移动,a d 折叠展开,q e 上一篇下一篇,j k 沿页面目录跳转,t 切换深浅色,l 切换语言,h 隐藏阅读外壳。输入框、文本域获得焦点或输入法处于组字状态时,单键快捷键全部让行。页脚最底层栏的问号按钮打开速查卡。
→ 键盘导航
反向链接
打开 params.ui.backlinks 后,每一页都会列出有哪些页面链接到它——构建时从你本来就写的普通链接派生,没有新语法,也没有 JavaScript。本站全站开启:看本页右栏的「反链」组,越常被引用的页面列表越长,超过八条会折叠。
→ 反向链接
文档之外的四种内容
主题还内置四类需要额外结构的页面:
- 书籍:章节编号,图 / 表 / 式 / 例用
{#id num=}编号、用xref交叉引用,book-toc、book-figures一类 shortcode 生成索引,整本可打印。 - 发布与下载页:
data/download/*.yaml生成发布卡片、资产表与校验和,发布状态可控。 - Landing 首页:
data/home/<lang>.yaml拼装首页分区;任意页面加layout: landing也能用data/landing/的数据。 - API 文档:Swagger UI 与 Redoc 都是本地运行时,spec 放站内即可。
→ 书籍出版 · 发布与下载页 · 首页与落地页 · API 文档
面向 AI 助手的输出
outputs 里加上 markdown,每个页面就多一份 .md,HTML 的 <head> 里带 rel="alternate" 指过去,页面动作里也多出「复制 Markdown」与「查看源码」。LLMS 输出格式在站点根目录生成 llms.txt 内容清单(本站是 https://oink.pgsty.com/zh/llms.txt)。
0.8.0 再加两种:栏目开启 LLMSFULL 后整个栏目拼成一份 llms-full.txt,agent 一次抓完;站点开启 NAVJSON 后每种语言发布一份 navigation.json,侧栏那棵树直接当数据读。两者都在本站开着:https://oink.pgsty.com/zh/docs/llms-full.txt 与 https://oink.pgsty.com/zh/navigation.json 就是真实产物。
「在 ChatGPT / Claude 中打开」默认关闭:读者点击时会把当前 URL 交给第三方,需要站点显式打开 params.ui.page_context_menu.assistant_links。
→ Agent 支持
多版本
配置 params.versions 后顶栏出现版本菜单,旧版本站点顶部显示归档横幅,提示读者查看最新版本;菜单是否逐页跳转由站点决定。多个版本是分别构建、分别部署的静态站点,不需要运行时支持。
→ 多版本
自己验证
本站启用了上面多数特性,四条自查:
- 在任意页面按
Cmd/Ctrl + K,输入postgres查看本地搜索结果;按\进入纯命令态。 - 在当前页面地址后加
index.md,得到这一页的 Markdown 版本。 - 打开 https://oink.pgsty.com/zh/llms.txt,那是给 AI 助手的站点清单;顺着它能找到整个文档栏目的
llms-full.txt与navigation.json。 - 看本页右栏的「反链」组,它列出链接到本页的页面。
相关
1.2 - Case 导览
正式的 Case 案例库 把十五个生产站点整理成可复用的实现模式, 首页展示的也是同样这十五个。它们全都使用 OINK,本站本身也作为自举案例列入。
当你已经知道自己要搭建哪类站点时,可以从这里开始:先通过案例了解架构与 取舍,再沿页面链接进入具体配置文档。案例中的数量描述对应盘点时的快照, 不是对持续变化的线上站点作永久承诺。
发行版文档
pigsty.io
大型英文站,把发行版手册、博客、扩展目录、分类、版本导航与价格落地页放在 同一个站点中。
pigsty.cc
独立部署的中文对等站;当两种语言的语料都已成为完整产品时,拆成两个单语站 是一种清晰的取舍。
pgsty.pro
双语版本档案站,从可复用的结构化发布数据渲染大量版本页面。
产品文档
PIG
紧凑的双语命令行工具手册,配有数据驱动首页与体量更大的博客。
SOW
双语运维手册,使用独立下载内容类型展示发布元数据与产物。
SILO
大型上游迁移案例,通过受检查的清单生成双语文档导航。
PG Exporter
把生成导航、结构化指标目录与系统字体组合起来的指标手册。
书籍
《设计数据密集型应用》
多语言、多版本书籍,也是编号图表、交叉引用、章节导航与索引最完整的案例。
《The Product-Minded Engineer》
只需要 OINK Book 外壳的聚焦型双语出版物。
《PG 技术内幕》
已完稿的中文译本,刻意做成单语 Book:没有文档树,也没有可切换的第二语言。
汇编、落地页与自定义站点
pgsql.cc
聚合型运维文库,让多个上游手册与完成度不一的翻译树共享搜索和视觉体系。
pgsty.com
小型双语公司站,展示 OINK 也可以主要作为数据驱动的落地页系统。
Capslock
每种语言只有两页,其中自定义外壳承载数据驱动交互配置生成器。
oink.pgsty.com
完整参考站:公开文档、实时组件示例、设计契约、多种内容外壳与回归覆盖都在 同一个仓库中。
pgext.cloud
PostgreSQL 扩展目录:把可检索的数据集作为站点主体呈现,收录 2,241 个扩展、 576 个已打包版本,覆盖 16 个 Linux 平台。
如何选择起点
- 常规产品手册:从 PIG 或 SOW 开始。
- 大型迁移:对比 SILO 与 pgsql.cc。
- 书籍:对比精简的 TPME 与更复杂的 DDIA, 单语场景可参考 《PG 技术内幕》。
- 落地页或交互站:参考 pgsty.com 或 Capslock。
- 最完整的参考实现:使用 OINK Docs。
- 如果读者是来查询数据集而不是来阅读的,看看 ext.pgsty.com 如何把数据集作为站点主体呈现。
主题仓库的 tests/site/ 是内部 CI 夹具,而不是起步模板;其中页面的职责是
触发渲染行为。上面的生产案例更适合作为架构与设计参考。
1.3 - 开源许可与致谢
OINK 由三层材料组成:主题源码、文档内容、随主题分发的第三方资源。三者各自的许可证不会被重新授权成一份统一作品。下面每张表都指向仓库里的权威文件,摘要与许可证原文不一致时以文件为准。
许可证对应关系
| 范围 | 许可证 | 权威文件 |
|---|---|---|
| OINK 主题源码(布局、partial、 shortcode、SCSS、JS、i18n) | Apache License 2.0 | 主题 LICENSE、NOTICE |
| 本站的站点代码、构建脚本与源自 Docsy 的材料 | Apache License 2.0 | 站点 LICENSE、NOTICE |
| 本站的原创文档内容(另有声明的除外) | Creative Commons Attribution 4.0 International | 站点 LICENSE-CC-BY-4.0 |
| 随主题分发的浏览器库、字体与图标 | 各组件自己的许可证 | 主题 VENDOR.json 与资源旁的许可证文件 |
两条边界要分清:CC BY 4.0 只覆盖原创文档内容,不覆盖主题代码、商标、截图与第三方资源;主题采用 Apache-2.0,也不会把随附依赖变成 Apache 许可的作品。
上游:Docsy
主题 NOTICE 记录的事实:
- OINK 派生自 Docsy,Copyright 2018 Google LLC and Docsy contributors。
- OINK 自身的主题工作 Copyright 2026 PGSTY contributors。
- 项目与上游同为 Apache License 2.0;第三方浏览器依赖的许可、来源、版本与校验值记录在
VENDOR.json,各自要求的 NOTICE 文件与对应资源放在一起分发。 - Docsy 名称与 Google 商标归各自权利人所有,此处引用只用于标识上游项目,不表示背书。
本站也派生自 Docsy 项目网站,这段渊源记录在站点自己的 NOTICE 里。Docsy 是 OINK 唯一的代码上游:源码历史、Apache-2.0 义务与版权声明完整保留,按 Apache-2.0 的要求,修改过的文件需要标注。
随主题分发的第三方运行时
主题把浏览器要用的资源全部提交在仓库里(assets/third_party/、assets/js/third_party/、static/webfonts/),消费站点不需要 npm,也不会在构建期下载任何东西。VENDOR.json 是这批资源的机器可读清单,逐项记录名称、固定版本、来源 URL、许可证文件路径,以及每个选取产物的 SHA-256;清单里还有三棵资源目录的整体校验值。
下表是清单快照(VENDOR.json 生成于 2026-08-17,schema 1,共 26 项)。版本会随主题发布变动,以仓库里的 VENDOR.json 为准。全部来源都是 npm registry(https://registry.npmjs.org/…)。
| 项目 | 版本 | 许可证 | 在主题里做什么 |
|---|---|---|---|
| bootstrap | 5.3.8 | MIT | 栅格、组件与 RTL 样式基础 |
| @popperjs/core | 2.11.8 | MIT | Bootstrap 的浮层定位 |
| @fortawesome/fontawesome-free | 7.3.1 | CC-BY-4.0 AND OFL-1.1 AND MIT | 全站图标 |
| @fontsource-variable/inter | 5.3.0 | OFL-1.1 | 界面与正文字体 |
| @fontsource/chakra-petch | 5.3.0 | OFL-1.1 | 品牌展示字体 |
| @fontsource/ibm-plex-mono | 5.3.0 | OFL-1.1 | 代码字体 |
| lunr | 2.3.9 | MIT | 本地全文检索 |
| @docsearch/js | 5.0.1 | MIT | 可选的 Algolia DocSearch 前端 |
| @docsearch/css | 5.0.1 | MIT | 同上的样式 |
| mermaid | 11.16.1 | MIT | Mermaid 图表 |
| katex | 0.18.4 | MIT | 数学公式 |
| markmap-autoloader | 0.18.12 | MIT | 思维导图 |
| markmap-lib | 0.18.12 | MIT | 思维导图 |
| markmap-view | 0.18.12 | MIT | 思维导图 |
| markmap-toolbar | 0.18.12 | MIT | 思维导图工具条 |
| d3 | 7.9.0 | ISC | Markmap 依赖 |
| @highlightjs/cdn-assets | 11.12.0 | BSD-3-Clause | Markmap 依赖 |
| webfontloader | 1.6.28 | Apache-2.0 | Markmap 依赖 |
| swagger-ui-dist | 5.32.13 | Apache-2.0 | OpenAPI 文档页 |
| redoc | 2.5.3 | MIT | OpenAPI 文档页 |
| asciinema-player | 3.17.0 | Apache-2.0 | 终端录像回放 |
| echarts | 6.1.0 | Apache-2.0 | 图表 |
| @antv/infographic | 0.2.19 | MIT | 信息图 |
| pako | 3.0.1 | MIT AND Zlib | 解压(图表数据) |
| external-svg-loader | 1.7.1 | MIT | 内联外部 SVG |
| idb-keyval | 6.2.0 | Apache-2.0 | 浏览器端缓存 |
许可证原文与各资源放在一起:例如 assets/third_party/bootstrap/LICENSE、assets/third_party/katex/LICENSE;Swagger UI、Redoc 与 ECharts 还随包带了各自的 NOTICE 或打包声明文件。Lunr 是唯一的例外,代码在 assets/js/third_party/,许可证在 assets/third_party/lunr/LICENSE。
再分发主题时,这些许可与声明材料必须一并保留。更新某个运行时意味着在同一次变更里同时更新产物、许可证文件、来源与校验值。
字体与图标
三款字体(Inter、Chakra Petch、IBM Plex Mono)都采用 SIL Open Font License 1.1,字体文件提交在 static/webfonts/:Inter 十四个子集文件、品牌字体四个,加上 Font Awesome 的三个,共二十一个。Font Awesome Free 7.3.1 是复合许可:图标图形 CC BY 4.0、字体文件 SIL OFL 1.1、代码 MIT,原文在 assets/third_party/Font-Awesome/LICENSE.txt。
主题不向远程字体服务发请求:仓库里没有 Google Fonts 之类的外链,字体一律由站点自身 baseURL 下发。更换字体或改用系统字体栈见品牌外观。
设计参考
代码上游只有 Docsy 一个。下面这些项目是设计语言上的参考,既不是代码来源也不是运行时依赖,OINK 没有移植它们的代码:
| 项目 | 借鉴之处 |
|---|---|
| Fumadocs | 以内容为中心的呈现、信息层级、文件树与参数表一类的写作组件(主题 NOTICE 记录了这条致敬) |
| Nextra | 精炼的文档外壳、代码块的文件名与复制交互、按页布局开关 |
| Hextra | Hugo 原生的实现取向、文件树、徽章、标签页 |
| Mintlify | 结构化导航分层、同步的代码分组、API 参考的阅读体验 |
Hugo 是构建平台,Go 在 Hugo Module 安装方式下负责解析模块。两者都是前提条件,主题不重新分发它们的可执行文件。
引用这些名字用于说明传承、依赖或灵感来源,不表示相关项目为 OINK 背书;各项目与产品名称归其权利人所有。
复用这份文档
CC BY 4.0 允许任何目的的分享与演绎,条件是给出署名、提供许可证链接、说明是否做过修改,并且不得暗示 OINK、PGSTY 或上游项目为改编内容背书。一段合格的署名可以是:
本文改编自 PGSTY 贡献者编写的 OINK 文档,采用 CC BY 4.0 许可,并做了修改。
页面里单独署名的图片或引文,要保留它们各自的署名与许可;删掉页脚不会免除署名义务。
复用这个主题
Apache-2.0 允许按条款使用、修改与分发主题源码及编译产物,条件是保留许可证、版权与归属声明,保留 NOTICE 内容,并在分发修改后的源码时标明改过哪些文件。主题发行包应当包含 LICENSE、NOTICE、VENDOR.json,以及清单引用的全部第三方许可证文件。
Apache-2.0 不授予商标使用权,也不会把第三方资源变成 Apache 许可的作品。
相关
2 - 快速上手
新站点的推荐起点是
pgsty/oink-starter,而不是复制本站这个
文档与回归测试仓库。Starter 是公开的 GitHub 模板:它固定 OINK
v1.0.0,默认即可构建,只包含中性的项目示例与部署 workflow。
OINK 声明的兼容性下限是 Hugo Extended 0.160.1。当前 Starter 与它的 CI 固定使用 Hugo Extended 0.165.0 和 Go 1.27。下面这条路径应 使用 Starter 固定的工具链;只有刻意维护旧环境的既有站点才使用较低的兼容下限。
选择起点
| 当前情况 | 推荐路径 | 得到什么 |
|---|---|---|
| 新建文档站或项目站 | OINK Starter | 一套精简的三语 Docs、Blog、Book 站点与两条部署 workflow |
| 已有 Hugo 站点 | 从零安装 | 不替换内容,只补 OINK 模块与 Goldmark 前置配置 |
| 已有 Docsy 或旧版 OINK 站点 | 版本升级 | 保留内容,迁移受支持的语法,并审查站点覆盖 |
五分钟建立基线
-
安装工具
安装 Git、Go 1.27 或更新版本,以及 Hugo Extended 0.165.0 或更新版本。Hugo 输出必须包含
extended:macOS 可以执行
brew install git go hugo。Linux 与 Windows 请按官方 Hugo 安装指南和 Go 下载页安装,并确认选择 Hugo Extended。 -
创建或克隆站点
准备长期维护时,请打开 Starter 仓库并点击 Use this template,然后克隆 GitHub 为你创建的新仓库。只想在本机评估原始模板时执行:
-
打开基线
打开 http://localhost:1313/。默认 Starter 还在
/zh/发布中文,在/fr/发布法语。开始修改前,先确认 Docs、Blog、Book、本地搜索、语言切换与深浅色 模式都能工作。 -
完成一个可见修改
修改
hugo.yaml顶部的站名与规范 URL,再修改data/home/en.yaml中的一句话。 浏览器刷新后能同时看到两处变化,才算证明配置、内容与固定版本的主题已经正确连通。
由浅入深地定制
- 使用 OINK Starter — 先改身份,再依次处理语言、首页、 内容、导航、品牌、集成与部署。
- Starter 仓库导览 — 每个文件负责什么,哪些要替换, 哪些可以删除。
- 编写页面 — front matter、标题、链接、图片、草稿与页尾控件。
- 组件总览 — 内容树稳定后,再增加表达能力。
- 品牌外观 — Logo、强调色、字体、页宽与 CSS 扩展点。
- 发布上线 — 使用内置 GitHub Pages 或 Cloudflare Pages workflow,再验证真实公开路由。
这个顺序是有意的。先证明构建与内容树,再逐项增加定制,比同时修改语言、导航、 CSS、分析与托管更容易定位问题。
发布门禁
第一次推送前,执行与 Starter workflow 相同的严格生产构建:
命令以 Total in … 结束、没有警告或错误,而且 public/ 中存在各语言根与代表性的
Docs、Blog、Book 路由,才算通过。此时仍只证明本地构建:本地构建、提交、推送、
workflow 变绿与公开站点正确,是彼此独立的关卡。
下一步
继续阅读完整 Starter 教程。如果模板有你不需要的结构, 按仓库导览安全删减。只有在给既有站点接入 OINK,或者 明确想亲手组装每个文件时,才走从零建站路径。
2.1 - 使用 OINK Starter
pgsty/oink-starter 是新建 OINK
站点的正式起点。它刻意小于 oink.pgsty.com:不会把主题文档、分析账号、评论仓库、
浏览器回归套件或 PGSTY 品牌复制进你的项目。
当前模板固定 OINK v1.0.0、Go 1.27 与 Hugo Extended 0.165.0。 默认三语、仅英文、英中双语三个 profile 都已经在这个版本上完成 warning 即失败的 严格构建。
模板包含什么
| 表面 | 内置基线 | 第一个决定 |
|---|---|---|
| 语言 | 英语、简体中文、法语 | 保留三语,或选择内置单语 / 双语 profile |
| 内容 | Docs、Blog 与一本简短 Book 教程 | 重写示例;确认整个表面不需要时才整棵删除 |
| 首页 | 每种语言一份精简 data/home/<lang>.yaml |
替换项目承诺与入口 |
| 品牌 | 中性 Logo 与 favicon | 有正式项目图形之前先保留 |
| 集成 | 仓库、Giscus、分析、分享、反馈示例均被注释 | 只启用你准备长期运营的完整配置 |
| 部署 | GitHub Pages 与 Cloudflare Pages Direct Upload workflow | 选择一条生产路径并验证真实 URL |
Starter 自己的 /book/ 是一份从预览到部署的四章短教程。本页是维护者级版本:
说明修改顺序、各层边界,以及每层之后应执行的检查。
创建自己的仓库
推荐使用 GitHub 模板
打开 Starter 仓库,点击 Use this template → Create a new repository,再克隆 GitHub 在你的账号或组织下 创建的仓库:
这样站点从一开始就有自己的 Git 历史,原始 Starter 只是上游参考,不会成为一个 可能误推送的 remote。
克隆原始仓库进行评估
只做一次性本地评估时执行:
真实项目不要从删除这个 clone 的 .git 目录开始。GitHub 模板操作已经创建了清晰的
项目边界,并保留可审计的初始提交。
修改前先预览
依次打开:
/、/zh/、/fr/:三个首页;/docs/、/blog/、/book/:三种内容表面;- 任意一组译文,再操作语言切换器;
- 本地搜索、深浅色切换,以及一个窄屏视口。
同时记录实际解析的模块:
结果应当是 github.com/pgsty/oink@v1.0.0。这份未修改的预览,是后面
判断每次改动的基线。
分层定制
第一层:站点身份
修改 hugo.yaml 顶部标有 CHANGE ME 的两个值:
标题的 YAML 锚点会把站名带进所有已启用语言。接着修改版权人,并在新仓库已存在后 取消仓库链接的注释:
重新运行 hugo server,检查浏览器标题、页脚、编辑 / 历史链接与 canonical URL。
项目图形尚未定稿时先不要改 Logo;文字身份更容易先完成评审。
第二层:语言 profile
根配置默认启用英语、中文和法语。如果这不是目标语言组合,请在其它配置修改之前 选择内置 profile:
这两份是完整的最小配置,不是可以叠加的片段;复制会覆盖根文件里那些被注释的集成
示例。因此应在最开始做;hugo.yaml 已有项目修改时,只合并 languages 与
disableLanguages,不要整文件覆盖。
未启用语言仍保留声明,让 Hugo 能识别 .zh.md 与 .fr.md 是译文并安全忽略。
要永久移除一种语言,先确认所选 profile 能构建,再删除对应内容与首页数据。
第三层:首页
首页是数据,不是难以维护的整页模板覆盖:
先改一种语言。每个文件里的 sections 决定顺序,hero、cards、cta 提供内容。
保持结构,替换项目承诺、目标 URL 与示例卡片。第一种语言确认无误后,再把同一组事实
翻译到已启用语言。
需要其它组合时,使用首页与落地页中的完整注册表;不要复制 Starter 的首页 partial,因为这里本来就没有站点自有模板。
第四层:内容与导航
重写或删除 content/ 下的示例叶子页面。确定整个表面不属于你的项目之前,先保留
栏目根:
内容树就是侧栏。顶部导航写在各语言 _index 根页的 menus.main 里,因此给 Docs、
Blog 或 Book 改名时,修改发生在它所描述的内容旁边,而不是另一棵全局菜单树。译文
并排放置,对应标题使用相同的显式 ID:
新增自定义导航数据之前,先读组织内容;大多数站点使用生成树 已经足够。
第五层:品牌与阅读功能
正式图形准备好后,替换 assets/icons/logo.svg 与 static/favicon.svg。随后一次只启用
一组最小而有用的配置:
自定义本地字体时,用 params.ui.fonts 写字体族,或者在站点 CSS 中声明字体文件。
布局、侧栏、搜索与组件配置应查询配置总览,不要复制
oink.pgsty.com 那份大得多的站点配置。
第六层:外部集成
Starter 默认关闭或注释了仓库操作、Giscus、Google Analytics、反馈与分享。只有 必需事实全部明确时才启用:
- 仓库链接需要真实 owner、repository 与 branch;
- Giscus 需要仓库 / 分类名称和不可变 ID;
- Google Analytics 需要项目自己的 measurement ID;
- 反馈只有在分析存在时才记录结构化
gtag事件; - 助手链接会把当前 URL 发送给第三方,因此必须做显式策略选择。
不完整的可选块应继续保持注释。各集成的运营边界见启用评论、 分析与 SEO和仓库与页面信息。
构建与部署
严格本地构建
启用托管 workflow 前执行:
提交 hugo.yaml、go.mod 与 go.sum;不要提交生成的 public/、resources/、模块
缓存或本地模块替换。
GitHub Pages
Starter 已包含 .github/workflows/github-pages.yaml。在
Settings → Pages 中选择 GitHub Actions 作为 Source。推送到 main 后,
workflow 使用固定工具链构建,向 GitHub 查询正确的项目子路径,再通过 Pages 部署
API 发布 public/。
Cloudflare Pages
内置 .github/workflows/cloudflare-pages.yaml 使用 Direct Upload。创建 Pages
Direct Upload 项目,添加 CLOUDFLARE_ACCOUNT_ID 与 CLOUDFLARE_API_TOKEN,再手动
运行一次 workflow。设置仓库变量 CLOUDFLARE_PAGES_ENABLED=true 后才会自动部署;
规范地址不是默认 pages.dev 域名时,再设置 CLOUDFLARE_SITE_URL。
同一个项目只选 Direct Upload 或 Cloudflare Git integration 其中一种。完整托管对比
与 baseURL 规则见发布上线。
验证并删除示例
宣布站点完成前:
- 搜索
Project Name、example.org、OWNER、PROJECT等占位符,逐项确认剩余位置 是否有意保留。 - 在桌面与移动端打开每种已启用语言的根,以及代表性的 Docs、Blog、Book 页面。
- 确认语言切换落到对页,而不是首页。
- 验证搜索、深色模式、一个组件、Markdown 输出、打印、404、canonical URL 与仓库操作。
- 把部署 workflow 和公开 URL 与本地构建分开检查。
删除示例 Book 或 Blog 之前,要同时移除对应顶部菜单根,以及首页上指向它的卡片。每整棵 删除一个表面就严格重建一次,才能让失败归因到单一改动。
下一步
用 Starter 仓库导览查询文件职责,再继续阅读 编写页面与配置总览。已有站点不应 继承 Starter 内容模型时,改走从零建站路径。
2.2 - Starter 仓库导览
本页说明从 pgsty/oink-starter
创建的仓库,不再介绍大得多的 oink.pgsty.com 文档与回归测试仓库。主题源码不会
复制进任何一个站点:go.mod 以 Hugo Module 形式固定版本,Hugo 把解析结果存进
Go 模块缓存。
顶层地图
oink-starter/
- oink-starter/
- hugo.yaml身份、语言、输出、参数与模块导入
- go.mod站点模块与精确 OINK 版本
- go.sum模块校验和
- examples/
- hugo.single.yaml仅英文的完整 profile
- hugo.bilingual.yaml英文 + 中文的完整 profile
- data/
- home/
- en.yaml每种语言一份精简落地页
- zh.yaml
- fr.yaml
- home/
- content/
- _index.md各语言首页根
- _index.zh.md
- _index.fr.md
- docs/简介、快速上手、教程、参考
- blog/文章、设计记录、发布说明
- book/介绍 Starter 的连续教程
- assets/
- icons/logo.svg经 Hugo 处理的项目 Logo
- static/
- favicon.svg原样复制到站点根
- i18n/
- fr.yamlStarter 自有法语界面覆盖
- .github/workflows/
- github-pages.yaml严格构建与 GitHub Pages 部署
- cloudflare-pages.yaml严格构建与 Cloudflare Direct Upload
- README.md面向仓库维护者的操作摘要
- LICENSE模板源码许可证
生成的 public/、resources/、.hugo_build.lock 与模块缓存是被忽略的构建状态,
不是源码。
最先修改什么
| 路径 | 职责 | 第一次操作 |
|---|---|---|
hugo.yaml |
身份、规范 URL、语言、输出、主题功能、可选集成 | 修改两个标记值;其它修改前先选择语言 profile |
data/home/ |
首页承诺、卡片与行动入口 | 一种语言确认后,再重写所有已启用语言 |
content/ |
全部读者可见内容 | 替换示例叶子;确认整个表面不要时才删除栏目根 |
assets/icons/logo.svg |
经处理的 Logo | 有正式图形后再替换 |
static/favicon.svg |
浏览器图标 | 与 Logo 一起评审后替换 |
hugo.yaml 中的 params.github_* |
编辑、历史、新建页面与 issue 链接 | 目标仓库已存在后才取消注释 |
哪些必须保留
go.mod与go.sum:两者共同固定并校验 OINK v1.0.0,都要提交。hugo.yaml中三项 Goldmark 设置:原生 Steps、Cards、Fields、图片属性与 Book 目标都依赖它们。outputs:删除markdown、LLMS或print,会有意删除对应的 Markdown、 Agent 索引或打印表面。- workflow 中的
fetch-depth: 0:保留enableGitInfo时,最后修改与贡献者事实需要 完整 Git 历史。 - CI 中的
GOWORK: off与HUGO_MODULE_WORKSPACE: off:开发者本地 workspace 不得 替换 CI 正在验证的公开版本。
可选表面
Docs、Blog 与 Book 是彼此独立的顶层表面。安全删除其中一个的顺序是:
- 删除对应的
content/<surface>/内容树; - 删除首页指向它的卡片或链接;
- 确认其它页面不再链接它;
- 严格构建,并检查剩余顶部导航。
不要只删除某种语言的栏目根:那会形成难以区分「有意不对称」与「漏译」的语言专属导航 和回退行为。要么在所有已启用语言中删除整个表面,要么明确记录这种不对称。
完成语言选择后,examples/ 下两个配置 profile 可以删除,也可以作为参考保留;真正
生效的站点配置只有根目录 hugo.yaml。
内容与导航
Docs 与 Book 下的目录结构和 weight 共同形成侧栏与翻页顺序。顶部导航来自栏目根的
menus.main。译文根重复相同的 identifier、parent 与 weight,只翻译可见标签。
Starter 刻意演示 Documentation System 内容模型:
- 简介回答是什么、为什么;
- 快速上手帮助新用户得到结果;
- 教程带领读者完成端到端任务;
- 参考记录精确的受支持行为。
可以按项目需要改名或重组,但应保留不同学习路径之间的分工,不要把所有答案混进一棵树。
语言模型
英文源码以 .md 结尾,中文和法语对页分别以 .zh.md、.fr.md 结尾。首页数据按
data/home/ 下的语言键分文件。根 profile 声明语言、locale、顺序与站点描述。
单语与双语 profile 仍声明被禁用的语言,这是有意设计:Hugo 会把未使用后缀识别为 译文,而不会把多个文件渲染到同一个英文 URL。只在项目配置开始前复制 profile;之后 应手工合并。
OINK 在哪里
两个文件建立模块边界:
hugo mod graph 显示实际解析版本。生产使用 go.mod 中的精确标签;本地
HUGO_MODULE_REPLACEMENTS 只是开发覆盖,绝不能提交,也不能当成发布证明。
部署文件
GitHub Pages workflow 在推送 main 后自动运行;仓库设置必须选择 GitHub Actions
作为 Pages Source。Cloudflare workflow 默认手动运行,只有仓库变量
CLOUDFLARE_PAGES_ENABLED=true 存在时才自动执行;所需账号 ID 与 API token 始终
保存在仓库 secrets 中。
只保留实际运营的部署路径。Cloudflare Direct Upload 与 Cloudflare Git integration 是同一个项目的两种所有权模型,不是应当同时运行的两道关卡。
安全的定制顺序
- 证明未修改的预览可用。
- 修改身份并选择语言。
- 替换一种首页,再补齐译文。
- 替换内容并验证导航。
- 品牌与阅读功能一次只改一组。
- 启用完整的外部集成。
- 执行严格生产构建。
- 部署,再独立验证生产环境。
仓库已经属于自己后,每层之间做一次提交。小边界能让后续回归与回滚明确归因到一个决定。
验证
模块图应显示固定发布,构建没有警告或错误,Git 状态只包含源码修改而没有 public/ 或
缓存。之后打开所有已启用语言的根,以及代表性的 Docs、Blog、Book 路由,再进入部署。
相关
- 使用 OINK Starter — 完整分层流程
- 从零建站 — 不采用这套内容模型,只接入 OINK
- 组织内容 — 侧栏、翻页与菜单权威
- 配置总览 — 当前全部站点参数
- 发布上线 — 托管商配置与生产检查
2.3 - 从零建站与其它安装方式
这是推荐路径 OINK Starter 的手工替代方案。本页从空目录
搭建一个最小 OINK 站点:一份精简 hugo.yml 加一条 hugo mod get,得到一个可预览
的单语站点。代价是首页、示例内容、部署 workflow 与每种组件用法都要自己组装。
已有 Hugo 站点时不需要脚手架:装上主题模块,再补三项 goldmark 前置配置(见写 hugo.yml),正文不用重写。已有 Docsy 站点见版本升级。
后半部分是四种安装方式的取舍:Hugo Module、Git submodule、离线归档、固定版本克隆。 当前 v1.0.0 发布路径应使用 Go 1.27 与 Hugo Extended 0.165.0;只有刻意维护旧环境的 既有站点才使用主题声明的较低兼容下限。
从空目录到第一页
-
建骨架并获取主题
hugo mod init后面跟的是你自己站点的模块路径,通常就是仓库地址。hugo mod get会写出go.mod与go.sum,两个都要提交。最新版本号在 GitHub Releases;本页出现的
v1.0.0是本站当前固定的版本。生产站点固定到发布标签,不要跟随main:@latest是一次性解析动作,不是版本策略。 -
写
hugo.yml把
hugo new site生成的hugo.yaml改名为hugo.yml(两个后缀 Hugo 都接受,本文统一用后者),内容替换为下面这份,可直接构建:hugo.yml五段分别管什么:
段 管什么 少了会怎样 顶层 + languages站名、域名、语言与顶栏菜单 baseURL不对,线上所有绝对链接指错markup.goldmark三项组件前置 属性行变成正文里的一行 {.steps}params搜索、仓库链接、外壳开关 交互功能默认关闭,主题不替站点决定 outputs每页的 .md、llms.txt、打印页页面菜单里没有「复制 Markdown」,也没有打印视图 module引用主题、声明 Hugo 下限 构建时找不到主题 -
写第一页
content/下的每个一级目录是一个分区,目录结构就是侧栏结构。文档分区至少要有一个_index.md:content/docs/_index.mdcontent/docs/install.md标题写显式
{#id}:后续加译文时两种语言的锚点才能对应。页面写法见编写页面。 -
预览
打开 http://localhost:1313/,侧栏里有 Docs → Install。修改文件是毫秒级热重载。
其它安装方式
上面用的是 Hugo Module。另外三种方式面向特定约束:网络隔离、平台要求构建输入包含完整主题树、组织内部需要评审主题副本。除 hugo mod vendor 之外,它们都不建立 Go 模块,站点用 theme: oink 而不是 module.imports 引用主题;共同的代价是版本解析与完整性校验由你自己负责。
Hugo Module(推荐)
唯一能让 Hugo 自己解析版本、校验 checksum、并在 go.sum 里留下审计记录的方式。hugo mod graph 看实际解析结果,hugo mod get -u 升级。需要本机有 Go。
Git submodule
在站点仓库里记录准确的主题 commit:
CI 必须在运行 Hugo 之前初始化 submodule,否则 themes/oink 是空目录:
离线归档
网络隔离环境使用。两条路径,都先在联网机器上准备,再整体搬入。
用 hugo mod vendor:把已解析的主题源码固化进站点目录,之后构建既不联网也不需要 Go。
_vendor/ 存在时 Hugo 优先使用它(hugo mod graph 输出 +vendor),hugo.yml 里的 module.imports 保持不变。这一步需要 Go,之后的构建不需要。升级主题要回到联网环境重新执行 hugo mod get 与 hugo mod vendor。
_vendor/ 只收主题挂载出来的目录(assets data i18n layouts static)以及 hugo.yaml 与 theme.toml,不含 LICENSE、NOTICE 与 VENDOR.json。要对外分发这份归档,把这三个文件从主题仓库一并取来。
用 tag 源码归档:不建 Go 模块,直接把某个版本的主题解压到 themes/oink/。
主题仓库的根目录就是模块根目录,解压出来直接是 layouts/、assets/、i18n/、static/ 这一层,不需要再进入下一级。重新分发时必须保留 LICENSE、NOTICE 与 VENDOR.json。最后一个记录了每个第三方运行时的版本、来源、许可证路径与 SHA-256,是离线审计的依据。
跨机器传输时,在联网侧从不可变标签生成归档与校验值:
把归档与 .sha256 一起传入隔离环境,先校验再解压:
这样得到的归档是自建产物,不是项目发行物。某个标签的发行页面是否附带归档与校验文件按发布而定,使用公开附件时独立验证其校验值。
断网构建之前确认归档内容完整,这十一项都要在:
themes/oink/
- oink/
- go.mod模块路径声明,Hugo Module 方式解析用
- hugo.yaml主题默认参数与 Hugo 版本下限
- theme.toml主题元数据,theme: oink 方式需要
- LICENSEApache-2.0
- NOTICE上游署名,再分发时必须保留
- VENDOR.json第三方运行时清单:版本、来源、许可证路径、SHA-256
- assets/SCSS、JS 与随主题分发的第三方运行时
- layouts/模板、partial、shortcode、render hook
- static/字体文件,原样发布
- i18n/32 份界面语言文件
- data/页尾出处行用的 SPDX 许可证表
固定版本克隆
托管平台要求构建输入包含完整主题树时用:
与 submodule 的区别是主题文件直接进入你的仓库历史,没有 .gitmodules 这层间接。记录最终解析出的 commit 与恢复流程。
四种方式对比
| 方式 | 需要 Go | 版本可审计 | 主题源码进你的仓库 | 适用 |
|---|---|---|---|---|
| Hugo Module | 是 | go.sum 自动校验 |
否 | 默认推荐 |
| Git submodule | 否 | 仓库记录 commit | 以引用形式 | 需要主题源码在库内 |
| 离线归档 | 否 | 手工核对 checksum | 是 | 网络隔离 |
| 固定版本克隆 | 否 | 需自行记录 | 是 | 平台要求完整树 |
Bootstrap、Font Awesome、字体、搜索与图表运行时全部随主题分发。站点不需要 node_modules、PostCSS、RTLCSS,也不需要 CDN。为 Docsy 站点安装 npm 依赖的教程属于上游 Docsy 的流程,不适用于 OINK。
用本地主题 checkout 开发
同时修改主题与站点时才需要这一节。把两个仓库克隆为同级目录:
用环境变量 HUGO_MODULE_REPLACEMENTS 把模块临时替换为本地 checkout,go.mod 不变:
文档站仓库的 Makefile 就是这几条命令的别名,make dev 与 make check 要求主题 checkout 在同级目录 ../oink:
Go workspace(go work init + HUGO_MODULE_WORKSPACE=go.work)是等价的另一种做法。两种做法都只作用于本机:CI 与生产构建用的是 go.mod 里的版本,go.work 不要提交。
验证
构建以 Total in … 结束、没有 WARN / ERROR 即通过。再确认:
/docs/打得开,侧栏里有你写的页面- 顶栏有搜索框,搜得到刚写的标题
- 深浅色切换按钮在,切换后代码块配色跟着变(说明
markup.highlight.noClasses: false生效) git status里有go.mod与go.sum,没有public/、resources/
相关
- 快速上手 — 在 Starter、既有 Hugo 站点与迁移之间选择
- OINK Starter — 推荐的新站点路径
- Starter 仓库导览 — 模板各目录的职责
- 配置总览 —
hugo.yml每个键的含义与默认值 - 编写页面 — 第一页之后怎么继续写
- 版本升级 — 升级主题模块、从 Docsy 迁移
3 - 创作内容
本栏覆盖 OINK 支持的几种内容类型:文档页、博客文章、书籍、发布下载页、OpenAPI 参考。它们共用同一套 Markdown 与 front matter,各自另有约定。
一页文档的构成
一页文档是一个 Markdown 文件。文件开头两行 --- 之间是 front matter,即页面元数据:标题、侧栏短名、描述、排序。其余部分是正文,内容为普通 Markdown 加 OINK 的原生组件。下面是一个完整页面:
存为 content/docs/install.zh.md,运行 hugo server 后页面出现在 /zh/docs/install/,侧栏出现「安装」一行。
内容类型与对应页面
3.1 - 编写页面
本页覆盖一页文档的完整写法:文件位置、front matter、标题锚点、链接、图片、草稿与页尾。前提是站点已能本地构建,尚未搭起时先看十分钟上手。
新建一页
页面是 content/ 下的 Markdown 文件,URL 由它在 content/ 里的位置决定:content/docs/install.md 发布为 /docs/install/。中文译文是同目录下的 .zh.md 同名文件,与英文页共享同一条逻辑路径。
没有附带资源的页面写成单个文件。页面带图片、cast、示例配置这类资源时改成一个目录,页面本身命名为 index.md,资源与它同放,这是 Hugo 的页面包(page bundle):
content/ 里的两种页面形态
- content/
- docs/
- _index.md栏目首页,英文
- _index.zh.md栏目首页,中文
- install.md单文件页面 → /docs/install/
- install.zh.md它的中文译文
- anatomy/页面包 → /docs/anatomy/
- index.md
- index.zh.md
- shell.webp页面资源,两种语言共用
- docs/
hugo new content docs/install.md 用 archetype 生成一个带 front matter 的空文件,见 Hugo 文档;手写文件同样可行。
中文页没有英文对等页时,Hugo 不会把无语言后缀的资源分给它。这种情况下资源文件名要带 .zh.(shell.zh.webp),正文里仍然写 shell.webp。
必要的 front matter
文件开头两行 --- 之间是 YAML front matter。四个键每页都应写上:
description 用一句话说清这页让读者做成什么。它出现在栏目首页的卡片、搜索结果与社交卡片中。weight 决定侧栏顺序,weight 相同时才退回字母序。
其余的键可选:图标、草稿、搜索权重、评论开关、页面外壳等,全表见页面参数。
标题层级与稳定锚点
正文用 ## 开始分节,# 留给 title。主题已渲染页面大标题,正文里再写一个 # 会出现两个一级标题。右栏的页面目录从 ## 开始收,收到第几级由 Hugo 的 markup.tableOfContents 决定,本站是 ####。
每个 ## 与 ### 都要手写英文锚点 {#id}:
理由有两条:
- 中英对齐。Hugo 从标题文字生成 ID,中文标题生成中文 ID:
/docs/install/#prerequisites与/zh/docs/install/#前提条件指向同一个语义位置,却是两个锚点,翻译审计无法比对。译文标题写上英文页的 ID,两边即同一个片段。 - 链接稳定。标题文字会随措辞调整而改变,公开链接不应随之失效。显式 ID 一旦发布即视为公开路由;需要改名时保留旧 ID 的空锚点:
ID 用短横线小写英文,全页唯一。本站的翻译审计脚本会比对英文页与中文页渲染出的标题 ID,不一致就报错。
链接写法
三种写法,用途不同:
| 写法 | 例子 | 什么时候用 |
|---|---|---|
| 站内绝对路径 | [配置总览](/zh/docs/customize/config/) |
默认写法。指向已发布的路由,便于审计与全站替换,不受源码文件移动影响 |
| 相对路径 | [另一页](../organize/)、 |
同一页面包内的资源,或有意跟着源码目录走的相邻页面 |
ref / relref shortcode |
[配置总览]({{</* ref "/docs/configure/overview" */>}}) |
需要构建期校验目标存在时;目标缺失时构建失败,不会留下死链 |
三种写法都带尾部斜杠,指向目录形式的路由(/zh/docs/write/pages/),与 Hugo 的默认永久链接一致。
主题没有链接渲染钩子,链接原样交给 Goldmark:外链不会自动加 target="_blank",需要新标签页时写成 HTML,或在站点自己的 layouts/_markup/render-link.html 里处理。
普通 Markdown 链接不做存在性检查。因此:
- 站内链接优先写绝对路径,改结构后用
grep全站替换; - 移动页面时给旧路径加
aliases,同时把站内链接改到新路由,不要让 alias 长期承担导航; - 拿不准的目标用
ref,让构建替你检查。
双语页面链接到逻辑页面(/zh/docs/write/pages/),不要链接 .zh.md 文件名;片段 ID 保持语言中立。
图片位置
页面自己的截图放页面包,多页共用的图放 assets/images/,不需要处理的大文件放 static/。三处在源码里都写成 ,属性行控制图注、尺寸、缩放与编号,见图片。
草稿与发布
draft: true 的页面不会进入构建产物:
预览时用 hugo server -D 显示草稿(-D 即 --buildDrafts)。date 写在未来的页面同样被排除,用 -F 显示。生产构建不加这两个开关,hugo 默认只发布已定稿的内容。
OINK 的 Markdown 扩展一览
正文是标准 Markdown(Goldmark),加上下面这些原生形态。它们都是普通 Markdown 语法加一行属性,在 GitHub 上按源码阅读同样可读:
| 组件 | 最短语法 | 页面 |
|---|---|---|
| 提示块 | 块引用首行写 > [!NOTE] |
提示块 |
| 标签页 | 相邻的两个围栏各加 {tab="Homebrew"} |
标签页 |
| 步骤 | 有序列表后面跟一行 {.steps} |
步骤 |
| 卡片 | 链接列表后面跟一行 {.cards} |
卡片 |
| 参数表 | 表格后面跟一行 {.fields meta="type default"} |
参数表 |
| 表格增强 | 表格后面跟一行 {.matrix}、{caption="…"} |
表格 |
| 代码块 | 围栏信息行写 {title="hugo.yml" copy=false} |
代码块 |
| 图片 | 独立成段的图片后面跟一行 {caption="…" width="600"} |
图片 |
| 文件树 | filetree 围栏,每行一个 - 名字/ # 注释 |
文件树 |
| 公式 | math 围栏,或用 $$ 包住的块级公式 |
公式 |
| 图表 | mermaid 围栏(还有 plantuml、markmap、echarts) |
Mermaid |
剩下的少数组件(徽章、按键、引用文件、终端录像、Book 的图表式例)用 shortcode,语法与参数见组件总览。
组合例子:步骤里放代码围栏与提示块。
- 安装 Hugo Extended,最低 0.160.1:
- 克隆 OINK Starter 并预览:
提示
加
-D连草稿一起预览。
页尾的自动内容
页面末尾的四块内容由主题按固定顺序生成,不必在正文里写:
| 位置 | 是什么 | 默认 | 怎么改 |
|---|---|---|---|
| 1 | 反馈:「这页有帮助吗」两个按钮 | 关 | 仓库与页面信息 |
| 2 | 最后修改:时间加最近一次提交的标题,链到 GitHub | 有 Git 信息时开 | 仓库与页面信息 |
| 3 | 翻页器:上一页 / 下一页,顺序与侧栏树一致 | docs / book / blog 开 | 导航与菜单 |
| 4 | 评论:giscus | 配置完整且开启时 | 启用评论 |
标题旁边的操作菜单(复制 Markdown、编辑本页、查看历史、提 issue、打印)也是自动的,同样在仓库与页面信息里配置。
单页关闭其中某一块用 front matter:feedback: false、annotation: false、pager: false、comments: false。键的含义见页面参数。
验证
写完一页,运行一次严格构建:
- 输出必须以
Total in …结束,没有 ERROR、没有 WARN。属性行写了不允许的键、组件参数非法、ref目标不存在,都在这一步失败并指出文件与行号;主题不做静默降级。 --printPathWarnings报出两个页面指向同一输出路径的情况,多语言站或改过permalinks时较常出现。
在浏览器里确认三项:
- 侧栏里出现了这一页,位置符合
weight; - 右栏目录列出了你写的
##,点击后 URL 里的锚点是英文; - 中英两个版本的同名标题锚点一致(本站有
node scripts/check-doc-translations.mjs --public public做这项审计)。
相关
3.2 - 组织内容
_index.md 与 weight、栏目首页样式、图标与折叠、隐藏页面、把文档放在任意路径。OINK 不需要单独配置导航:content/ 下的目录结构就是侧栏树。本页覆盖目录与文件的摆放、栏目首页、排序、图标、折叠、隐藏,以及多根侧栏。
目录就是侧栏
一个目录是一个栏目(Hugo 称 section),目录里的 Markdown 文件是它的页面,嵌套目录是它的子栏目。侧栏按这棵树逐层渲染,顺序由 weight 决定,标签取 linkTitle,缺省时取 title。左侧这棵树的源码如下:
content/docs/ 的前两层
- content/
- docs/
- _index.zh.md栏目根:type: docs + cascade
- about/简介
- _index.zh.md
- features.zh.md
- start/快速上手
- _index.zh.md
- write/创作内容(本栏目)
- _index.zh.mdweight: 30
- pages.zh.mdweight: 10
- organize.zh.mdweight: 20
- frontmatter.zh.mdweight: 30
- components/组件
- _index.zh.md
- docs/
每个目录都要有 _index.md
栏目首页是目录里的 _index.md(中文为 _index.zh.md)。缺少它时 Hugo 仍会生成栏目,但没有标题、描述、图标与 weight:侧栏那一行显示目录名,排序不受控制。
栏目 _index.md 另有一项专属能力:用 cascade 把共享设置一次下推给整棵子树,不必每页重复。
排序:weight 用 10 的倍数
同一栏目里的页面按 weight 升序排列,weight 相同时才退回日期与 linkTitle 字母序。一律用 10 的倍数(10、20、30),此后往中间插页不必改动其它页。栏目自身的 weight 决定它在父级里的位置。
没写 weight 的页面视为 0,Hugo 把它们排在所有写了 weight 的页面之后,彼此按日期与标题排列。这个顺序会随内容改动漂移,因此每页都写上 weight。
单文件还是页面包
没有自身资源的页面用单文件 slug.md;带图片、cast、示例文件的页面改成目录加 index.md,资源与它同放。两种形态在侧栏里没有区别,URL 也相同。详见编写页面。
栏目首页显示子页列表还是卡片
_index.md 的正文之后,主题自动接上子页索引,两种样式:
list 是主题默认,每个子页一行标题加描述;cards 是链接卡片网格,读取子页的 icon、linkTitle 与 description。本站用 cards,本栏目首页即是例子。单个栏目需要另一种样式时在它的 front matter 里覆盖:
两个页面级开关不受样式影响:simple_list: true 渲染紧凑的项目符号列表,no_list: true 不生成索引,用于正文自行手写导航的场合。
卡片样式下 description 即卡片正文。描述控制在一句话、单行可显示。
侧栏图标
在页面或栏目的 front matter 里写一对 Font Awesome class:
图标密度是站点级策略,用于避免叶子页全部带图标:
| 取值 | 效果 |
|---|---|
all |
每个写了 icon 的条目都显示(未设置时的兼容默认值) |
groups |
只有根节点和有子页的节点显示图标,普通叶子页不显示 |
none |
侧栏不显示任何条目图标 |
新站点建议显式写 groups:保留分组的语义标识,去掉叶子层的图标。本站使用这个设置,左侧只有六个栏目带图标。
展开与折叠
有子页的栏目在侧栏里带一个折叠箭头,读者的展开状态保存在本地。默认行为:当前页所在的那条路径展开,其余收起;博客类栏目默认展开。
站点级的折叠、紧凑模式、初始展开层数、宽度与截断在布局与页面类型里配;键的完整定义见配置总览。
从侧栏里藏起来
| front matter | 效果 |
|---|---|
toc_hide: true |
页面不出现在侧栏树里(页面本身照常发布,链接照常可用) |
hide_summary: true |
页面不出现在栏目首页的子页索引里 |
sidebar_divider: true |
这一项不再是链接,而是侧栏里的一条分组标题 |
manual_link: https://… |
侧栏这一行指向别处;配 manual_link_title、manual_link_target: _blank 用 |
toc_hide 与 hide_summary 控制两个不同的入口,两处都不该出现时才同时设置。
外壳由 type 决定,不是路径
文档外壳(侧栏、目录、面包屑、翻页器)不取决于目录名,只取决于页面的 type 是否在 params.ui.shell_types 里:
文档因此可以放在任意路径,用 cascade 指定 type 即可。例如把一套手册放在 content/handbook/,栏目根的写法如下:
文档目录不叫 docs 时,type: docs 之外还要写 sidebar_root_for: self。否则侧栏会按 params.ui.docs_section(默认 docs)去找根,读者在 /handbook/ 下却看到 /docs/ 的树。
多根侧栏
侧栏树默认以读者所在的顶层栏目为根,树上方一行标出当前的根。规模较大的子树可以自己成为一个根,例如带版本的 API 参考或一本独立的手册:
| 取值 | 语义 |
|---|---|
self |
这个栏目的首页及其全部后代都以它为侧栏根 |
children |
首页仍留在父级树里,只有后代以它为根 |
根节点上方的切换器是全站的:它列出所有顶层栏目,加上站内所有 sidebar_root_for: self 的栏目。只有一个入口时它退化成一个普通链接,两个及以上才是下拉菜单。顶层栏目不出现在切换器里时,在它的 _index.md 写 sidebar_root_menu: false。
切换器下方,栏目首页仍是树里的第一个链接:切换器选择一棵树,根链接指向一篇文档。sidebar_root_link_self: false 让根那一行改为指向父级栏目。
验证
必须 Total in …,没有 ERROR / WARN。--printPathWarnings 报出两个页面指向同一输出路径的情况,改目录结构时较常出现。
在浏览器里逐项确认:
- 侧栏里的顺序与写下的
weight一致,新栏目出现在预期位置; - 栏目首页的子页索引齐全(缺项来自
hide_summary或缺少_index.zh.md); - 面包屑与翻页器的顺序与侧栏一致,翻页器读的是同一棵树;
- 换语言之后树的形状相同(每个
_index.md都要有.zh.md对等文件)。
侧栏条目超过 params.ui.sidebar_menu_truncate 时构建给出警告,并指出应调到多少。这个警告不可忽略:被截断的条目不会出现在侧栏里。
相关
3.3 - 页面参数
本页是页面级参数的全表,包含 OINK 主题会读取的键,以及 1.x 明确保留的兼容
no-op。主题仅为提示「已重命名或已移除」而读取的旧键不在此列——它们在
迁移里,也不会出现在生成的编辑器 Schema 中。Hugo
自身的 front matter 字段(slug、url、build、sitemap、expiryDate 等)
照常可用,语义见 Hugo 文档。
站点级参数(hugo.yml 里的 params.*)见配置总览。
表格说明
优先级从高到低:
- 页面自己的 front matter;
- 最近一层
cascade(多层 cascade 都设了同一个键时,离页面最近的那一层生效); hugo.yml里的站点参数。
「默认」列标「站点值」的键,未写时回落到同名的站点参数。
页面键一律写在 front matter 顶层,键名是站点键去掉 ui. 前缀:站点的 params.ui.section_index 对应页面的 section_index。front matter 里不写 ui: 段,键一律在顶层。写在 ui: 段里的键不会被读取,也不会有任何提示——某个设置看着没生效时,先对照本页核一遍键名。
放进 cascade 时键名不变,多包一层:
非法值不会中断构建。主题会发一条警告,指出键名、收到的值以及实际用了哪个回退值,然后按表里的默认值把这一页渲染出来——一个笔误只降级一个设置,而不是让 hugo server 下每个 URL 都返回 HTTP 500。它也不会因此混进线上:所有发布关卡都带 --panicOnWarning 构建,那条警告在真正要紧的地方仍然是硬失败。
没有任何 front matter 键会中断构建;主题的模板从不报错。当继续构建会发布出错误内容而不只是朴素内容时——比如残缺的上游署名,半条声明读起来和完整的一模一样——警告之后是整块略去,而不是回退。这里唯一会中断构建的属于 Hugo 而不是主题:解析不到目标的引用。
基本
title, ,- 页面大标题、浏览器标题、搜索结果标题。每页必写
linkTitle, ,- 侧栏、面包屑、翻页器、卡片里的短名
description, ,- 一句话摘要:栏目卡片、搜索摘要、
meta description;博客页里渲染成正文上方的导语 weight, ,- 同级排序,用 10 的倍数;
0(不写)排在所有写了 weight 的页面之后,见组织内容 draft, ,- 草稿不进构建产物,
hugo server -D可预览,见编写页面 date, ,- 博客日期、发布页排序依据;未来日期默认不构建
lastmod, ,- 页尾「最后修改」;站点启用
enableGitInfo时不必手写 aliases, ,- 旧路径重定向到本页;用于页面迁移,不用于日常导航
type, ,- 决定模板与外壳:
docsbookblogswagger,见组织内容 layout, ,- 为单个页面指定布局:
landing、releases cascade, ,- 把下面这些键下推给整棵子树
侧栏与导航
指南在组织内容。
icon, ,- 侧栏、栏目卡片与搜索结果的图标,例如
fa-solid fa-rocket toc_hide, ,- 不出现在侧栏树里,也不进翻页序列
hide_summary, ,- 不出现在栏目首页的子页索引里
sidebar_divider, ,- 这一行渲染成侧栏分组标题:不是链接,也不进翻页序列
sidebar_expanded, ,- 这个栏目在侧栏里默认展开
sidebar_root_for, ,- 让这个栏目成为侧栏树的根;
self连同栏目首页,children只管后代。其它取值告警并忽略 sidebar_root_link_self, ,- 根那一行链接自身;
false改为链接父栏目。非布尔值告警并使用true sidebar_root_menu, ,- 顶层栏目是否出现在根切换器里
toc_root, ,- 侧栏根是站点首页时,把这个顶层栏目整个排除在树与翻页序列之外
manual_link, ,- 侧栏与栏目索引里这一行指向别处
manual_link_relref, ,- 同上,但用
relref解析;目标不存在时构建失败 manual_link_title, ,- 手动链接的悬停标题
manual_link_target, ,- 例如
_blank,主题自动补noopener no_list, ,- 栏目首页不生成子页索引
simple_list, ,- 子页索引渲染成紧凑的项目符号列表
section_index, ,- 子页索引的样式。非法值告警并回退
section_index_columns, ,- 卡片样式的列数
notoc, ,- 不显示右栏页面目录
pager, ,false关闭本页的上一页 / 下一页。非布尔告警并忽略该覆盖navbar_enabled, ,- 这一页是否渲染顶栏
navbar_autohide, ,- 顶栏在指针设备上自动隐藏
breadcrumb, ,- 本页是否渲染面包屑;Docs/Book 默认开启,Blog 默认关闭
theme_color, ,#rgb/#rrggbb十六进制色,为本页的强调底着色。写在分区根的cascade里就给整个分区一个身份 —— 见品牌外观theme_color_dark, ,- 强调色的暗色一半。若上层 cascade 同时设了这个键,只覆盖
theme_color的页面会继承那个暗色,所以要两个一起写。theme_color: false可让页面整体退出继承的栏目色 page_context_menu, ,- 标题行的页面操作菜单(复制 Markdown、编辑本页、打印……)
page_context_menu.assistant_links, ,- ChatGPT / Claude 交接项,写成
page_context_menu: { assistant_links: false }。页面只能收窄站点策略,不能单独开启
页面外壳
站点级的默认值与效果说明在布局与页面类型。
page_width, ,- 正文栏宽度。非法值告警并回退
reading_width, ,- Book 页的阅读行宽,只对
type: book生效 footer_style, ,- 页脚形态。非法值告警并回退
body_class, ,- 追加到
<body>上的 class,供站点自己的 CSS 使用 reading_time, ,- 本页是否显示阅读时长;写
false关掉 sidebar_enabled, ,- 这一页是否显示左侧栏;写
false关掉 scroll_spy, ,- 1.x 静默兼容 no-op;普通外壳运行时始终提供当前标题跟踪
keyboard_nav, ,- 单键键盘导航,见键盘导航。非布尔告警并回退
lastmod_commit, ,- 「最后修改」后面怎么显示提交。非法值告警并回退
sidebar_expand_levels、sidebar_menu_compact、sidebar_menu_foldable、sidebar_item_overflow, ,- 侧栏行为也可以逐页覆盖;取值见配置总览
sidebar_width_min、sidebar_width_max, ,- 本页桌面侧栏拖拽宽度的上下限;下限大于上限时告警并恢复站点值
code_copy, ,- 本页代码块复制控件的默认值;围栏显式
copy=仍然优先 toc_style, ,- 固定右栏面板,或从内容流开始的较宽右栏
toc_taxonomies, ,- 分类词云是否与页面目录共同进入右栏
taxonomy_icons, ,- 为本页或分区 cascade 覆盖各分类法图标
搜索
指南在全文检索。
search_keywords, ,- 附加检索词,包含中英文与同义词
search_boost, ,- 排序乘数,最终得分为文本匹配分乘以该值。非数字、非有限、零或负值告警并回退
1.0 search_exclude, ,- 不进本地索引
输出形态
指南在 Agent 支持(.md 与 llms.txt)与打印支持。
outputs, ,- 这一页生成哪些输出格式;写
[HTML]时不再生成.md no_print, ,- 不进入整章 / 整书的聚合打印输出
页尾:评论、反馈与出处
顺序固定为反馈 → 出处 → 翻页器 → 评论,见编写页面。
上游出处
页面改写自别处的材料时,用 upstream_link 声明来源,页尾出处行会给出作品、版权人、许可证与完整声明的链接。这一族键的解析顺序是站点参数 → data/upstreams 中由 upstream_source 指名的条目 → 本页 front matter,最具体的声明胜出。
upstream_link 只从 front matter 读取(cascade 有效,站点参数无效)——站点级的值会让每一页都声称同一个来源。没有 upstream_link 却写了任何一个同族键,告警并略去署名。
upstream_link, ,- 本页据以改写的材料地址。写空串退出 cascade 继承来的值
upstream_name, ,- 上游作品名,按上游自己的写法。设了
upstream_link即必填 upstream_copyright, ,- 版权声明,保留上游原文。必填
upstream_license, ,- 必须能在
data/licenses中查到,否则告警并略去署名。必填 upstream_notice, ,- 承载完整声明(许可证全文、免责声明、上游 NOTICE、快照版本)的页面。必填
upstream_ref, ,- 快照对应的 tag 或 commit,显示在作品名后的括号里
upstream_source, ,data/upstreams中的条目名,用于集中声明多页共用的上游事实;条目不存在时告警并略去署名upstream_modified, ,- 把署名动词改成「改编自」,站点配了仓库信息时在同一句里带上「查看历史」链接——是一句话,不是多加一行。非布尔值告警并按未修改处理
四个必填键(upstream_name、upstream_copyright、upstream_license、upstream_notice)缺一即告警并略去整条署名:残缺的署名比明显的缺失更糟。主题自带一份 SPDX 表 data/licenses.yaml,站点用同名文件补充或覆盖条目。
图片缩放
image_zoom, ,- 本页的图片是否可点击放大,见图片。非布尔告警并回退
博客与文章
指南在博客与文章。
author, ,- 文章署名,支持行内 Markdown。页面写了
authors时忽略它 authors, ,authorstaxonomy 的 term,顺序即署名顺序,见作者与署名。需要在taxonomies:下声明author: authorsseries, ,seriestaxonomy 的 term。正文上方的横幅取第一个,见系列series_weight, ,- 在系列中的位置。带权重的成员按升序排在前,其余按日期升序跟在后
tags, ,- 标签,见分类体系
categories, ,- 分类,同上
images, ,- 第一项作为文章封面与分享卡片;写进栏目
_index.md的cascade即为栏目级默认。images: []让这一页不继承 cascade 里的值,但不会屏蔽页面 bundle 里已有的featured、cover或thumbnail图片 byline, ,- 解析到的题图实际渲染时显示的图片署名
featured_image, ,- 本文正文里怎么渲染自己的题图;
hero使用沉浸式通栏外壳。非法值告警并回退 blog_index, ,- 写在博客根目录上,决定该栏目索引形态;
table不分页,列出整个栏目。非法值告警并回退 blog_index_columns, ,- 宽视口下的卡片列数;中等与窄视口仍保留响应式限制
blog_index_size, ,list与cards每页文章数;table始终列出整个栏目blog_index_toggle, ,- 同时发布三种索引形态,让读者切换;隐藏形态不加载图片
share, ,- 页尾分享目标,整体替换继承来的列表;
false让本页退出,见分享。未知目标告警并丢弃 summary, ,- 标签 / 分类页上文章行的摘要回退来源,
description优先
Book
指南在书籍出版。整本书通过栏目 cascade 设 type: book。
book_number, ,- 章节编号,显示在页面标题与侧栏条目前面
book_status, ,- 标记草稿章节:侧栏与目录里带草稿标记,索引里默认不列
sidebar_headings, ,- 在侧栏当前条目下展开 h2–h4 分支。超出范围告警并回退
book_draft_banner, ,- 草稿章节正文开头加一条横幅。非布尔告警并回退
Landing
指南在首页与落地页。任意页面写 layout: landing 就用落地页外壳。
landing, ,- 数据取自
data/landing/<key>/<语言>.yaml sections, ,- 在 front matter 里内联分区定义,优先于
landing。不是数组时告警,不渲染任何分区
发布页
指南在发布与下载页。栏目写 layout: releases 后忽略 weight,按发布日期与 SemVer 倒序排列。
release_url, ,- 一个 GitHub 发布地址,
https://github.com/<owner>/<repo>/releases/tag/<tag>。主题从中解析出项目、标签、日期与资产列表。其它写法告警并跳过发布区块
相关
3.4 - 博客与文章
博客文章与文档页的正文写法相同,区别在外壳:文章带日期、作者、标签与封面图,列表按年份倒序排列,栏目带 RSS。本页覆盖博客栏目的建立、文章 front matter、封面图、列表分页与 Feed。
博客目录结构
博客是 content/ 下的一个栏目,type: blog 使它使用博客外壳。子目录按发布方与受众划分,文章平铺其中。不要建年份目录,年份分组由列表页自动生成:
本站的 content/blog/
- content/
- blog/
- _index.mdtype: blog + cascade
- _index.zh.md
- oink/工程实践与公告
- _index.zh.mdcascade: images: [/images/oink.webp]
- oink-announcement.md
- oink-announcement.zh.md
- release/带版本号的发布注记
- _index.zh.mdcascade: images: [/images/releasenote.webp]
- 0.4.0.md
- 0.4.0.zh.md
- blog/
栏目根把类型下推给整棵子树,并设定该栏目共用的行为:
params.ui.blog_section(默认 blog)指明博客根的位置。目录另起名字时改这个参数,或按上面的写法用 sidebar_root_for: self。
侧栏里博客栏目默认展开,条目按日期倒序;给某篇文章写上 weight 会把它固定在最前。
一篇文章的 front matter
与文档页不同的几点:
date必填。它决定文章在列表里的位置、年份分组与 RSS 时间。写在未来的日期默认不构建,hugo server -F可以预览。description渲染成正文上方的导语,不只是搜索摘要,因此写成给读者阅读的一句话。author支持行内 Markdown,可以写成[Vonng](https://vonng.com)。需要多位作者、头像或作者主页时,改用下面的authorstaxonomy;两者互不干扰,没写authors的文章照旧渲染author。- 日期显示格式由
params.time_format_blog决定,可以按语言分别设置(本站英文是Monday, January 02, 2006,中文是2006年1月2日)。
双语文章成对存放,两种语言的 date、author、weight、aliases 保持一致;标题、描述、标签要翻译,提交 ID、版本号、命令和 URL 不翻译。
封面图
列表页与标签页的每一行左侧有一张缩略图,按以下顺序解析,第一个命中的生效:
- 文章 front matter 的
images,取第一项; - 页面包里文件名含
featured的图片资源(会被裁切成缩略图,图片资源自己的byline会作为图注); - 从祖先栏目
cascade继承来的images,就近生效。
栏目级默认封面用 Hugo 原生的 cascade 覆盖整棵子树,本站两个子栏目各设一张:
某一篇不要封面时,在它的 front matter 写 images: [];整个子栏目都不要,就把 images: [] 写进那一层的 cascade。站点级的 params.images 不受影响 —— 它只做分享卡片,不会渲染成列表缩略图。
渲染到文章正文里
默认情况下,解析出来的这张图只出现在列表行与社交卡片里,文章本身什么都不显示——手写一个题图,迟早会和卡片对不上。params.ui.featured_image 让主题用同一个解析结果把它渲染出来:
| 模式 | 文章里显示什么 |
|---|---|
none |
什么都不显示。主题默认值,所以今天不渲染题图的站点,升级后渲染出的字节完全一样 |
banner |
标题上方一张固定 16:9 的图,连着读一串文章时节奏统一 |
wash |
图铺在文章头部背后,只留十分之一的不透明度,在正文开始之前渐隐为无——文章从自己的主题里取到一点颜色,却不消耗任何对比度 |
页面键是 featured_image,所以某个子栏目的 cascade 可以只为那棵树打开它,单篇文章也可以退出。没有题图的文章在两种模式下都不渲染任何东西——正因如此,一个题图有一搭没一搭的栏目也可以整体打开这个开关。两种模式都不引入脚本,也不增加打包成员。
列表页与分页
栏目 _index.md 的正文之后,主题自动接上文章列表:按年份分组(「撰写于 2026」),年份倒序,每条显示标题、日期、所属子栏目、标签、缩略图与正文前 250 字的摘要。
分页用 Hugo 原生的分页器,默认每页 10 篇,在 hugo.yml 里调整:
取值与其余分页选项见 Hugo 文档。
卡片形态
params.ui.blog_index: cards 把同一份列表渲染成内容卡片网格而不是行列表:文章题图的 16:9 裁切在上,标题、日期与子栏目行居中,下面三行摘要。
这个选择纯粹是呈现层面的——按年分组、分页与 manual_link 的行为完全一致,行列表那一路的输出一个字节都没变。列数只在 xl 断点以上生效;md 到 xl 之间恒为两列,md 以下一列。博客根目录的 front matter blog_index 或它的 cascade 可以按栏目设置。Term 页与 taxonomy 页保持行列表,读者侧没有在两种形态之间切换的开关。
卡片题图只要资源可处理就走 Hugo 的 .Fill,一屏卡片不会为此下载一堆原图。
RSS
哪些页面产出 Feed 由 outputs 决定。给 section 加上 RSS,每个栏目就有自己的 Feed:
outputs 一旦写出就整体替换 Hugo 的默认值,RSS 必须显式写回。漏写等于关闭该类页面的 Feed,构建不会报错。
本站因此有 /zh/blog/index.xml(整个博客)与 /zh/blog/release/index.xml(只有发布注记)。栏目 Feed 递归包含所有子栏目的文章,订阅 /zh/blog/ 即可收到全部。单篇文章没有自己的 .xml。
每种语言有各自的 Feed,地址是该语言路由加 index.xml。条数上限由 Hugo 的 services.rss.limit 控制。在博客根与它的一级子栏目页上,标题行右侧操作按钮的首位是 RSS 链接,读者不必手拼地址。
全站不需要 Feed 时用 disableKinds 关闭这一类输出,比逐个页面类型删除 RSS 更彻底:
组件在 Feed 里退化成静态形态:折叠块展开、交互控件去掉。四态输出的规则对博客与文档一致。
分类与标签
tags 与 categories 是 Hugo 的分类体系,主题把它们渲染成文章头部的 chip、右栏的标签云和顶栏的筛选菜单。启用、双语标签与按内容类型开关见分类体系。
发布注记
带版本号的发布公告写成普通文章,惯例放在 blog/release/ 下,linkTitle 带版本号(Oink v0.4.0)。需要发布卡片、资产表与校验和的下载页见发布与下载页。
文章里用组件
提示块、标签页、代码块、图片、表格的用法与文档页相同,语法见组件总览。文章正文的标题同样写显式英文 {#id}。
文章末尾的反馈 / 最后修改 / 翻页器 / 评论四块与文档页一致,见编写页面。博客通常关闭反馈、保留评论。
作者与署名
声明这个 taxonomy 就是全部开关,主题不为此增加任何参数:
文章按顺序写出作者:
文章头部就按这个顺序渲染头像与带链接的名字——front matter 里的序列既是集合也是顺序——列表行渲染名字,博客 feed 为每篇文章的每位作者发一条 <dc:creator>,与站点级的 managingEditor 并存。名字之间用 CSS 的 gap 分隔而不是连接词,因为「和」是个逐语言的决定,而这里有 32 种语言。
作者主页就是 term 页本身,所以不存在另一份 data/authors 和它打架:
显示名取的是 term 页的链接标题——写了 linkTitle 就用它,否则用 title——所以主页可以挂全名、署名处用短昵称。description 是一句话介绍,正文是长介绍,头像则是题图解析器为这一页选中的那张——images: 与页面包里的肖像文件,走的是文章题图那套同样的规则。双语主页就是旁边一个 _index.zh.md。文章写了、但没人给它建主页的名字照样出署名:链接标题、一个首字母,以及指向归档页的链接。
0.4 的 author: 字符串在没有 authors 的地方原样保留,两种写法互不告警。
系列
系列是一条穿过若干篇各自独立成文的文章的阅读路径。编号、交叉引用与聚合输出属于书籍,这里是更轻的那个东西。声明 taxonomy 同样就是全部开关:
文章写出系列名,也可以给自己定个位置:
它的正文上方就会出现一条横幅,写明系列名、自己是第几篇、下一篇是哪篇,以及折在 <details> 里的完整列表——不用 JavaScript,也不增加打包成员。term 页 content/series/<name>/_index.md 是系列的引言,旁边放一个 _index.zh.md 就成双语。
阅读顺序由主题自己算,因为 term 页给不出这个顺序:Hugo 的 taxonomy weight 既到不了 Page.Weight,也进不了 GroupByParam。带权重的成员按 series_weight 升序排在前,其余按日期升序跟在后面,同序时用 Path 决胜。横幅与 term 页读同一个解析结果,所以它们不可能对「第二篇是哪篇」有分歧——这也意味着系列 term 页是由旧到新排列的,和其它所有 term 页相反。这正是这个功能本身。
一篇文章属于多个系列时只显示一条横幅,取它写在最前面的那个系列。只有一篇的系列不显示横幅。
authors 与 series 都不出现在文章的通用 taxonomy 标签行里,因为它们各自有专门的呈现面。想把某一个放回去,就在 params.taxonomy.page_header 里写上它的名字。
分享
params.ui.share 在页尾最前面放一条分享栏。它默认为空,所以在站点写出目标之前什么都不渲染;写出来的顺序就是渲染顺序:
可选的目标有十六个:x、bluesky、mastodon、facebook、linkedin、reddit、hackernews、telegram、whatsapp、line、pinterest、weibo、chatgpt、claude、email、copy。未知的名字告警并丢弃。Discord 是故意没有的:它根本没有公开的 share-intent URL,与其让主题去猜一个私有 scheme,不如用 copy 顶上。
页面键是 share,所以 cascade 可以把这条栏限定在一棵树里,页面自己的列表会整体替换继承来的那份,share: false 则让单页退出:
只有普通页面渲染分享栏——列表页、term 页与首页没有「唯一被分享的那个东西」——打印、Markdown 与 RSS 一概不带。
它不做什么,才是它能出现在这个主题里的原因。没有分享计数、没有平台 SDK、没有 iframe、没有第三方脚本或样式表——而那三样正是这类组件通常的形态:每一页都向一家读者从未选择过的公司发一次请求。每个目标都是一个纯粹的 <a href> intent 链接,只带这一页自己的 permalink 与标题,不挂任何投放参数,另加一个本地复制按钮。站点构建时不取任何东西,页面加载时也不取;一次分享唯一可能引发的请求,就是读者点下去之后自己发起的那次跳转。把十六个目标全开的构建,不加 --third-party 也能通过 bin/check-output-security.py。
chatgpt 与 claude 是把同一个构建期 permalink 交给助手,附一句「请读这一页」。它们不是页面操作菜单里的「在 ChatGPT 中打开」/「在 Claude 中打开」——那两条由运行时在激活时改写成浏览器里的实时 URL,因此留在 page_context_menu.assistant_links 后面。
复制按钮就是内置的 copy_link 动作,也就是说不管有没有配分享栏,命令面板在每个站点的每一页上都带着它。
验证
必须 Total in …,没有 ERROR / WARN。随后确认:
- 文章出现在
/zh/blog/的正确年份分组里,日期显示为中文格式; public/zh/blog/index.xml存在,里面有这篇文章,链接是完整的绝对地址;- 缩略图出现在列表里(缺失说明三条封面来源都没命中);
- 标签 chip 能点进对应的标签页。
相关
3.5 - 书籍出版
type: book 把一棵目录树变成一本书:章节编号、图表式例编号、交叉引用、生成式索引与整本打印。一本书是一棵 type: book 的内容树:目录决定章节顺序,front matter 决定章节编号,图 / 表 / 式 / 例各带一个手写编号与稳定锚点。交叉引用在四种输出里都能解析,书根页面可以生成整本打印 HTML。
前提两条:站点的 markup.goldmark 已开启属性行与 passthrough(见组件总览);params.ui.shell_types 保留 book(主题默认包含)。
一本书的目录
书根是一个普通的 Hugo section,章是它的子目录,节是章里的页面。没有第二份章节清单:侧栏、翻页器、生成的目录读的都是这棵树。
content/handbook/ 一本书
- content/handbook/
- _index.md书首页:type: book + cascade,放 book-toc 与各类索引
- ch01/
- _index.md第 1 章章首页:book_number: 1
- install.md1.x 节
- bootstrap.md
- ch02/
- _index.md第 2 章:编号 2(book_number),草稿可标 draft
- replication.md
- failover.md
- appendix.md不编号的附录,照样进侧栏与翻页顺序
章节编号手写:book_number 写什么就显示什么,主题不按目录顺序自动编号。图 / 表 / 式 / 例的 num 同理,是作者掌握的字符串(2-1、5.3、A-2 均合法),不是渲染时计算的序号。重排目录因此不会让已经印出去的编号漂移。
书首页与章首页
书根声明类型、级联给后代,并显式请求 print 输出。这项聚合输出构建代价高,主题不替消费站开启:
分区书对应 Hugo 的 section 输出类型,书位于站点根时才用 home:
章首页只需要编号与顺序:
book_number 显示在页面标题、侧栏与生成目录里。book_status: draft 是可见的编辑状态标签,不改变 Hugo 的发布状态:草稿章节照常构建、照常发布。
sidebar_headings 接受 false、true(只到 h2)或 2–4 的最大层级。要被引用的标题一律写显式 ID,如 ## 同步复制 {#sync-replication}:自动生成的 slug 适合导航,不适合作为长期引用目标。
编号:原生形态
四种编号对象各有一种原生形态:一个 Markdown 块,紧跟其后一行属性行。属性行里 num= 是编号,#id 是锚点,caption= 是纯文本题注。
图
图片块后面跟属性行。#id 省略时默认是 fig-<num>。

原生图形态要求站点设置 markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false,否则属性行会挂到段落上被忽略。替代文字取自 Markdown 图片本身,不会被题注替代。
表
管道表后面跟属性行,默认 ID 是 tbl-<num>。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Committed | 不可能 | 可能 | 可能 |
| Repeatable Read | 不可能 | 不可能 | 可能 |
| Serializable | 不可能 | 不可能 | 不可能 |
式
$$ 块后面跟属性行,默认 ID 是 eq-<num>。编号与题注排在公式右侧的同一行里,不换行;题注写长了会挤压公式那一列,公式随之变成需要横向滚动的区域。公式的题注要短。
原生形态依赖站点开启 Goldmark passthrough。未开启时用下面的 eq shortcode,它走本地服务端 KaTeX。
例
代码围栏加 num= 与 caption= 即编号例,默认 ID 是 eg-<num>。围栏里写的 #id
命名外层 <figure>,即引用目标,不是代码块本身。例的题注必填:只写 caption 时
忽略它,只写编号时丢弃编号并告警;严格发布构建拒绝这条警告。编号例渲染成一个
整体:题注是框的表头,正文在框内;正文恰好是一个代码块时贴着框排,不再另画一圈边框。
编号:shortcode 形态
四个 shortcode fig tbl eq eg 渲染出与原生形态一致的 <figure>,注册到同一个目标表,按源码位置排序。仅在原生形态做不到时使用:图片要外链跳转、表格要在一个编号下放多张表、站点未开 passthrough、例子体是多个围栏加说明文字。
fig 用 src=(也接受内部 Markdown 内容,二者互斥),并额外支持 link alt width height class 与迁移用的 title 别名:
tbl 把标签、表格、题注与锚点包进一个语义 figure:
| 输出 | 标签 | 锚点 |
|---|---|---|
| HTML | 可见 | 稳定 |
| 打印 | 可见 | 稳定 |
eq 的内容交给本地服务端 KaTeX,因此不依赖 passthrough:
不带参数的 {{< eq >}} 是无编号的块级公式兜底:不注册目标,不能被 xref 引用,也不出现在公式索引里。
eg 是包装型 shortcode,正文按页面的 Markdown 策略渲染,通常装一个或多个围栏:
同一页里 ID 必须唯一,同一类里一个编号也只能对应一个 ID。重复时告警并保留第一项; 严格发布构建拒绝这条警告,消息指出先占用它的那一处在哪行。
Hugo 把 shortcode 的正文当作独立 Goldmark 文档渲染,脚注是页面级的。tbl、
eg、fig、card、tab、field、include 的正文里出现 [^label] 会告警,
消息给出文件、行号与标签;严格发布构建拒绝这条警告。定义写在页面上时该引用会
原样印出 [^label],定义写在正文里则生成第二份脚注列表、fn:N 与页面自身 ID
冲突——两种结果都不该发布。
需要脚注的表格或代码块改用原生形态:表格、图片、围栏加 {num=… caption=…},内容留在页面文档里,脚注照常编号、跳转与回链。渲染出来的图表与 shortcode 形态一致,所以这通常是一行改动。代码里形似脚注的文本(列表里的 [^0-9] 字符类、行内代码)不受影响。
交叉引用
引用同页目标可以用普通 Markdown 链接:表 2-1 指向上面那张隔离级别表。代价是标签与编号手写,改编号时需要自己检索。
xref 把标签、编号与锚点合成一处,并支持跨页与跨语言:
参见 图 2-2 与 示例 2-1; 显式锚点:图 2-1。
规则:
- 最多一个类型键(
figtbleqeg)。类型提供本地化标签(图 / 表 / 公式 / 示例)并推导出默认锚点<kind>-<num>。 anchor=覆盖推导出的锚点,用于目标写了显式#id的情况。page=跨页引用,走 Hugo 当前语言的页面查找,源码里不必硬编码/zh/前缀。- 不给类型时必须同时给
anchor=和内部链接文字:{{< xref page="../ch01/install" anchor="sync-replication" >}}同步复制{{< /xref >}}。 - 引用可以出现在目标之前,渲染时不读注册表,因此前向引用合法。
跨页的普通 Markdown 链接在整本打印里仍然是站点 URL。需要在聚合文档里也能跳转的引用写成 xref。
索引:目录与图表清单
五个索引 shortcode 遍历同一棵书树,触发后代内容并聚合注册结果。它们通常放在书首页(_index.md)或专门的「插图目录」页上。
这五个 shortcode 在本页只给源码。它们从当前页所在的导航根向下遍历,放在一棵普通文档树里会把整棵 docs 树当作书列出。真实效果见《使用 OINK 创作优美的内容》,源码位于
content/book/_index.md。
book-toc的depth取 1–3:1 列章,2 加入嵌套分区,3 再投射每页的标题树;drafts=false只把book_status: draft的行从这份生成列表里滤掉,不影响页面发布。book-figures/book-tables/book-equations/book-examples不接受任何参数,各列一类,条目形如「图 2-1 — 题注」并链到稳定 ID。- 整本打印时,这些链接全部变成文档内片段。
顺序阅读与草稿
翻页器默认对 docs、book、blog 三种类型开启,顺序是侧栏那棵树的前序遍历:分区首页在前,子页按 weight。关闭整类改 params.ui.pager_types,关闭单页写 pager: false。
toc_hide、manual_link 纯链接占位、sidebar_divider 分隔行都不会成为翻页目的地。
草稿章节除了侧栏上的「草稿」标签,还可以开启页首横幅:
横幅只在 type: book 且 book_status: draft 的页面出现,文案来自本地化键 book_draft_notice。
打印整本
书根有了 print 输出后,按可见的阅读顺序生成封面、本地目录、根页面正文与每个后代章节,全部装在一个 HTML 文档里。no_print: true 的页面、纯链接节点、分隔行与隐藏占位不会成为章节。
聚合文档里,编号组件的 ID 逐字节保留。页面内的 Markdown 标题与脚注 ID
会加上来源页面前缀,避免多章共有 summary 这类锚点、或都从 fn:1 开始时冲突;
生成的链接同步改写。页面单独渲染为 Print 时,与普通 HTML 保持相同的页面局部
ID——只有多页分区或整书聚合才增加命名空间。
产物是面向打印的 HTML。可选的 BookManifest 输出会把同一份阅读顺序记成 JSON,
主题另外提供 bin/book-epub.py 与 bin/book-pdf.py,把清单与打印 HTML 打包成
EPUB 和 PDF。
具体开关与整章打印见打印支持。
迁移既有书稿
已有的中文书稿通常用站点自己的 figure shortcode、加粗的假题注、指向 #fig_* 的裸链接来表示图表编号。主题仓库带一个迁移脚本,把这些旧形态改写成 fig、tbl 与 xref,并保留原有的公开锚点。站点先固定到一个包含 Book 组件的已发布 OINK 版本,再迁移内容。
四个配方对应三份真实书稿的旧约定(DDIA 的 v1 与 v2 各一个),只识别在那些书稿里观测到的形态:
--profile |
识别的旧形态 |
|---|---|
tpme |
假 h6 题注加相邻图片、题注加相邻表格、/en/...#fragment 裸链接 |
ddia-v2 |
站点自有的 figure shortcode,按编号图 / 表 / 代码例分类 |
ddia-v1 |
裸图片加相邻的一条加粗编号题注,ID 由图片文件名推导 |
pg-internal |
加粗或斜体的中英文「图 N」题注紧邻一张图片,编号表题注紧邻一张表格 |
--profile- 必填,取上表四个值之一
--root- 必填,消费站仓库根目录
--path- 限定
--root下的文件或目录,可重复;默认扫描整棵内容树 --write- 应用改写。默认是干跑,不写任何文件
--no-diff- 不打印 diff,仍输出摘要与报告
--report- 写出机器可读的 JSON 报告
diff 走标准输出,摘要走标准错误,报告含 files_scanned、files_changed、counts、skipped、idempotent 五项。脚本只改写能唯一确定的目标:无法确定编号、题注不唯一、标记形态不认识的地方原样保留,逐条记进 skipped 供人工处理。旧题注里的粗体、行内代码与公式会降级为纯文本,因为 Book 的题注契约是纯文本。
审阅 diff 之后在专用分支上应用,再运行第二遍确认幂等:
第二份报告应当是 files_changed: 0、counts 为空、idempotent: true;脚本以退出码 0 表示幂等。
配方只识别这三份书稿里实际观测到的旧形态;书稿的旧约定不在这四个配方之内时,脚本不适用,需要按编号:原生形态手工改写。主题仓库的 bin/check-book-migrations.py 用干跑与幂等两项检查覆盖这四个配方。
验证
- 构建零告警:
hugo --printPathWarnings --panicOnWarning。编号写错、ID 重复、题注缺失都在这一步失败。 - 页面上应看到「图 2-1」这样的本地化标签、可点的
xref链接,以及点击后正确跳转的锚点。 - 对比侧栏、翻页器、
book-toc与整本打印四处的章节顺序是否一致。 - 检查 Markdown 输出:
curl -s http://localhost:1313/zh/handbook/ch02/index.md。shortcode 形态应退化成**图 2-2.** 题注加原始正文,原生形态原样保留源码块与属性行。 - 从主题仓库对构建产物跑一遍锚点检查:
它校验每个引用的目标锚点存在、类型与编号匹配、页内 ID 唯一,以及编号图片有与题注相称的替代文字。
Book shortcode 参数
num, ,- 必填(
eq无参形态除外)。匹配[0-9A-Za-z.-]+,要加引号 id, ,- 匹配
[A-Za-z][A-Za-z0-9_.:-]*,逐字节保留 caption, ,eg必填;figtbleq可选。不是 Markdownclass, ,- 追加到
<figure>;需要num src, ,- 仅
fig。与内部内容互斥,走共享图片解析顺序 linkaltwidthheight, ,- 仅
fig。宽高是正整数 title, ,- 仅
fig。caption的迁移别名,二者互斥
xref:
figtbleqeg, ,- 至多一个。提供本地化标签并推导锚点
anchor, ,- 无类型时必填,且必须有内部链接文字
page, ,- 走当前语言的页面查找,找不到时告警并渲染无链接文字
book-toc:
depth, ,- 1 章 / 2 含嵌套分区 / 3 含标题树
drafts, ,false时从生成列表里滤掉草稿章节
book-figures、book-tables、book-equations、book-examples 不接受任何参数。
限制与常见问题
- 没有自动编号。章节号、图号、表号都手写;改编号是一次有意的编辑,不是构建的副作用。
- 属性行必须紧贴块,中间不能有空行。被 Prettier 之类工具移动过的属性行静默失效,图退化成普通图片。
book_kind与book_part是契约认可的元数据键,当前主题模板不渲染它们;有视觉效果的是book_number与book_status。- 索引 shortcode 会触发后代内容渲染,在超大树上明显拉长构建时间。整本
print需要显式开启也是同一原因。 - shortcode 正文里不能出现脚注引用;出现时告警并指出改用原生形态,严格发布构建 拒绝这条警告,见上文编号:shortcode 形态。
- 打包是可选的,且在构建之外运行。
BookManifest加上bin/book-epub.py/bin/book-pdf.py可以产出 EPUB 与 PDF,但没有任何一次 Hugo 构建会自己生成这两个文件;专业排版的分页、字体嵌入与索引编制仍在契约之外。
相关
3.6 - 发布与下载页
OINK 把发布事实集中在两处本地数据:页面 front matter 的 release_url 指明这一页对应哪个 GitHub 发布,data/download/<key>.yaml 记录安装方式。发布卡片、资产表、下载区块与索引页都从这两处推导。构建期不访问 GitHub,也不声称某个标签或资产已经存在。
front matter 里放了一个 release_url(OINK v0.4.0),下面的卡片、资产表与下载区块都是真实渲染。校验和与资产文件名是构造的:URL 由组件按仓库与标签本地推导,指向的文件在真实发布里不存在,不要用这里的哈希校验产物。
组件与事实来源
| 你要的 | 用什么 | 事实来自 |
|---|---|---|
| 版本摘要卡片(标签、日期、归档、仓库) | release-card |
页面的 release_url |
| 校验和资产表 | checksums 围栏 / release-assets |
正文里的 sha*sum 行 |
| 多渠道下载区块 | download |
data/download/<key>.yaml |
| 按时间排序的发布索引页 | layout: releases |
各页的 release_url,没有则用标题 |
页面拥有发布事实
发布页 front matter 里的一个键就是全部记录——精确到标签的 GitHub 发布 URL:
owner、项目名与标签从 URL 里解析出来,日期用页面自己的 date。不是精确
标签形式的 GitHub 发布 URL 会警告并跳过发布区块——--panicOnWarning 构建
随之失败。0.5 的 release 映射(product / version / repo / tag / date /
prev / checksums)及其字符串简写已移除;仍携带它的页面会收到指名
release_url 的警告。
在需要摘要的位置放一个不带参数的 shortcode,调用里不接受任何事实:
v0.4.0 ·
卡片带着仅凭 URL 就能推导的四个链接——发布页、两种源码归档、仓库——全部本地推导。校验和文件放在正文下方的资产表里,版本对比在 GitHub 上看。
发布索引页
一个分区可以改用发布索引布局。它列出小节里的每一个常规页面,从新到旧 ——按页面日期排序,同一天内以标签里的版本号决胜(SemVer 优先级,非 SemVer 标签用确定的字典序兜底):
release_url 可解析的条目读作「项目名 + 标签」——如 oink v0.4.0——下一行
是页面描述;没有它的页面保留自己的标题,版本之间夹一篇普通短文是合法条目,
不是警告。0.5 的 release_products 过滤与 release_group_by_product 分组
已移除;写了会警告。
本站的版本发布目前用普通博客列表。需要严格时间序时改用 layout: releases。
校验和资产
checksums 围栏是校验和表的原生形态,围栏里写 sha*sum 命令的原样输出:
| 文件 | 校验和 |
|---|---|
| oink-0.4.0-linux-amd64.tar.gz Linuxamd64SHA-256 | 1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4 |
| oink-0.4.0-darwin-arm64.tar.gz macOSarm64SHA-256 | 7b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250 |
只接受两种行:<十六进制><两个空格><文件名> 与 <十六进制><空格>*<文件名>。空行
与以 # 开头的行忽略。哈希长度决定算法(MD5 / SHA-1 / SHA-256 / SHA-512),一个块
里只能有一种算法。格式错误的行带行号告警并跳过;严格发布构建拒绝这条警告。文件名
必须是单个路径段。类型、操作系统与架构徽章由文件名推断,属于装饰,推断不出时不显示。
资产链接的基址:页面有 release_url front matter 时推导为 https://github.com/<repo>/releases/download/<tag>/;没有发布事实的页面必须显式写 base=。两者同时存在时报错。
release-assets 是同一个解析器与渲染器的 shortcode 形态。它多一个围栏没有的 src=,可以把校验和文件本身提交为页面资源或全局资产(src 与围栏内容互斥);group="auto" 按平台与架构分组:
.rpm
| 文件 | 校验和 |
|---|---|
| oink-0.4.0-1.el9.x86_64.rpm Linuxamd64SHA-256 | 5a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6 |
| oink-0.4.0-1.el9.aarch64.rpm Linuxarm64SHA-256 | c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8 |
HTML 里哈希截断显示,完整哈希保留在无障碍名称与复制源里,复制按钮由按需加载的本地运行时提供。禁用 JavaScript 时仍是一张完整的带链接表格。打印展开完整哈希且不带控件,Markdown 与 RSS 是完整哈希的管道表。
下载渠道数据
安装方式属于产品,不属于某一次发布,因此存放在 data/download/<key>.yaml。本站真实的记录是 data/download/prd5.yaml:
记录级字段只有 version repo tag published channels 五个。多写一个键时告警并
跳过记录,严格发布构建拒绝这条警告。version 也可以不写在这里,改由站点的
params.version 提供。
version, ,- 两处都没有时告警并跳过区块
repo, ,- 固定版本渠道有链接或资产时必填
tag, ,- 只允许 URL 安全字符
published, ,false表示不可变发布还不存在channels, ,- 非空
每个渠道:
id, ,- 记录内唯一,用作锚点
kind, ,- 决定能不能插值版本事实
title, ,- 必须能解析出非空值
note, ,- 渠道下方的一行说明
icon, ,- 例如
fa-solid fa-bolt url, ,- 仅
pinned可插值 steps[], ,- 代码步骤走 OINK 的增强代码渲染器
checksums, ,- 仅
pinned;与checksums_src互斥 checksums_src, ,- 把校验和文件当作 Hugo 资产读入
两条规则:
- 本地化按后缀解析:
<字段>_<精确语言>→<字段>_<主语言>→<字段>。中文站解析title_zh_cn、title_zh、title。不接受 camelCase 别名。 - 只有固定版本渠道的
url与steps[].code能插值${version}与${tag}。滚动渠道拒绝插值,避免稳定版安装命令被绑定到某个版本。标题与说明不插值。
渲染下载区块
download 接受恰好一个位置参数,即数据键:
安装脚本
滚动渠道刻意不插入版本号。
源码归档
发布资产
| 文件 | 校验和 |
|---|---|
| oink-0.4.0.tar.gz SHA-256 | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
HTML 渲染一排锚点 chip 加各渠道分区,代码步骤复用增强代码块与按需加载的复制运行时,校验和渠道复用上面那张资产表。打印静态展开同样的内容,Markdown 输出标题、源码围栏与完整哈希,RSS 不输出这个组件。
标签未打、资产未上传时,把记录标为未发布:
滚动渠道照常可用。固定版本渠道变成不可点击的「待发布」状态,省略固定版本命令,禁用资产链接与复制控件。标签与资产可解析之后再翻转这个开关,不要先在正文里写入推测出来的链接。
同一份记录也能被 Landing 页面的 download 分区消费,不需要第二套版本模型,见首页与落地页。
与博客发布注记的关系
两者分工:
- 博客里的发布注记(本站在
content/blog/release/)是叙事:这一版改了什么、怎么升级、有什么破坏性变更。它的 front matter 里带release_url,页首可以放一张release-card。写法见博客与文章。 - 下载数据是操作:选哪个渠道、运行哪条命令、校验哪个哈希。它与版本号解耦,升级时只改一处。
一次发布的顺序:更新 data/download/<key>.yaml 的 version → 新写一篇 content/blog/release/<version>.md 并填 release_url → 标签与资产就绪后把 published 翻成 true。
验证
- 构建零告警:
hugo --printPathWarnings --panicOnWarning。哈希行格式、算法混用、缺base、渠道字段拼错都在这一步失败。 - 页面上:卡片显示的标签与日期与仓库一致;资产表每行都能点开真实的下载 URL。
- 逐条核对哈希与实际产物:组件只负责排版,不验证内容。
- 检查非 HTML 输出里哈希是完整的:
- 发布前先用
published: false走一遍,标签与资产确实存在后再改成true;每种语言、子路径部署各测一次。
相关
3.7 - API 文档
一页接口文档由一份 OpenAPI 规范加一个 shortcode 构成。Swagger UI 与 Redoc 两个运行时随主题分发(版本分别是 5.32.13 与 2.5.3,见仓库 VENDOR.json),只有用到它们的页面、且只在 HTML 输出里加载,构建与浏览都不访问外部服务。Swagger UI 的在线 validator 已写死关闭(validatorUrl: null),已发布的接口页面不会把规范地址发往任何地方。
三个步骤:把规范文件放进 static/,新建一页写上 shortcode,需要专用外壳时把页面 type 改成 swagger。
规范文件的位置
规范文件放在 static/ 下,原样发布到站点根,两个 shortcode 得到的都是浏览器可取的 URL:
规范文件的位置
- static/
- openapi/
- docs-demo.yaml发布为 /openapi/docs-demo.yaml
- openapi/
- content/
- docs/
- write/
- openapi.zh.md这一页
- write/
- docs/
不要把规范文件放在页面旁边。两个 shortcode 都把本地值视为 static/ 下的路径,
都不解析页面资源。内容页面旁边的 .yaml 属于页面资源,仅在 shortcode 中写出
它的名字并不会让 Hugo 发布它,浏览器因此会得到 404。
远程规范(https://… 开头)两个 shortcode 都接受,但那是一项网络依赖,还会把读者的元数据暴露给那台主机。内网部署与有 CSP 的站点应当使用同源规范。只接受 http 与 https:其它 scheme、协议相对的 //host 或空值都会告警,shortcode 不渲染。
下面的例子用真实存在的 /openapi/docs-demo.yaml,一份演示用的集群管理 API,没有可访问的服务端。
Swagger UI
swagger 只有一个具名参数 src,值是从站点根开始的 URL。它经过主题的 URL 校验,子路径部署同样正确:
它渲染一个 class="td-swagger-ui" 的容器,规范地址放在 data-td-spec-url 上;页面上所有容器由一个可缓存的 js/chunks/swagger-init.js 统一挂载。容器 ID 由页面地址与 shortcode 序号推导(td-swagger-<hash>-<n>),因此同一页可以放多个。
本页只给源码,不真渲染 Swagger UI:它自己生成的标记有 axe WCAG AA 违规(服务器下拉框没有可访问名称、版本号区域是不能聚焦的可滚动区),本站的无障碍门禁要求每个页面零违规。下面的 Redoc 是真渲染的——但要知道两个控件都被排除在那道门禁之外,因为 Redoc 的接口描述文字自身有对比度缺陷。两者都不是完全无障碍的界面,见限制。
Redoc
redoc 只接受一个位置参数,即规范路径。多写一个参数会告警,shortcode 不渲染。
OpenAPI 规格文件 — https://oink.pgsty.com/openapi/docs-demo.yaml
http 或 https URL 保持为远程地址。其它通过校验的值都是 static/ 下的路径,
开头有无斜杠等价。例如站点 baseURL 为 https://example.com/preview/ 时,
openapi/docs-demo.yaml 与 /openapi/docs-demo.yaml 都会变成
https://example.com/preview/openapi/docs-demo.yaml。与 swagger 不同,Redoc
接收的是这个基于 baseURL 的绝对 URL。
主题固定了 hide-hostname hide-logo suppress-warnings lazy-rendering native-scrollbars 五个属性,并用 CSS 隐藏 Redocly 品牌图标。Redoc 的其余属性目前不开放给作者,需要它们时在站点里覆盖 layouts/_shortcodes/redoc.html。
专用页面外壳
接口文档页通常较宽较长,可以用 swagger 页面类型:
swagger 是主题默认的外壳类型之一(params.ui.shell_types 默认是 [docs, book, blog, swagger],站点覆盖这个列表时需要保留它)。它与 docs 外壳的差别只有两处:<body> 上多一个 td-swagger class 供样式挂钩,以及不显示版本横幅。侧栏、目录、面包屑、翻页器与页尾都照常。
外壳与页宽的完整说明见布局与页面类型。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 完整的交互式 Swagger UI / Redoc;运行时按需加载,本地文件,无 CDN,且只在这一种输出里 |
| 打印 | 一行带标题的静态链接,规范地址可见;两套运行时都不加载 |
| Markdown | 一个纯 Markdown 链接 [OpenAPI 规格文件](/openapi/example.yaml),不会退化成接口清单 |
| RSS | 同样的纯链接 |
在 HTML 之外,接口文档是一个指路牌而不是一份参考。要让打印或 Agent 输出里也有接口信息,在同一页用正文写关键端点的说明;shortcode 之外的正文在四种输出里都完整保留。
限制与常见问题
- 两个组件的容器 ID 都按「页面地址 + shortcode 序号」推导,同一页放多个互不冲突。
- 两者可以同页共存,但页面会很长,HTML 输出也会同时加载两套运行时。正式站点选一个。
- 两个界面都不是完全无障碍的,且都来自主题不改写的上游产物。Swagger UI 的标记有 axe WCAG AA 违规(
select-name、scrollable-region-focusable);Redoc 的接口描述文字不满足 AA 对比度。本站因此把.td-swagger-ui与.td-redoc排除在零违规门禁之外——有同类门禁的站点只能照做,并且应当明说,而不是默认其中某一个能过。 redoc不接受额外属性参数:写第二个位置参数会告警,shortcode 不渲染。- 本地
redoc路径以static/为根,开头的/可有可无;它不解析页面资源。 - 规范文件必须能被浏览器取到:放
static/,构建后确认public/下存在该文件。 - 没有服务端 mock:Swagger UI 的 “Try it out” 会向
servers里写的地址发起真实请求,示例规范里的地址不可访问。
验证
- 构建零告警:
hugo --printPathWarnings --panicOnWarning。 - 规范确实发布了:
ls public/openapi/docs-demo.yaml,或访问http://localhost:1313/openapi/docs-demo.yaml。 - 页面上能展开端点、看到 schema;浏览器控制台没有 404 或跨域报错。
- 断网后再刷新一次:运行时是本地的,规范同源时界面应照常出现。
相关
4 - 组件总览
这一栏回答一个问题:某个组件在 Markdown 里怎么写。每页的顺序相同:最简例子、逐步深入的例子、输出形态、参数表、限制。查语法见下面的速查表。
两种形态
组件的第一形态是 Markdown 语法本身:块引用、列表、表格、图片、围栏,加上紧跟其后的一行 {…} 属性。原生形态在 GitHub 与任意 Markdown 编辑器中仍然可读,Markdown 输出保留的也是源码。
原生形态表达不了的场景使用 shortcode:正文标签页、带块级描述的参数表、带图标与徽章的卡片、终端录像。规则有五条:
- 所有 shortcode 都写
{{</* 名字 */>}},只有{{%/* steps */%}}用%分隔符,因为它的正文是页面级 Markdown。 - 嵌套名字(
tab、card、field)只在各自的父 shortcode 里有效。 - 作者参数写错不会静悄悄降级。普通预览会发出带源码位置的警告,并采用文档规定的
回退或略去不安全部分;发布构建带
--panicOnWarning时,那条警告会让门禁失败。 - 公开字符串参数(图注、标签、标题)一律是纯文本,不解析 Markdown。只有正文是 Markdown:
tab、card、field的正文,include引入的文件,以及 Book 的fig、tbl、eg正文。 - 页面没用到的组件不下发运行时。HTML 只引用这一页真正需要的稳定能力分片,打印、 Markdown 与 RSS 不加载交互运行时。
站点前置配置
组件依赖三项 Goldmark 设置。OINK Starter 已经配好;从零建站时照抄以下片段:
renderer.unsafe: true:Goldmark 默认丢弃内容里的原始 HTML,关闭时组件正文里嵌套的 HTML 会消失。parser.attribute.block: true:属性行的总开关。关闭时{.steps}、{caption="…"}只是正文里的一行字符串。parser.wrapStandAloneImageWithinParagraph: false:独立成段的图片不再包进<p>,图片才能成为带图注的 figure,属性行才跟得上去。
个别组件另有前置条件:公式需要开启 Goldmark 的 passthrough,PlantUML 与 Draw.io 需要自建渲染服务,各页分别说明。完整的配置键见配置总览。
速查表
「形态」列的取值:原生 = Markdown 语法加属性行;围栏 = 带语言标记的代码围栏;shortcode = {{</* … */>}}。「运行时」列说明这个组件是否往页面上下发 JavaScript。
| 组件 | 一句话 | 最短写法 | 形态 | 运行时 |
|---|---|---|---|---|
| 提示块 | 把前提、警告与折叠说明从正文中分离 | > [!NOTE] |
原生 | 无 |
| 图片 | 图注、尺寸、缩放、编号与构建期图片处理 |  |
原生 | 需站点开关 |
| 代码块 | 高亮、标题、复制、折叠、行链接 | ```sh |
围栏 | 按页加载 |
| 标签页 | 同一件事的多个平台或语言版本 | 属性行 {tab="Linux"} |
原生 + shortcode | 按页加载 |
| 表格 | 普通表格,加满宽、矩阵、标题与编号 | {.full-width} |
原生 | 无 |
| 参数表 | 参数清单,带类型 / 必填 / 默认值芯片 | {.fields meta="type default"} |
原生 + shortcode | 无 |
| 步骤 | 有先后的流程 | {.steps} |
原生 + shortcode | 无 |
| 卡片 | 一组并列的去处 | {.cards} |
原生 + shortcode | 无 |
| 文件树 | 目录结构与对齐的注释列 | ```filetree |
围栏 | 按页加载 |
| 公式 | KaTeX 行内与块级公式 | $$ … $$ |
原生 | 按页加载 |
| Mermaid | 流程图、时序图、甘特图 | ```mermaid |
围栏 | 按页加载 |
| PlantUML | UML 图;需要自建渲染服务 | ```plantuml |
围栏 | 需站点开关 |
| 思维导图 | Markdown 列表变成思维导图 | ```markmap |
围栏 | 需站点开关 |
| Draw.io | 可回编辑的图;需要自建服务 |  |
原生 | 需站点开关 |
| ECharts | 声明式数据图表 | ```echarts |
围栏 | 按页加载 |
| Infographic | AntV 信息图 | ```infographic |
围栏 | 按页加载 |
| 画廊 | 一组图片共用一个缩放对话框 | ```gallery |
围栏 | 需站点开关 |
| 徽章 | 行内状态标记 | {{</* badge text="Beta" */>}} |
shortcode | 无 |
| 按键 | 键位与组合键 | {{</* kbd "Ctrl" "K" */>}} |
shortcode | 无 |
| 引用 | 引入文件、插入站点参数、构建期注释 | {{</* include file="parts/x.md" */>}} |
shortcode | 无 |
| Asciinema | 终端录像 | {{</* asciinema file="images/x.cast" */>}} |
shortcode | 按页加载 |
「运行时」列的四条细则:
- 代码块只在块上有复制或折叠按钮时加载
code-block.js;文件树只在树带注释列时加载filetree.js,它负责拖动那条分栏线。 - 图片与画廊共用一个缩放对话框运行时,需要站点开启
ui.image_zoom,且页面上确有候选图。 - 公式在构建期由 KaTeX 渲染成 HTML 与 MathML,页面上只多一份 KaTeX 样式表与字体,没有脚本。
- Draw.io 只在渲染内容含 PNG 或 SVG 候选图的页面加载,并且每个不同的图片 URL 只检查一次。
每个组件在 HTML、打印、Markdown、RSS 四种输出下都有确定形态,见各页的「输出形态」一节。
4.1 - 提示块
> [!NOTE] 这样的块引用写出带颜色、图标与标题的提示、警告与折叠块,不需要短代码。提示块(Callout)是 GitHub / Obsidian 风格的块引用:> [!TYPE] 起头,正文跟在后面。用于把提示、警告、前提条件从正文中分离出来;正文一句话能说清的内容不必使用提示块。
最简例子
Hugo Module 需要本机安装 Go;只用离线归档时不需要。
不写标题时使用本地化的类型名(中文站显示「注意」,英文站显示 “Note”)。源码在 GitHub 上按 GitHub 的提示块渲染,在普通 Markdown 阅读器中显示为块引用,内容都不会丢失。
十种类型
前五种与 GitHub 一致,后五种是 OINK 追加的语义类型。每种类型有默认图标与强调色。
用 hugo server -D 可以预览草稿。
主题下限是 Hugo Extended 0.160.1,低于它构建直接失败。
hugo --cleanDestinationDir 会清空 public/。
删除 resources/_gen 后第一次构建会慢很多。
构建通过、零告警——可以推上线了。
不要把 go.work 提交进仓库。
站点要不要开评论?看启用评论。
pgsty.com 就是一个只用了提示块与表格的纯文档站。
Documentation is a love letter that you write to your future self.
类型名不区分大小写。
自定义标题
标记同一行的后续文字是标题,支持行内 Markdown(代码、粗体、链接)。
public/生产构建前先确认 baseURL 指向正式域名,否则所有绝对链接都会指错。
正文内容
正文是页面级 Markdown:列表、代码围栏、表格、图片、嵌套的提示块。每一行都以 > 开头,围栏也不例外。
- 克隆:
git clone https://github.com/pgsty/oink-starter my-docs - 进入目录并预览:
- 打开 http://localhost:1313/
| 端口 | 用途 |
|---|---|
| 1313 | Hugo 开发服务器 |
折叠
类型后加 - 默认收起,加 + 默认展开;两者都渲染为原生 <details>,不加载 JavaScript。适用于完整输出、备选方案、背景说明这类不必默认展示的内容。
Hugo 通过 Go 的模块系统下载主题(hugo mod get)。用 submodule 或离线归档时可以不装 Go。
收起状态不会被记住,刷新后回到默认。
中性折叠块 DETAILS
[!DETAILS] 是没有语义颜色的折叠块:不加符号默认收起,[!DETAILS]+ 默认展开。用于冗长输出、完整配置文件等需要折叠的内容。
hugo version 输出自定义图标
块引用结束后的下一行写属性 {icon="fa-solid fa-xxx"}(一对 Font Awesome class),替换该类型的默认图标。属性行紧接块引用,中间不能有空行。
从 Pigsty v4 起默认安装 PostgreSQL 18。
嵌套
提示块可以嵌套(每层多一个 >),也可以放在列表项或步骤中。建议最多嵌套一层。
升级主题版本可能改变渲染结果。
git tag pre-upgrade 就够了——回滚只是 git checkout pre-upgrade。
未知类型与易错写法
未知的类型名不会导致构建失败,也不会丢失内容:该块渲染为普通块引用,[!TYPE] 标记原样可见。
[!NOTICE] 这不是合法类型
标记会保留在页面上提醒你。
其它常见问题:
- 正文与标题合并:经过 Prettier 等格式化工具的文件,在标题行下保留一个空的
>行,否则工具会把标题并入正文。 - 属性行被格式化工具移动:把
{icon=…}这类标记行放在<!-- prettier-ignore-start -->/<!-- prettier-ignore-end -->之间。 style、onclick与不支持的属性会告警并忽略:属性行只接受icon与class; 严格发布构建拒绝这条警告(见下表)。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 静态类型是 <div class="td-callout" role="note">;折叠类型是原生 <details> + <summary> |
| 打印 | 全部静态展开,折叠块带 data-td-callout-collapsible 标记 |
| Markdown | 保留源码块引用(含 [!TYPE] 标记与标题) |
| RSS | 与打印相同,静态展开 |
提示块不加载脚本。
参数参考
标记行 > [!TYPE]± 标题:
TYPE, ,NOTETIPIMPORTANTWARNINGCAUTIONSUCCESSDANGERQUESTIONEXAMPLEQUOTEDETAILS;大小写不敏感;未知值渲染为普通块引用±, ,-折叠默认收起,+折叠默认展开;DETAILS不加符号即收起标题, ,- 与标记同一行
属性行 {…}(块引用之后紧接的一行):
icon, ,- 例如
fa-solid fa-database;DETAILS默认无图标 class, ,- 原样透传给站点 CSS
style、on* 与其它键会告警并忽略;严格发布构建拒绝这条警告。
限制与常见问题
- 不能自定义颜色:颜色由类型决定,需要新语义时选最接近的类型并自定义标题。
- 折叠状态不持久化。
- 提示块可以放在
{.steps}列表项与{{%/* steps */%}}步骤中(见步骤),块引用的每一行都以>开头,缩进与列表项对齐。
相关
4.2 - 图片
图片只有一种写法:Markdown 的 。独立成段的图片可以在下一行跟一行 {…} 属性,成为带图注的 figure、缩放候选、编号图或经 Hugo 处理的派生图。主题没有图片 shortcode。
最简例子

这张图与本页放在同一目录(页面包)中,主题读取它的固有尺寸并写入 width/height,页面加载时不发生跳版;所有图片懒加载。替代文字供屏幕阅读器与搜索引擎使用,应当始终填写;空 alt 表示装饰性图片,缩放会跳过它。
图片来源
来源按以下顺序解析,写法相同:
| 放法 | 源码里怎么写 | 适合 |
|---|---|---|
与页面同目录(页面包 index.md + 图片) |
 |
只有这一页用的截图;随页面一起移动、翻译共用 |
全局资源 assets/images/… |
 |
多页共用、还要做处理(缩放 / 裁切)的图 |
静态目录 static/images/… |
 |
不需要处理的大图、下载物;主题拿不到尺寸时可以用 width/height 补 |
| 远程 URL |  |
少用:构建期不会下载,也不能处理 |
相对路径先按页面资源、再按全局资源查找,都找不到时按静态路径原样输出;主题不检查
静态路径与远程 URL 是否存在。要求处理(command=)却解析不到可处理资源时,普通
预览告警并保留未处理图片;严格发布构建拒绝这条警告。
行内与块级
位于文字中间的是行内图片,渲染为一个 <img>,不能带属性;独立成段的是块级图片,可以带属性行。
这一枚小图
夹在句子里,是行内图片。

行内图片按自身尺寸显示(这里是 50×32)。没有固有尺寸的 SVG 行内插入时会被拉伸到容器宽度,SVG 应作为块级图片使用并给出 width/height。
块级图片依赖站点设置 markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false(本站已配置;见配置总览)。缺少它时 Goldmark 会把独立图片包进 <p>,属性行也会被当作正文。
图注
属性行加 caption="…",图片渲染为 <figure> + <figcaption>。图注是纯文本,不解析 Markdown。

Markdown 里的 "标题" 保持原义(悬停提示),不会成为图注。
尺寸
width/height 是正整数,覆盖资源自身的尺寸:为静态或远程图片提供占位框以避免跳版,或把大图缩小显示(浏览器缩放,不改文件)。

处理型图片
页面资源与全局资源可以在构建期由 Hugo 处理:command 与 options 必须同时给出,命令是 Fit Resize Fill Crop 之一,选项是 Hugo 的图片处理字符串。渲染出的 src 是派生图;启用缩放时对话框打开原图。


静态路径、远程 URL 与 SVG 不能处理。对它们写 command 时告警并保留未处理图片;
严格发布构建拒绝这条警告。选项语法(锚点、质量、格式转换,如
300x150 webp q80)见 Hugo 图片处理。
链接图片
两种写法,用途不同:
- 没有图注、图片本身是链接:用 Markdown 的链接包图
[](href)。 - 有图注的 figure 整体可点:属性行加
link="…"(必须同时有caption或num)。

带链接的图不参与缩放。没有图注只写 link= 时告警并丢弃链接,消息提示改用
[](…);严格发布构建拒绝这条警告。
编号图
编号图用于书籍与长篇手册:属性行加 num,可选 #id。编号是作者书写的字符串(2-1、3.4),主题不自动计数;图注前加本地化的「图 2-1」前缀,#id 缺省为 fig-<num>。正文用普通链接 [图 2-1](#fig-2-1) 或 xref shortcode 引用;全书图目录见书籍出版。

见图 2-1。
编号图可以同时是处理型图片(num + command),也可以带 link。
缩放
图片缩放默认关闭。站点开启后,块级图片、figure、画廊中带 alt 的图成为可点击的按钮,在原生 <dialog> 中查看大图(Esc 关闭,焦点回到原处)。本页在 front matter 中开启了它,上面的图都可以点击。
不缩放的图:行内图、alt 为空的装饰图、带链接的图、data-no-zoom 标记的图。运行时只在页面确有候选图时加载;打印 / Markdown / RSS 中没有对话框。

深浅色图片
主题没有按深浅色切换图片的参数。需要两张图时,各写一个 class,在站点 CSS 中按 [data-bs-theme="dark"] 显示其一:
class 由主题原样透传,供站点 CSS 使用。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 行内 <img>;块级 <img class="td-image">;有图注 / 编号时 <figure class="td-figure"> + <figcaption>;缩放候选带 data-td-image-zoom |
| 打印 | 同 HTML,去掉缩放控件 |
| Markdown | 原样输出  与属性行 |
| RSS | 图片 src 改为绝对地址;无缩放 |
参数参考
属性行 {…}(块级图片之后紧接的一行):
caption, ,- 有它就渲染成 figure;不解析 Markdown
#id, ,[A-Za-z][A-Za-z0-9_.:-]*;作为锚点与 Book 目标 IDnum, ,[0-9A-Za-z.-]+;注册为 Book 图目标,图注加「图 N.」前缀width/height, ,- 覆盖尺寸;静态 / 远程图靠它避免跳版
command, ,FitResizeFillCrop;必须与options同给;仅页面 / 全局资源options, ,- Hugo 图片处理选项,如
600x300、300x150 Left、800x webp q80 link, ,- 把 figure 包进链接;需要
caption或num;带链接的图不缩放 class, ,- 透传给站点 CSS
data-*/aria-*, ,- 透传
style、on*、alt、title、src 与不支持的键出现在属性行时告警并忽略;
严格发布构建拒绝这条警告。alt、title、src 属于 Markdown 图片本身。
限制与常见问题
- 图注不含 Markdown:所有公开字符串参数都是纯文本;富文本说明写在图片下方的段落中。
title不是图注:的c是悬停提示。- 处理型图片只对资源生效:
static/中的图需要处理时移到页面包或assets/。 - 构建期不下载远程图片。
- 缩放不支持拖拽、平移、上一张 / 下一张;一组相关图片使用画廊。
相关
4.3 - 代码块
代码块是普通的 Markdown 围栏,高亮由 Hugo 内置的 Chroma 在构建期完成,浏览器里没有高亮器。用于命令、配置片段与源码:围栏信息行上的 {…} 属性决定标题栏、复制行为、行号与行锚点。图示类围栏(mermaid、echarts、filetree 等)不走这条路径,它们各有渲染钩子。
最简例子
没有属性的围栏同样有完整外壳与复制按钮。无标题栏时不渲染空白横条,复制按钮浮在右上角,鼠标悬停或焦点进入块内时出现,触屏设备上始终可见。外壳不显示语言名,lexer 名字只写入 data-language,供样式表与测试使用。
语言标记就是 Chroma 的 lexer 名。diff 围栏用 Chroma 的增删行样式呈现补丁,不需要额外组件:
文件名标题
title 给块加一条可见标题栏,通常写文件名或路径。它同时成为这个块的无障碍名称。
filename 是 title 的历史别名,两个一起写时告警并使用 filename;严格发布构建
拒绝这条警告。
行号、起始行与高亮
lineNos 取 inline(行号与代码同一列)或 table(行号独立成列,可单独选中不被复制)。lineNoStart 改显示的起始编号。hl_lines 标记要强调的行,计数按围栏内的源码行,从 1 开始,与 lineNoStart 无关。
lineNos="table" 把行号放进独立的一列(两种模式下复制按钮都会剔除行号):
tabWidth 决定制表符展开成几个空格,与 style 一样原样转交 Chroma。本站使用基于 class 的 Chroma 调色板(深浅色各一套),style 只在把 Hugo 切回内联样式模式时才生效。
长行换行
wrap=true 只改变显示:源码不变,复制出来的文本也不变。不加它时长行横向滚动。
wrap=true 与表格行号不能共存:行号列与代码列是两个表格单元格,换行后会错位。
写在一起时告警并关闭换行,提示改用 lineNos="inline" 或去掉换行;严格发布构建
拒绝这条警告。
折叠长代码
collapse=N 让块初始只显示 N 行,底部给一个「显示全部 N 行」按钮。服务器输出完整代码,折叠是浏览器量出第 N 行位置后的视觉裁切:没有 JavaScript 时、读屏器中、打印时代码都是完整的。
行数不超过 collapse 时按钮不出现。换行与折叠可以一起用:折叠测量的是第 N 个源码行节点的底边,换行的行不会被截断。
复制内容
默认复制整块源码。终端会话(console 与 shell-session 两个 lexer)默认只复制命令:带提示符的行留下,提示符本身与输出行去掉。下面这个块复制出来只有两条命令,没有 $ 也没有输出。
要连提示符与输出一起复制就写 copy="all"。把 copy="command" 用在 bash、sh
之类普通 lexer 上时告警并使用 copy="all",因为它们分不出提示符、命令与输出;
严格发布构建拒绝这条警告。多行命令请在续行里写出续行提示符(通常是 >),否则
那一行会被当成输出而排除。
会话 lexer 的块里一行提示符都没有时,复制按钮报失败:图标转为错误状态,控制台留一条错误,剪贴板不变。它不会退化成复制全文。
copy=false 关掉这一块的复制按钮,用于不应被抄走的反例片段:
整站关掉复制用 params.ui.code_copy: false,它优先于每个块自己写的 copy(见配置总览)。复制按钮只有图标,成功与失败会换图标并播报本地化状态;复制内容保留缩进、空行与 Unicode,去掉行号,末尾只留一个换行。
行链接与稳定 ID
把「看第 3 行」做成链接需要两步:给围栏一个明确的 id,再打开 anchorLineNos=true。行号随即变成锚点链接,锚点是 #<id>-<行号>。
跳到 第 4 行。
不写 id 时主题也会生成一个页面内唯一的 ID,但它依赖围栏在页面里的顺序,前面
插入一个新围栏就会变。只有作者书写的 id 才是永久链接。ID 不能含空白与控制字符,
也不能与页面上其它块的 viewport、标签、面板、标题、行锚点 ID 重复;无效或重复 ID
会告警,严格发布构建拒绝这条警告。
编号例
写书或长手册时给代码片段编号:num 加 caption,这个围栏就成了一条 Book「示例」目标,可以被 xref 引用,也会进入全书的示例目录。编号由作者书写,主题不自动计数;id 默认是 eg-<num>。
参见 示例 4-1。
num 与 caption 必须成对出现。只写 caption 时忽略它,只写编号时丢弃编号并告警;
严格发布构建拒绝这条警告。num 与标签页属性 tab 互斥。图、表、公式的编号写法与
索引见书籍出版。
一组围栏做成标签页
连续几个带 tab 的围栏会在浏览器里合成一个标签页集,第一个围栏上的 group 让它可分享、可同步、可记住选择。
完整规则(分组语法、URL hash、跨组同步、正文标签页)在标签页。
易错写法
- 在文档里展示 shortcode:围栏不阻止 Hugo 解析,写在代码块里的
{{< tabs >}}仍会执行。要让它原样显示,在两侧定界符的内侧各加一对注释符号,写成{{</* tabs */>}},百分号形式对应{{%/* steps */%}}。本页每一处展示 shortcode 的地方都是这么写的。 - 围栏里套围栏:外层用四个反引号、内层三个,本页每一段「源码」都是这么写的;内层还有围栏时外层再加一个。
- 属性写在信息行上:围栏的属性跟在开栏那一行的语言后面,表格与图片的属性才写在块的下一行。写到下一行会变成正文里一段可见的花括号。
- 未知、不安全与主题保留属性在普通预览中告警并忽略,消息列出允许的名字;严格 发布构建拒绝每条此类警告。
- 列表项里的围栏:缩进要与列表项内容对齐(
1.之后恒定三个空格),否则围栏会脱离列表。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-code"> 外壳 + Chroma 的 .highlight/.chroma;复制、折叠按钮在服务器输出里是 hidden,脚本确认可用后才显示 |
| 打印 | 完整代码,去掉复制、折叠、渐隐;长块允许跨页;标题栏保留 |
| Markdown | 原样输出源码围栏,连 {…} 属性一起 |
| RSS | 静态代码块,无按钮 |
没有复制或折叠控件的页面不加载 code-block.js;打印、Markdown 与 RSS 输出不加载。
参数参考
开栏那一行、语言之后的 {…} 里,OINK 自己的属性:
title, ,- 可见标题栏(通常是文件名),同时是无障碍名称
filename, ,title的历史别名;两者同时出现时告警并使用filenamecopy, ,true等价于all;command只允许console/shell-sessionwrap, ,- 视觉换行,不改源码;与表格行号互斥
collapse, ,- 初始显示的最大行数;行数不足时不生效
label, ,- 无障碍名称,不显示在页面上;与
aria-label互斥 id, ,- 稳定的块 ID 与行锚点前缀;不能含空白
tab, ,- 标签名,见标签页;与
num互斥 group, ,- 写在一组的第一个围栏上,启用 hash / 同步 / 持久化;需要
tab value, ,- 分组内每个围栏必填,无分组时禁止;需要
tab num, ,- 编号示例(Book
eg);必须与caption同时出现 caption, ,- 编号示例的说明;必须与
num同时出现 class, ,- 追加到
.td-code根元素 data-*/aria-*/role, ,- 透传到根元素
title、filename 与 label 已经为块生成了无障碍名称与 role="group"。它们中的
任意一个与 aria-label、aria-labelledby 或 role 同时出现时告警并忽略冲突属性;
严格发布构建拒绝这条警告。这三个属性只在块没有标题也没有 label 时可以透传。
同一行还能写 Chroma 选项,主题原样转交 Hugo:
lineNos, ,- 行号形态;
table与wrap=true互斥 lineNoStart, ,- 显示的起始行号,不影响
hl_lines的计数 hl_lines, ,- 如
"2 4-5",按围栏内源码行计数 anchorLineNos, ,- 行号变成锚点链接,前缀取自块的
id tabWidth, ,- 制表符展开的空格数
限制与常见问题
- 不换高亮器:没有 Shiki、Twoslash、浏览器端高亮,也没有可执行的代码演练场。补丁用
diff围栏,Chroma 的.gi/.gd就是增删行的样式。 copy="command"只认会话 lexer:写在别的语言上是构建错误,不会退化成复制全部。- 自动生成的 ID 不是永久链接:要发链接就写
id。 mermaid、math、chem、markmap、plantuml、echarts、infographic、checksums、filetree、gallery不是代码块:它们有各自的渲染钩子,不套这层外壳,也没有复制按钮。
相关
4.4 - 标签页
{tab=} 属性就得到标签页;加上 group 之后可分享链接、跨组同步、记住读者的选择。标签页并列等价的几种写法:包管理器、发行版、YAML / TOML / JSON、环境变量与配置项。有先后的步骤、互不相关的内容不适合标签页,读者一次只看见其中一个。
原生形态是给相邻的块加 tab 属性。正文(多个段落、列表、提示块)要做成标签页时才用 tabs/tab shortcode。两种形态共用一个运行时、一套 DOM 与一样的键盘行为。
最简例子
连着写两个带 tab 的围栏,中间只隔空行。
服务器输出两个带标题的代码块,没有面板被隐藏;页面加载后运行时把相邻的同类块重组为标签页。在 GitHub 上、打印时、关闭 JavaScript 时,读者看到的是连续两块完整内容。
分组:链接、同步与记忆
只在第一个块上写 group,这一组就有了公开的 URL hash #<group>-<value>、页内同步与浏览器持久化;分组内的每个块都要写 value。
value 是机器值(^[a-z0-9][a-z0-9_-]*$),tab 是给人看的标签名,两者互不相干。上面这组的 pnpm 面板对应的 hash 是 #pkgmgr-pnpm,带这个 hash 访问本页会直接选中它。
同组联动
下面这组用了同一个 group="pkgmgr"。在上面那组切换包管理器,这组会跟着切;在这组切换,上面那组也跟着切。选择写入 localStorage 的 td-tabs:v1:pkgmgr 键,在其它页面同组的标签页上仍然生效。
这组没有 yarn 面板。同步时缺哪个值就保持不动,不会出现「一组没有选中项」的状态。初始选哪个的优先级是:URL hash,存储的值,shortcode 的 default 或第一个块,第一个标签。带 hash 打开页面只切换,不覆盖读者已经存下的偏好。
表格也能做标签页
同一套属性写在表格的属性行上,连着的表格就组成一组标签页。
| 参数 | 默认值 |
|---|---|
shared_buffers |
25% RAM |
max_connections |
100 |
| 参数 | 默认值 |
|---|---|
shared_buffers |
128MB |
max_connections |
100 |
围栏与表格是两种块类型,相邻也不会合成同一组:一组标签页里只能全是围栏或全是表格。两者混排使用下面的 shortcode 形态。
标签名与文件名共存
围栏的 tab 和 title 可以一起写:标签名进标签栏,文件名标题栏留在面板里。
单独一个块只是带标题的块
一个块要凑够两个相邻的同类块才会变成标签页。落单的块保留标题,不会变成只有一个标签的标签栏。
块之间只允许空行。三种情况会断开一组:中间隔了正文(段落、标题、列表都算);中间有一条 HTML 注释,<!-- prettier-ignore-end --> 是常见的一处;后一个块自己写了 group,一组里只有第一个块可以带 group。
正文标签页
面板里要放段落、列表、提示块或多个块时,用 tabs/tab shortcode。正文是完整的 Markdown。
仓库自带 .github/workflows/,推到 main 就会构建并发布。
baseURL 要写成仓库的 Pages 地址。
在 Cloudflare 控制台里连接仓库,构建命令:
default 指定初始选中的面板,它必须是某个子项的 value,并且需要 group。没有 group 时不能写 value,主题自动生成 tab1、tab2 等值,这组标签页只在本地切换,不动 URL 也不写存储。shortcode 形态比属性形态严格:写错的地方在构建期就报出来,不留到浏览器里。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-tabs"> + role="tablist" 的按钮与面板;运行时接管前所有面板都可见 |
| 打印 | 连续的带标题静态分节,没有标签栏 |
| Markdown | 围栏形态保持源码围栏(含 {tab=} 属性);shortcode 形态输出 **标签名** 加正文 |
| RSS | 与打印相同,堆叠的带标题分节 |
只有用到标签页的页面才加载 tabs.js;打印、Markdown 与 RSS 输出不加载。
参数参考
写在围栏信息行或表格属性行上的属性:
tab, ,- 可见标签名;单独出现时就是这个块的标题
group, ,- 写在一组的第一个块上,启用 hash、页内同步与持久化;需要
tab value, ,- 分组内每个块必填,无分组时禁止;需要
tab
tabs shortcode:
group, ,- 同上,启用 hash、同步与持久化
default, ,- 初始选中的面板;需要
group label, ,- 标签栏的无障碍名称,不显示在页面上
tab shortcode:
label, , required- 可见标签名
value, , required- 无分组时禁止书写,自动生成
tab1、tab2等值
行为约定:面板 ID 在分组里是 <group>-<value>,同一页出现第二组同名 group 时后续各组的 ID 加 -2、-3 后缀(深链目标始终是第一组),未分组时由主题生成;存储键是 td-tabs:v1:<group>;用户点击或按键会用 replaceState 更新 hash 并写入存储,带 hash 访问只切换不写入。键盘上左右方向键(感知 RTL)与 Home/End 移动并激活标签,焦点停留在标签上。
限制与常见问题
- 无效分组与组合会在 Hugo 构建中告警并采用安全回退:丢弃不可用的 group/value/default、忽略夹杂正文、保留后出现的重复项,或不渲染空集合。严格发布 构建拒绝每条警告,消息带源码位置。
- 属性形态没有可用
value时失去同步能力,只保留本地标签页;分组不会静默编造身份。 - 围栏与表格不会混成一组,正文与代码混排请用 shortcode 形态。
- 标签页不是折叠块。只想收起长输出用
> [!DETAILS](见提示块)。 - 同名
group是全站共享的:读者在 A 页选了 pnpm,B 页同组的标签页也会是 pnpm。这是它的用途,也意味着group名要按含义取,不用tabs1这种。
相关
4.5 - 表格
表格是普通的 GFM 管道表格。主题的表格渲染钩子把每张表包进一块可横向滚动的区域,表格下面那一行 {…} 属性决定它是哪一种表:带标题的表、兼容矩阵、参数表、编号表或标签页。合并单元格、排序与筛选不在能力范围内,需要它们的场景请改换呈现方式。
最简例子
不写属性行就是一张普通表。对齐方式照旧来自分隔行,表头单元格是 th scope="col"。
| 组件 | 端口 | 用途 |
|---|---|---|
| PostgreSQL | 5432 | 数据库 |
| Pgbouncer | 6432 | 连接池 |
| Patroni | 8008 | 高可用编排 |
宽表格自己滚动
列太多的表不会把页面撑宽,它在自己的区域里横向滚动。这块区域可以用键盘聚焦:Tab 停入后方向键滚动,无障碍名称是本地化的「可横向滚动的表格」。
| 集群 | 角色 | 版本 | 状态 | 延迟 | 连接数 | 大小 | 备份 |
|---|---|---|---|---|---|---|---|
| pg-meta | primary | 18.1 | running | — | 42 | 12 GB | 2026-08-17 |
| pg-test | replica | 18.1 | streaming | 12 ms | 8 | 12 GB | 2026-08-17 |
表格标题
{caption="…"} 加一个可见的 <caption>,纯文本,不给表编号。
| 条目 | 取值 |
|---|---|
| 主题版本 | v0.8.1 |
| Hugo 下限 | 0.160.1 Extended |
| 许可证 | Apache-2.0 |
兼容矩阵
{.matrix} 用于「行 × 列 = 支持与否」的对照表:第一列成为行表头(th scope="row"),滚动时表头行与第一列吸附不动,其余单元格居中,分隔行另有对齐时以分隔行为准。✅ 与 ❌ 是作者写的字符,主题不解析它们。
| OS / PG | PG18 | PG17 | PG16 | PG15 | PG14 |
|---|---|---|---|---|---|
| EL 9 | ✅ | ✅ | ✅ | ✅ | ✅ |
| EL 8 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Debian 13 | ✅ | ✅ | ✅ | ❌ | ❌ |
| Ubuntu 24.04 | ✅ | ✅ | ✅ | ✅ | ❌ |
用整个画布
{.full-width} 让表格越出正文栏宽,占满文章可用的宽度。适合列多但每列都短的表。
| 语言 | 代码 | 侧栏 | 搜索 | 目录 | 打印 | 状态 |
|---|---|---|---|---|---|---|
| 简体中文 | zh |
✅ | ✅ | ✅ | ✅ | 已审校 |
| English | en |
✅ | ✅ | ✅ | ✅ | 已审校 |
参数表
{.fields} 把表格变成定义列表:第一列是名称,最后一列是说明,中间列是元数据。它是记录配置项、命令参数、API 字段的形态,写法见参数表。
offline_search, ,- 构建本地搜索索引
page_width, ,- 正文栏宽度
编号表
写书或长手册时给表编号:num 加可选的 #id 与 caption。表格会被包进一个带本地化「表 N.」标签的 <figure>,并注册成 Book 目标,可以被 xref 引用、进入全书表格目录。编号由作者书写,主题不自动计数;id 缺省是 tbl-<num>。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读已提交 | 否 | 是 | 是 |
| 可重复读 | 否 | 否 | 是 |
| 可串行化 | 否 | 否 | 否 |
参见 表 9-1。
表格做成标签页
连着的表格加 {tab="…"} 就组成一组标签页,规则与相邻围栏一致:第一张表上的 group 启用 hash、同步与持久化,此后每张表都要 value。完整规则见标签页。
| 目录 | 内容 |
|---|---|
content/ |
页面 |
data/ |
首页与发布数据 |
| 目录 | 内容 |
|---|---|
assets/ |
SCSS 与图片资源 |
static/ |
原样拷贝的文件 |
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-table-scroll"> 可聚焦滚动区 + <table>;矩阵与全宽是这个包装器上的修饰 class |
| 打印 | 完整表格按页宽排版;包装器仍在,但标成 td-table-scroll--static,不再是可聚焦视口 |
| Markdown | 原样输出源码表格与属性行 |
| RSS | 完整静态表格 |
表格不加载任何脚本。
参数参考
表格下一行的属性行:
.full-width, ,- 越出正文栏宽,占满文章画布
.matrix, ,- 第一列作行表头,表头与首列吸附,其余单元格居中
.fields, ,- 渲染成定义列表,见参数表
caption, ,- 可见表格标题;在
.fields上是列表的标签 meta, ,- 命名
.fields中间列的语义,取值typerequireddefault-;必须与.fields同用 #id, ,[A-Za-z][A-Za-z0-9_.:-]*;写在<table>(编号表则写在<figure>)上num, ,[0-9A-Za-z.-]+;注册为 Book 表目标,标题前加「表 N.」tab/group/value, ,- 相邻表格组成标签页
class, ,- 站点 CSS 用,原样留在
<table>上 data-*/aria-*, ,- 透传
style、on* 与其它键会告警并忽略;严格发布构建拒绝这条警告。
限制与常见问题
- 互斥规则:
.fields不能和.matrix、.full-width或num一起用;num与tab互斥;group/value需要tab;meta需要.fields。 - 属性行必须紧贴表格:中间空一行,它就变成正文里一段可见的花括号。Markdown 格式化工具常移动这一行,把它包进
<!-- prettier-ignore-start -->/<!-- prettier-ignore-end -->。 - 没有合并单元格、没有排序、没有筛选:GFM 管道表格能表达的就是全部。需要合并表头的复杂表请拆成两张表或改成一张矩阵。
- 单元格里放不下块内容:多段说明、列表、围栏要用
fields/fieldshortcode。 .matrix的居中由 CSS 实现:分隔行里写了对齐就以分隔行为准。
相关
4.6 - 参数表
{.fields} 记录配置项、命令参数与 API 字段:名称、类型、默认值、说明各就各位,窄屏不挤,每条都能单独链接。参数表(Fields)把「一串具名值 + 元数据 + 说明」渲染成响应式定义列表:名称独占一行,类型、是否必填、默认值是名称旁边的小字,说明另起一行,每一条自带锚点。用于配置项、命令参数与 API 字段。要按同一批列横向比较很多行时用普通表格,内容是操作顺序时用步骤。
写法有两种:普通表格加 {.fields}(默认选它),以及 fields/field shortcode(说明需要多个段落、列表或代码块时才用)。两种形态渲染出相同的条目。
最简例子
一张至少两列的管道表格,下一行写 {.fields}。第一列是名称,最后一列是说明,中间每一列都是元数据,标签就是表头文字本身。
offline_search, ,- 构建本地搜索索引并启用命令面板
offline_search_max_results, ,- 搜索结果条数上限
page_width, ,- 正文栏宽度,可选
narrownormalwide
这里的元数据显示成「表头: 值」。主题不推断表头的含义,类型 只是一个标签;要让它变成标准芯片见下一节。单元格接受行内 Markdown(代码、强调、链接),空的中间单元格省略。
语义列 meta=
meta 按顺序说明每一个中间列扮演什么角色:type(类型)、required(必填)、default(默认值),或者 -(保留表头当标签)。有了它,表格形态渲染出的芯片与 shortcode 形态一致。
baseURL, , required- 站点地址,含子路径
title, , required- 站点名,出现在顶栏与页签
defaultContentLanguage, ,- 默认语言,决定无前缀路径属于哪种语言
规则:
meta应为每一个中间列写一个角色,个数等于总列数减二;写多写少时告警并忽略meta,严格发布构建拒绝这条警告。required列是「非空即真」:单元格里写「是」「yes」「✔」都一样,渲染出来的是不翻译的required芯片;留空就不显示。type与default单元格如果本身没有行内标记,会自动套上代码格式,与 shortcode 形态对齐。- 三种语义芯片按
type、required、default的顺序显示,与列的顺序无关;-列跟在后面,按列顺序排。
- 可以和语义角色混用,用来保留一列自定义标签:
HUGO_MODULE_WORKSPACE, ,- 指向
go.work,让主题从本地 checkout 解析 HUGO_ENV, ,- 设为
production时启用压缩与指纹
标签与容器 ID
caption 给整张表加一个可见标签(同时是无障碍名称),id 命名外层容器,方便从别处链接过来或写站点 CSS。
params.ui.image_zoom
enable, ,- 打开图片缩放
selector, ,- 扫描候选图片的根选择器
每一条都能单独链接
每个条目获得一个 field-<名称> 形式的锚点,鼠标移上去时名称右边出现自链接图标。上面第一张表里的 page_width 就是 #field-page_width,回答问题时可以把这一行的链接单独发出去。
同一页里重名的字段按 -2、-3 顺延,规则与 Goldmark 处理重名标题一致。锚点只在 HTML 里生成:打印和 RSS 会把很多页拼成一个文档,页内锚点在那里会冲突。
shortcode 形态
说明需要多个段落、列表或代码块时,表格单元格装不下,改用 fields/field:
pig 命令常用参数
--config, , required配置文件路径。相对路径按当前工作目录解析。
如果同时设置了
PIG_CONFIG环境变量,命令行参数优先。--log-level, ,日志级别,从低到高:
debug:打印每一次远程调用info:默认值error:只在失败时输出
--dry-run, ,只打印将要执行的动作,不改任何东西:
required=true 与 default=false 是布尔值,不加引号。default 接受任何标量:default=0、default="" 都会如实显示(空字符串显示成 ""),不写 default 就不显示这一项。每个 field 必须有非空正文,并且必须是 fields 的直接子项。
两种形态的选择
| 情况 | 用法 |
|---|---|
| 每条说明一句话,能放进表格单元格 | 表格 + {.fields} |
| 说明要分段、带列表或代码块 | fields/field shortcode |
| 读者需要按同一批列横向比较很多行 | 用普通表格,不转成参数表 |
| 内容是操作顺序 | 用步骤 |
表格形态在 GitHub 上仍然是一张可读的表,OINK 的 Markdown 输出也保持表格原样,这是默认选它的理由。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-fields"> + 语义 <dl>;条目带 #field-<名称> 锚点与自链接 |
| 打印 | 完整定义列表,不带条目锚点 |
| Markdown | 表格形态保留源码表格;shortcode 形态输出「名称 — 类型;required;default: 值」加缩进说明的项目符号列表 |
| RSS | 完整静态 <dl>,不带条目锚点 |
不加载任何脚本。
参数参考
表格属性行(写在表格下一行):
.fields, ,- 必需;把表格渲染成参数表
meta, ,- 空格分隔,取值
typerequireddefault-;个数等于中间列数;语义角色不可重复 caption, ,- 可见标签,同时是列表的无障碍名称
id, ,- 外层容器的 ID
class, ,- 透传给站点 CSS
data-*/aria-*, ,- 透传
fields shortcode:
label, , required- 可见标签,作用同表格的
caption id, , required- 外层容器 ID;不能含空白、引号、
<、>、& class/data-*/aria-*, , required- 与表格属性行同一套策略
field shortcode:
name, , required- 字段名
type, , required- 类型标签,如
booleanstring[]duration required, , requiredtrue时显示不翻译的required芯片,默认falsedefault, , required- 字符串 / 布尔 / 整数 / 浮点;
false、0、""都会显示
限制与常见问题
- 第一列必须非空,且在同一张表内唯一:重名或空名时告警并跳过该行,严格发布构建 拒绝这条警告。
.fields不能与.matrix、.full-width、num组合,meta不能用在没有.fields的表上。- 表格单元格里放不下块内容:需要段落、列表、围栏就换 shortcode 形态。
required与default是不翻译的 API 词汇,在所有语言下都显示英文,它们是契约词,不是界面文案。- 暂不支持
kind、since、deprecated、location、字段级链接与嵌套结构,也不会在构建时解析 TypeScript 或 OpenAPI schema。
相关
4.7 - 步骤
{.steps} 就是带编号圆点与竖线的操作步骤;步骤要带标题、要进目录时改用 steps shortcode。步骤(Steps)是带编号圆点与竖线的有序列表:一个普通有序列表,加一行 {.steps} 标记,编号圆点与串起它们的竖线由 CSS 绘制,不加载脚本。用于有先后的操作流程。并列而无先后的内容用普通列表或卡片。
写法有两种:有序列表加 {.steps}(默认选它),以及 {{% steps %}} shortcode,每一步要有自己的标题、标题还要进右侧目录时用它。
最简例子
每一项都写 1.,让 Markdown 自己数。这样插入、删除、调换步骤都不用手改编号,而且内容缩进恒定是三个空格。
- 安装 Hugo Extended
- 克隆 OINK Starter
- 启动本地预览
{.steps} 必须紧贴列表最后一行,中间空一行它就会变成正文里一段可见的花括号。
步骤内容
列表项里可以放任何块级内容:段落、代码围栏、提示块、表格、嵌套列表、图片。缩进对齐到列表项的内容列(三个空格)即可。
-
克隆 OINK Starter,它是面向项目的精简模板。
-
启动本地服务器。
说明首次构建会通过 Go 模块代理拉取主题,需要本机安装 Go。
-
替换三处内容,它就是你的站点。
位置 替换为 hugo.yml的title你的站名 hugo.yml的baseURL你的域名 content/你的内容
{{< … >}} 形式的 shortcode(标签页、卡片、徽章等)也可以写在列表项里;{{% … %}} 形式不行,见下面的限制。
一步里按平台分开
某一步在不同平台上命令不同时,把带 {tab=} 的围栏并排写进那个列表项,它们照样会合成标签页。
-
安装 Hugo Extended。
-
安装依赖:
EL / RHELDebian / Ubuntu -
运行
hugo server预览。
接着上一组往下编号
正文隔断了一组步骤时,把新一组的第一项写成它实际的序号,Markdown 会输出 start,编号从那里继续(支持到 40)。
- 配置
baseURL与部署工作流。 - 推送到
main,等待 GitHub Actions 构建完成。
带标题的步骤
步骤本身很长、每一步该有个能被链接和被目录收录的标题时,用 {{% steps %}}:它的正文是页面级 Markdown,里面的每一个直接子标题就是一步,正文不用缩进。下面三步的标题就在这一页的右侧目录里。
安装工具链
需要 Hugo Extended ≥ 0.160.1 与 Go。
启动服务器
brew install hugo go
sudo apt install hugo golang-go
发布
推送到 main,仓库自带的工作流会构建并发布。
它是主题里唯一的 {{% … %}} shortcode。百分号形式的正文交给 Goldmark 当页面级 Markdown 处理:只有这样,里面的标题才能进目录,里面才能放 tabs、cards、fields 这些容器 shortcode。代价是它自己不能嵌进列表项,也不能嵌进另一个百分号容器。
同一组步骤的标题保持同一层级,不要把一个 steps 套进另一个里。
两种形态的选择
| 情况 | 用法 |
|---|---|
| 步骤是一两句话加一段命令 | 有序列表 + {.steps} |
| 每一步需要标题、需要被链接、需要进目录 | {{% steps %}} |
步骤里要放 tabs、cards、fields 容器 |
{{% steps %}} |
| 步骤本身要嵌在另一个列表项里 | 有序列表 + {.steps} |
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 原生形态是 <ol class="steps">,编号与竖线由 CSS 画;shortcode 形态是 <div class="td-steps"> 加各级标题 |
| 打印 | 编号与内容照旧,竖线保留 |
| Markdown | 原样输出源码:有序列表加 {.steps},或标题加正文 |
| RSS | 静态列表 / 标题分节 |
不加载脚本;关闭 JavaScript 后呈现不变。
参数参考
两种形态都没有参数,只有写法约定:
{.steps},- 必需;写在无序列表上不生效
1.,- 让 Markdown 自己数;内容缩进恒为三个空格
,4.(首项)- 输出
<ol start="4">,编号从 4 接着走,支持 2–40 {{% steps %}},- 直接子标题(
##–######)就是步骤;正文不缩进
限制与常见问题
- 列表项里不能写
{{% … %}}:百分号 shortcode 的多行输出会把列表截断。要在步骤里放容器就整组改用 shortcode 形态。 {{% steps %}}不能放进列表项,也不能套在另一个百分号容器里。- 标记要紧贴列表:
{.steps}与列表之间不能有空行;经过 Prettier 之类的格式化工具时,把它包进<!-- prettier-ignore-start -->/<!-- prettier-ignore-end -->。 {.steps}只对有序列表有效:写在-开头的无序列表上不会有编号。- 步骤不折叠、不记进度:没有「已完成」状态,也没有展开收起。
相关
4.8 - 卡片
{.cards} 的链接列表排出导航卡片网格;需要图标、徽章、图片时改用 shortcode。卡片(Cards)是一组并列的链接:每张卡片一个链接标题加一句描述,网格随容器宽度自适应。适合栏目首页、「接下来读什么」与几条并列路径的入口。不适合排版正文段落(用普通段落)或做图片墙(用画廊)。
最简例子
带 {.cards} 的链接列表就是卡片。链接是标题,— 之后是描述。
整张卡片是点击热区,不只是标题文字。没有 columns 参数:列数由容器宽度决定,窄屏收成一列。
只有标题的卡片
描述可以省略。一行一个链接,{.cards} 收尾。
松散列表与多段描述
一句话装不下时改用松散列表:链接单独一段,描述另起一段,列表项之间空一行。标题独占一行,描述在标题下方。{.cards} 仍然紧贴最后一段,中间 不能有空行。
图标与徽章
链接列表不支持图标、徽章、图片与多段描述,这些用 cards / card shortcode。icon 是恰好一对 Font Awesome class,badge 是一段纯文本。
图标不是一对有效的 Font Awesome class 时,普通预览告警并丢弃图标;严格发布构建 拒绝这条警告。
Markdown 正文
card 的正文按页面级 Markdown 渲染:行内代码、强调、链接、列表都可以。title、badge 这些参数是纯文本,不解析 Markdown。
hugo mod get github.com/pgsty/oink。推荐方式,升级只需改一行版本号。
无需安装 Go:
git submodule add- 主题落在
themes/oink
不写 link 的卡片渲染成加粗标题,不生成链接。
带图片的卡片
image 与  的解析顺序一致:页面资源 → 全局资源 assets/ → 静态路径 /images/… → 远程 URL。本地资源带上固有尺寸,避免加载跳版。
image 需要一个替代文字来源:image_alt="…"(有信息的图)或
decorative=true(纯装饰)。两个都写时告警并保留 alt;两个都不写时告警并按装饰图
渲染。严格发布构建会拒绝任一警告。
卡片图片不参与图片缩放,整张卡片本身已经是链接。
栏目首页的自动卡片
栏目首页(_index.md)不需要手写卡片列表:主题读子页的 title、description、icon 自动生成一组卡片。本站在 hugo.yml 中全局启用:
单个栏目可以在自己的 front matter 里覆盖,也可以用 cascade 把选择推给整棵子树:
自动卡片与手写卡片使用同一套 td-content-card 样式,区别只在数据来源。栏目首页不要手写子页清单:手写清单会与侧栏不同步。要排的内容不是本栏目的子页时(例如混合站外链接、跨栏目推荐),才在正文里手写卡片。相关键的完整定义见配置总览。
两种形态的选择
| 你要的 | 用哪种 |
|---|---|
| 一句话描述的链接网格 | {.cards} 链接列表 |
| 图标、徽章、图片 | cards / card shortcode |
| 描述里要列表、代码、多段 | cards / card shortcode |
| 没有链接的卡片 | cards / card shortcode |
| 本栏目的子页 | 什么都不写,靠 section_index: cards |
链接列表在 GitHub 上仍是一个链接列表,shortcode 不是。能用原生形态时用原生形态。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 原生形态是 <ul class="cards">;shortcode 形态是 <div class="td-content-cards"> + 每张 <article class="td-content-card">。两者都是纯 CSS 网格,不加载脚本 |
| 打印 | 原生形态竖排,shortcode 形态收成两列;两者的单张卡片都避免跨页断开 |
| Markdown | 原生形态原样输出链接列表;shortcode 形态输出 - [标题](链接) (徽章) — 描述 |
| RSS | 与 HTML 同样的标记(没有站点 CSS 时是一份可读的链接清单) |
参数参考
原生形态:
{.cards}, ,- 写在无序列表 之后 的一行;只对无序列表生效
列表项首个链接, ,- 卡片标题,同时是整张卡片的点击目标
其余内容, ,- 描述。紧凑列表里跟在
—后面,松散列表里另起一段
card 的参数(cards 自身不接受任何参数):
title, ,- 必填,非空。卡片标题
link, ,- 站内路径、相对路径、
http(s):、mailto:;外链自动加rel="noopener" icon, ,- 例如
fa-solid fa-rocket;格式不符时告警并丢弃 badge, ,- 标题右侧的小标签
image, ,- 页面资源 / 全局资源 / 静态路径 / 远程 URL
image_alt, ,- 有
image时与decorative二选一 decorative, ,true表示装饰图,输出空 alt正文, ,- 卡片描述
没有 cols、columns、accent、desc、color 参数。未知参数在普通预览中告警
并忽略;严格发布构建拒绝这条警告。
限制与常见问题
{.cards}只认无序列表:有序列表加了这个标记不会变成卡片。{.cards}必须紧贴列表:中间空一行、或缩进进列表项,标记被静默丢弃,构建不报错,列表仍是列表。渲染结果不是卡片时先检查这一行。card只能待在cards里:单独使用、或放进别的 shortcode 时告警并跳过;严格 发布构建拒绝这条警告。- 列数不可配:网格按容器宽度自适应,只有栏目首页的自动卡片能用
params.ui.section_index_columns指定列数。 - 卡片不放长文:描述超过两行时改用正文段落或提示块。
相关
4.9 - 文件树
filetree 围栏画带注释的目录结构:对齐的注释列、逐条目图标、可折叠目录、可拖动的分栏。文件树(FileTree)是一个 filetree 围栏,围栏正文就是目录清单:缩进表示层级,结尾的 / 表示目录,# 之后是注释。适合解释一份目录结构里与读者有关的那部分,并逐条加上说明。需要读者逐字复制的清单用普通代码块。
最简例子
- content/
- _index.zh.md
- docs/
- blog/
- hugo.yml
- go.mod
项目符号(-、*、+)可以省略,效果相同。有子项的条目是目录;没有子项时,结尾的 / 告诉主题它是目录。
加注释
每行第一个前面带空白的 # 之后是注释,渲染成对齐的右列。注释是纯文本,里面的 Markdown 按字面显示;要一个字面井号就写 \#。
- content/全部页面,中英双语同目录
- docs/你正在读的这棵文档树
- blog/发布说明与文章
- assets/scss/站点自己的 SCSS,覆盖主题变量
- layouts/站点级模板覆盖,越少越好
- static/images/不需要构建期处理的图
- hugo.yml站点配置:语言、菜单、params.ui
注释列的起点在构建期算出,由最宽的一行决定,因此每行的 # 从同一列开始,与源码里是否对齐无关。注释列最多占面板的右半边,最少占三成。中间的虚线是分隔条,可以拖动,也可以用 Tab 聚焦后按方向键调整(Home / End 到两端)。
过长的名称与注释各自在本列内用省略号截断,鼠标悬停时由 title 提示完整文本。分隔条是文件树唯一的 JavaScript,只有 带注释 的树才加载它。
两列都发生截断
- runbooks/
- a-deliberately-long-runbook-filename-for-a-failover-drill.md同样超长的注释,写在一行里,因此必须在注释列内截断
- restart.md短名字
标题栏
围栏属性 {title="…"} 在树上方渲染一条标题栏;不写时没有标题栏。
oink.pgsty.com 仓库根目录
- content/页面
- assets/参与构建的资源
- data/首页、Landing、下载页的数据
- layouts/模板覆盖
- static/原样拷贝的文件
- tests/Playwright 与 node --test
- hugo.yml
- go.mod用 Hugo Module 引入主题
- Makefilemake d / make b / make c
缩进与层级
层级由缩进决定。两个空格、四个空格、制表符(按四列计算)都可以,同一棵树内不要求统一,条件是每次退回的层级此前已经打开过。tree 命令的输出可以整段粘贴,包括开头的根目录行与结尾的统计行,统计行会被丢弃。
- content/docs
- about
- _index.zh.md
- features.zh.md
- components
- filetree.zh.md
- image
- index.zh.md
- _index.zh.md
- about
退回到未打开过的缩进层级时告警并跳过该行;消息带围栏内的行号,严格发布构建 拒绝这条警告。
折叠与显式类型
有子项的目录默认展开,{open=false} 使其初始收起。目录用原生 <details> 渲染,键盘可操作,不需要 JavaScript。open 只能写在目录上。没有子项、名字也不以 / 结尾的条目按文件处理,{type=dir} 覆盖这个判断,{type=file} 同理。
内容目录
- content/
- docs/新文档树
- components/22 个组件页
- callout.zh.md
- filetree.zh.md
- image/页面包:正文 + 图
- customize/站点级配置
- config.zh.md
- components/22 个组件页
- blog/
- release.zh.md
- docs/新文档树
图标与配色
图标默认按名字推断:目录用文件夹图标,随开合切换;文件先按完整文件名匹配(LICENSE、Makefile、go.mod、package.json、.gitignore 等),再按扩展名匹配(md yml toml json sh py go js sql css png svg pdf zip 等),都不匹配时用普通文件图标。
{icon=…} 覆盖它,取值是恰好一对 Font Awesome class。{tone=…} 给图标上色,取值与徽章相同:neutral info success warning danger。
部署目录:权限与要点
- /etc/pigsty/0755 root:root · 配置根目录
- pigsty.yml0644 root:root · 集群清单
- ca/0700 root:root · 自签 CA,不要提交进 Git
- ca.key0600 root:root
- /var/lib/pgsql/18/data/0700 postgres:postgres · 数据目录
- postgresql.conf0600 postgres:postgres
- /usr/bin/pig0755 root:root · 命令行工具
tone 只给图标上色,不改文字。颜色是补充,含义写在名字或注释里。
条目链接
条目名写成 [名字](链接) 即为链接。站内路径、相对路径、http(s): 都可以,URL 校验与其它组件是同一套。
本站的组件页
- content/docs/components/
- callout.zh.md提示块
- filetree.zh.md当前页面
- gallery.zh.md画廊
- image/页面包
- hugo.yml站点配置(GitHub)
按平台分成标签页
围栏带 tab=(以及 group= value=)时成为一组标签页中的一页,可以与代码围栏混排。
- /etc/pigsty/配置
- /var/lib/pgsql/数据
- /usr/bin/pig可执行文件
- ~/Library/Application Support/pigsty/配置
- /opt/homebrew/bin/pig可执行文件
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-filetree">,可选标题栏,目录是原生 <details>;带注释时多一条可拖动分隔条(唯一的运行时) |
| 打印 | 同一棵树,全部展开,没有分隔条,注释换行不截断 |
| Markdown | 原样输出 filetree 围栏 |
| RSS | 围栏源码放进 <pre> |
窄屏(小于 sm 断点)时布局收成单列:注释移到名称下方,不再截断,分隔条隐藏。不带注释的树是单列,也不加载任何脚本。
参数参考
围栏属性(写在 ```filetree 后面):
title, ,- 树上方的标题栏;不写就不画;不能为空
tab, ,- 让这棵树成为一个标签页
group/value, ,- 标签页分组与同步值;必须与
tab同时出现 class, ,- 透传给站点 CSS
条目属性(写在每行末尾的 {…} 里):
icon, ,- 例如
fa-solid fa-lock;格式不符时告警并使用默认图标 tone, ,neutralinfosuccesswarningdanger,只给图标上色open, ,- 仅目录;
false表示初始收起 type, ,dir或file,覆盖自动判断
行语法本身:
缩进- 两个空格 / 四个空格 / 制表符 /
tree的│ ├── └──连线都行 - name- 项目符号可省略;
-*+等价 name/- 结尾斜杠表示目录;名字原样渲染,斜杠保留
[name](url)- 带链接的条目
# 注释- 第一个前面带空白的
#之后的内容;\#是字面井号 N directories, M filestree的统计行,自动丢弃
未知属性、未知取值、写在文件上的 open、格式错误的 {…}、退回到未打开过的
缩进层级,都会告警并采用安全回退或跳过坏行,消息给出围栏内行号;严格发布构建
拒绝这些警告。
限制与常见问题
- 只有
filetree围栏这一种形态:没有{.filetree}列表标记,也没有 shortcode。 - 注释与名字都是纯文本:写
**粗体**会原样显示,围栏源码在任何环境里都读得通。 - 不读取磁盘:树是手写或粘贴的静态内容,不随仓库变化。
- 不提供搜索、多选、复制整棵树:需要逐字复制时用普通代码块。
- 分栏宽度不持久化:拖动过的位置刷新后回到构建期算出的默认值。
相关
4.10 - 公式
公式由 KaTeX 在构建期渲染成 HTML + MathML,页面只额外加载一份本地 KaTeX 样式表,没有 JavaScript,也不请求远程数学服务。行内公式写 \(…\),块级公式写 $$…$$、\[…\],另有 math 与 chem 两种围栏。需要 TikZ 绘图或 KaTeX 不支持的宏包时,改用预渲染的图片。
最简例子
行内公式写在句子中,前后的空格与标点留在分隔符外面。
共享缓冲区命中率是 ,其中 是 blks_hit, 是 blks_read。
块级公式
独占一段的公式用 $$ 包起来,居中显示,字号更大。\[…\] 是等价写法。
一棵扇出为 、共 个键的 B 树,其高度为:
一行装不下的长公式在正文列内横向滚动,不会把版面撑宽;打印时保持静态。
math 围栏
math 围栏是块级公式的另一种写法,不依赖站点的 passthrough 配置。源码在 GitHub 上是一个普通代码块。
上式是 Little 定律在连接池上的形式:稳态下需要的并发连接数等于到达速率乘以平均响应时间。连接池大小通常远小于客户端数量。
化学式与单位
chem 围栏使用 KaTeX 的 mhchem 扩展,正文写 \ce{…}。同一个扩展也能排物理单位。
语法见 mhchem 手册。
编号公式
块级公式下面跟一行属性即成为编号公式。num 是作者书写的字符串(3-1、5.3),主题不自动计数;#id 不写时默认为 eq-<num>。编号显示在公式右侧,前缀「公式」按站点语言本地化。
见公式 3-1:乘上保留天数就是归档盘容量的下限。
caption(纯文本)可以省略。#id 与 caption 必须与 num 同时出现,不存在
「半编号」的公式。不完整或重复目标会告警,并丢弃不可用部分或保留第一项;严格
发布构建拒绝这条警告。
交叉引用
正文可以用普通链接引用编号公式,上一节即是这种写法。跨页引用、或需要自动带上「公式 N」标签时用 xref:
容量规划从 公式 3-1 开始。
xref 可以写在目标之前,前向引用合法。整本书的公式目录、book-equations 索引见书籍出版。
eq shortcode
eq 供无法开启 passthrough 的站点使用,正文交给同一个 KaTeX 渲染器。不带参数时是一个不注册编号的块级公式;带 num 时与上一节的属性行形态等价。
本站已开启 passthrough,日常写作用 $$。eq 用于迁移来的书稿与不能修改 hugo.yml 的场合。
站点前置配置
math 与 chem 围栏无需配置。$$、\[…\]、\(…\) 这些分隔符依赖 Goldmark 的 passthrough 扩展。Hugo 不合并主题的 markup 配置,这段必须写在站点自己的配置文件里。本站使用下面这份:
各键的完整定义见配置总览。分隔符不能与站点正文冲突:单个 $ 没有配进去,避免「$5」这样的价格被当成公式。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 构建期渲染好的 KaTeX HTML + MathML;本页额外加载一份本地 katex.min.css,没有公式的页面不加载 |
| 打印 | 同 HTML,静态,长公式不滚动 |
| Markdown | 原样输出源码:$$ 块(连同下面的属性行)、math / chem 围栏、\(…\);eq shortcode 输出 **公式 3-2.** 说明 + 一个 $$ 块 |
| RSS | 与 Markdown 相同的静态文本 |
任何形态都不加载 JavaScript。
参数参考
四种写法:
\(…\),- 由站点 passthrough 配置决定;不能带属性
$$…$$/\[…\],- 同上;可以跟一行属性变成编号公式
```math,- 不依赖 passthrough 配置;不接受属性
```chem,- 同上,正文写
\ce{…}
块级公式的属性行 {…}:
num, ,[0-9A-Za-z.-]+;注册为编号公式,右侧显示「公式 N」#id, ,[A-Za-z][A-Za-z0-9_.:-]*;锚点与交叉引用目标caption, ,- 编号后面的说明;需要
num
eq shortcode 的参数:
num, ,- 同上;不写就是一个不编号的普通块级公式
id, ,- 需要
num caption, ,- 需要
num class, ,- 需要
num;透传给站点 CSS 正文, ,- 必填,非空
TeX 写错时普通预览告警并保留原表达式。消息带 KaTeX 详情与源码位置;严格发布构建 拒绝这条警告。
限制与常见问题
- 分隔符由站点配置决定:
$$、\[…\]、\(…\)是否渲染只取决于站点markup.goldmark的 passthrough 扩展。front matter 里写math: true主题不读,缺少配置时$$仍然原样显示;改用math围栏或eq可以绕开。 - 只有
$$块和eq能编号:math围栏不接受属性行,需要编号就换写法。 - 编号是手写的:主题不自动计数,也不重排;调整章节顺序要自己改
num。 - 行内公式不能带属性:属性行只对块级公式有效。
caption是纯文本:里面的 Markdown 不解析。
相关
4.11 - Mermaid
mermaid 围栏把文本写成流程图、时序图、甘特图、类图与状态图,本地渲染、跟随深浅色、diff 友好。mermaid 围栏把一段文本渲染成流程图、时序图、甘特图、类图、ER 图与状态图。图以源码形式存在,可以进 Git、可以 review diff、可以被搜索命中;渲染由主题自带的 Mermaid 在读者浏览器里完成,不请求外部服务。需要像素级控制的示意图画成 SVG,按图片使用。
最简例子
flowchart LR
内容["content/"] --> Hugo
配置["hugo.yml"] --> Hugo
主题["OINK 主题"] --> Hugo
Hugo --> 站点["public/"]围栏语言写 mermaid 即可,没有其它开关。主题检测到这个围栏后才把 Mermaid 运行时加入这一页,同一页里画十张图也只加载一次。
时序图
sequenceDiagram 描述参与者之间按时间发生的消息,适合说明请求链路与加载顺序。
sequenceDiagram
autonumber
participant 读者 as 读者浏览器
participant CDN as 静态托管
participant JS as 页面脚本包
读者->>CDN: GET /zh/docs/components/mermaid/
CDN-->>读者: HTML(一个 figure 加围栏源码)
读者->>CDN: GET 本页的脚本包
CDN-->>读者: mermaid.min.js
JS->>JS: 把围栏源码渲染成 SVG
Note over JS: 未使用的运行时不下载甘特图
gantt 画时间区间。下面是 PostgreSQL 各大版本从发布日算起的五年社区支持期,1825d 即五年。
gantt
title PostgreSQL 大版本的五年社区支持期
dateFormat YYYY-MM-DD
axisFormat %Y
section PG 15
发布于 2022-10-13 :2022-10-13, 1825d
section PG 16
发布于 2023-09-14 :2023-09-14, 1825d
section PG 17
发布于 2024-09-26 :2024-09-26, 1825d
section PG 18
发布于 2025-09-25 :active, 2025-09-25, 1825d类图与 ER 图
classDiagram 画类型与关系,erDiagram 画实体与基数。两者都常用来解释数据模型。
classDiagram
class Page {
+string Title
+string Description
+int Weight
+Content()
+OutputFormats()
}
class Resource {
+string Name
+string RelPermalink
+Resize(spec)
}
class OutputFormat {
+string Name
+string MediaType
}
Page "1" --> "0..*" Resource : 页面包资源
Page "1" --> "1..*" OutputFormat : html / print / markdown / rsserDiagram
pg_database ||--o{ pg_namespace : "包含模式"
pg_namespace ||--o{ pg_class : "包含关系"
pg_class ||--o{ pg_attribute : "包含列"
pg_class ||--o{ pg_index : "被索引"
pg_class {
oid oid PK
name relname
char relkind
}
pg_attribute {
oid attrelid FK
name attname
smallint attnum
}状态图
stateDiagram-v2 画状态与迁移条件。下面是 OINK 主题一次发布依次经过的五个状态。这五个状态互不等价,本地构建通过不属于其中任何一个。
stateDiagram-v2
[*] --> 源码完成
源码完成 --> 已验证 : 主题检查脚本 + 站点测试套件全绿
已验证 --> 已发布 : 推送不可变的签名 vX.Y.Z 标签
已发布 --> 已文档化 : 站点 go.mod 钉住该标签
已文档化 --> 已部署 : 生产构建上线
已部署 --> [*]
已发布 --> 源码完成 : 发现问题只能出新补丁版本,标签不移动单张图的标题与配置
围栏正文最前面可以写 Mermaid 自己的 YAML 头,它不是 Hugo front matter。title 给图加标题,config 覆盖这一张图的 Mermaid 配置。写死 config.theme 的图不再跟随站点深浅色。
---
title: 只有用到的运行时才会进包
config:
flowchart:
curve: linear
---
flowchart TD
页面 --> 判断{用了什么组件?}
判断 -->|Mermaid 围栏| M[mermaid.min.js]
判断 -->|ECharts 围栏| E[echarts.min.js]
判断 -->|都没用| B[只有基础包]深浅色
页面初始化时主题读取当前配色模式:深色模式下用 Mermaid 的 dark 主题,浅色模式下用站点配置的主题。读者切换配色时图会就地重绘,页面不会重载;重绘期间每张图保持原有高度,页面不会在读者眼皮底下跳动。
因此不要把 Mermaid 图放进需要保留输入状态的页面,例如带表单的页面。
站点级默认写在 hugo.yml 里,键名小写,主题按 Mermaid 的默认配置匹配回正确的大小写:
完整键表见配置总览,可用值以 Mermaid 配置文档为准。
放进标签页与步骤
mermaid 围栏没有 tab 属性,相邻围栏标签页只对普通代码围栏生效。并排比较两张图用 tabs shortcode。
flowchart LR
Markdown --> Goldmark --> 渲染钩子 --> HTMLflowchart LR
页面 --> HTML
页面 --> 打印
页面 --> Markdown
页面 --> RSS{{% steps %}} 里的每一步是页面级 Markdown,其中可以写 mermaid 围栏,用法见步骤。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 一个 figure,里面是空舞台加上以 JSON 保存的围栏源码,页面的 Mermaid 运行时把 SVG 画进去 |
| 打印 | <pre class="td-mermaid-source"> 包着的源码,静态输出,不跑运行时 |
| Markdown | 原样保留 mermaid 围栏与它的源码 |
| RSS | <pre class="td-mermaid-source"> 包着的源码,订阅端看到的是文本 |
参数参考
围栏属性:没有。mermaid 围栏不读属性行,写 {height=…}、{class=…} 之类既不生效也不报错;尺寸由图自身与容器宽度决定,并在其中居中。
站点参数(hugo.yml):
params.mermaid, ,- 整个映射按 Mermaid 的
initialize()配置传入;键名写小写,主题按 Mermaid 默认配置匹配回正确大小写 params.mermaid.theme, ,- 浅色模式下的主题;深色模式下被强制为
dark
单张图的配置写在围栏正文最前面的 YAML 头里(title、config),属于 Mermaid 语法,不是主题参数。
放大查看
图在正文栏里居中;比栏宽更宽的图会被 Mermaid 缩小到能放下为止——一张宽的时序图在手机上可能只剩自身尺寸的三分之一。把指针移到图上(或用键盘走到它),图的角上会出现一个按钮,点开后图会按原始尺寸重新渲染一遍:拖动平移,滚轮、双指捏合或 + - 键缩放,0 复位,Esc 关闭。如果一张图要缩到一半以下才放得下,它会按 1:1 停在起始角打开而不是变成缩略图;而无论多大,往回缩总能看到整张图。这一切不下载任何东西,也没有开关要配置,它跟着围栏一起来。
限制与常见问题
- 图不能编号:Mermaid 输出的是内联 SVG,不是
<img>,{#id num=}编号不适用;需要编号时导出成图片,按图片的编号写法使用。 - 围栏属性无效:宽度在图里控制(
flowchart的方向、classDiagram的布局),或者用 CSS。也没有对齐属性——图总是居中。 - 语法错误只在浏览器里可见:Hugo 不解析 Mermaid 语法,写错的图在页面上显示一条带解析错误与图源码的提示,构建照样通过,发布前要在浏览器里确认。
- RSS、Markdown 与打印输出里是源码而不是图:结论要写在正文里,不要只画在图上。
相关
4.12 - PlantUML
plantuml 围栏写时序图、类图、组件图、活动图与用例图;渲染必须由你自己配置一个 PlantUML 服务。plantuml 围栏里写 PlantUML 源码,浏览器把源码压缩编码后拼在一个 PlantUML 服务的
URL 后面,换回一张 SVG。适合需要完整 UML 表达力的时序图、类图、组件图、活动图与
用例图。渲染依赖一个服务:主题不提供默认端点;enable: true 却没给
svg_image_url 时,普通预览告警并保持关闭,严格发布构建拒绝这条警告。没有可用
服务时改用 Mermaid。
PlantUML 要连你自己的服务,本站不假设读者有哪个端点可用。当前主题版本的 plantuml 围栏还会把 <、>、&、" 二次转义,带箭头或引号的源码送到端点后返回 Syntax Error? 图(见限制与常见问题)。下面每段源码本身都是正确的 PlantUML。
编码后的图表源码作为 URL 发给你配置的端点。不要在 PlantUML 图里写口令、内网主机名或客户名称。内网站点自建端点,或改用预渲染的图片。
最简例子
时序图是 PlantUML 最常用的一类:participant 声明参与者,-> 是同步消息,--> 是返回。
画出来是四条泳道、四条消息的一张时序图:读者打开页面 → 浏览器带着编码后的源码请求端点 → 端点返回 SVG → 运行时把围栏替换成图片。
类图
class 写成员,"1" -- "0..*" 写关系基数,用来解释数据模型。
三个方框各带一列字段,两条带基数标注的连线:一个发布可以被多个订阅使用,每个订阅绑定一个复制槽。
组件图
package 圈出部署单元,[组件] 是方块,--> 是依赖方向。
两个虚线框,框里各三个组件方块,五条带标注的箭头串起采集链路。
活动图
start / stop 加 if … then … else … endif 画带分支的流程。这类图不含箭头字符,是当前版本里能正常渲染的一类。
一条竖向流程线,两个菱形判断各分出「是 / 否」两支,四个终点。
用例图
actor 是小人,(用例) 是椭圆,rectangle 圈出系统边界,适合放在文档的「读者是谁」一节。
左边三个小人,右边一个方框里七个椭圆,连线表示谁能做什么。
深色模式下的配色
服务端不知道站点的配色模式,渲染出来的 SVG 底色是固定的白色。skinparam backgroundColor transparent 去掉底色,图落在页面背景上。线条与文字设成中性色后,两种模式下都可读。
PlantUML 的 !theme 指令(例如 !theme plain)也可用,主题包由服务端提供,自建端点需要确认已安装。
渲染服务
围栏本身没有开关,能否渲染取决于站点配置:
enable: true却没写svg_image_url→ 构建报错params.plantuml.enable requires an explicit params.plantuml.svg_image_url。主题不代替站点选择公共服务。- 自建可以用官方镜像
plantuml/plantuml-server,svg_image_url指向它的/svg/路径,结尾的斜杠不能省略,编码后的源码拼在它后面。 - 端点的跨域策略、站点 CSP 的
img-src(svg: true时还有connect-src)都要放行;子路径部署时写绝对 URL。
这几个键的完整定义在配置总览。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 先输出 <pre><code class="language-plantuml"> 源码,启用后由运行时替换成 <img>(svg: true 时是 <svg data-src>) |
| 打印 | 与 HTML 相同:打印视图同样加载运行时并请求端点 |
| Markdown | 原样保留 plantuml 围栏与它的源码 |
| RSS | 只有围栏源码,订阅端看到的是文本 |
未启用、或运行时没有加载时,页面上留下的是一段可读的源码块,不会出现坏图标。
参数参考
围栏属性:没有。plantuml 围栏不读属性行;它也不走 OINK 的代码块外壳,title、copy、行号这些代码块参数在这里都无效。
站点参数(hugo.yml):
params.plantuml.enable, ,- 关闭时围栏保持为代码块,不加载运行时
params.plantuml.svg_image_url, ,- 渲染端点,编码后的源码直接拼在它后面;
enable: true时必填,否则告警并保持关闭 params.plantuml.svg, ,false插<img src>;true插<svg data-src>并额外加载外部 SVG 加载器,SVG 内容进 DOM、可被 CSS 影响
主题只读这三个键,其它键写了没有效果。
限制与常见问题
<、>、&、"会被二次转义:当前主题版本的plantuml围栏对内容多做了一次转义,页面上留下-->、"这样的字面文本,端点收到后返回一张Syntax Error?图。带箭头的图(时序、组件、用例、状态)目前渲染不出来,只有活动图这类不含这些字符的能正常渲染。修复前请改用 Mermaid 或预渲染的图片。- 必须有服务:主题不提供、也不默认任何公共端点。
- 图表源码会离开浏览器:涉密内容不要写进 PlantUML 围栏。
- 不跟随深浅色:服务端不知道读者的配色模式,只能靠
skinparam自己调。 - 不能编号、不能缩放:运行时插入的
<img>不经过图片渲染钩子,{#id num=}与图片缩放都用不上。
相关
4.13 - 思维导图
markmap 围栏把一段 Markdown 大纲变成可展开、可缩放的思维导图,源码本身就是能读的提纲。markmap 围栏的正文是一段普通的 Markdown 大纲:标题与列表决定层级,浏览器把它画成一棵可展开、可折叠的树。适合把「这一节讲了什么」的层级一次呈现。节点之间有方向、有条件的流程用 Mermaid。
最简例子
# OINK
## 本地优先
- 运行时全部随主题分发
- 不依赖任何 CDN
## Markdown 原生
- 组件是围栏和属性行
- 不写 shortcode 也能用
## 四态输出
- HTML
- 打印
- Markdown
- RSS
一级标题是根节点,其余标题与列表项按缩进挂在它下面。点击节点上的圆点折叠或展开这一支,鼠标滚轮缩放,拖动平移。右下角一排工具按钮提供缩放、适应窗口与下载 SVG。
多层级
层级越深字号越小,画布自动排布。下面是本站文档的六个栏目与它们的页数。
# OINK 文档
## 简介(4 页)
### 它是什么
### 功能一览
### 案例
### 许可
## 快速上手(4 页)
### 选择起点
### OINK Starter
### 仓库导览
### 从零开始
## 创作内容(8 页)
### 组织内容
### 编写页面
### 页面参数
### 博客
### 书籍
### 发布与下载
### OpenAPI
## 组件(22 页)
### 提示块 / 标签页 / 步骤 / 卡片
### 图片 / 画廊 / 表格 / 参数表
### 图表:Mermaid / PlantUML / 思维导图 / ECharts
## 定制站点(15 页)
### 品牌 / 导航 / 搜索 / 多语言
### 首页 / 版本 / 分类 / 打印
## 维护管理(7 页)
### 预览 / 部署 / 升级
### 评论 / 统计 / 排错
链接、代码与强调
节点里可以写行内 Markdown:链接可点击,行内代码用等宽字体,粗体与斜体照常生效。
# 日常命令
## 预览
- `hugo server` — 打开 [localhost:1313](http://localhost:1313/)
- `hugo server -D` — **连草稿一起**预览
## 构建
- `hugo --printPathWarnings --panicOnWarning`
- `hugo --gc --minify` — 发布用
## 主题
- `hugo mod get -u github.com/pgsty/oink`
- [主题仓库](https://github.com/pgsty/oink)
- [本站源码](https://github.com/pgsty/oink.pgsty.com)
节点里的公式
Markmap 运行时带了一份本地 KaTeX,节点里的 $…$ 会被渲染成公式。
# 常看的几个 PostgreSQL 指标
## 缓存命中率
- $\frac{blks\_hit}{blks\_hit + blks\_read}$
- 低于 0.99 时检查 shared_buffers
## 复制延迟
- $lsn_{primary} - lsn_{replica}$
## 事务吞吐
- $TPS = \frac{\Delta xact\_commit}{\Delta t}$
控制初始展开层数
围栏正文最前面可以写一段 Markmap 自己的 YAML 头,它不是 Hugo front matter。initialExpandLevel 只展开前几层,其余分支由读者点开。colorFreezeLevel 指定从第几层起同一分支使用同一种颜色。
---
markmap:
initialExpandLevel: 2
colorFreezeLevel: 2
---
# 主题仓库的检查脚本
## 源码级契约
### check-i18n.py
### check-taxonomy.py
### check-font-tokens.py
## 输出级检查
### check-output.py
### check-goldens.py
### check-code-blocks.py
### check-content-primitives.py
### check-media-primitives.py
## 浏览器运行时
### node --test tests/js/**/*.test.js
折进折叠块
每张导图固定 300 像素高,正文里连着放三张会占掉大量版面。把全景图折进 > [!DETAILS],由读者自己展开。折叠块里的每一行都要以 > 开头,围栏也不例外。
# pgsty/oink
## layouts/
- baseof.html 与各类型的壳
- _partials/shell/
- _markup/ 渲染钩子
- _shortcodes/
## assets/
- scss/ 令牌与组件样式
- js/ 浏览器运行时
- third_party/ 随主题分发的库
## i18n/
- 32 个语言文件,键完全对齐
## docs/
- 冻结契约文档
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 先输出 <pre><code class="language-markmap">,运行时把它换成 <div class="markmap"> 并画出 SVG |
| 打印 | 与 HTML 相同:打印视图同样加载运行时 |
| Markdown | 原样保留 markmap 围栏与它的大纲源码 |
| RSS | 只有大纲源码,订阅端看到的是一段可读的提纲 |
大纲本身就是内容:拿不到 JavaScript 的地方读到的仍是完整层级。
参数参考
围栏属性:没有。markmap 围栏不读属性行,高度由主题固定为 300px(.markmap > svg),宽度撑满正文栏。
站点参数(hugo.yml):
params.markmap, ,- 关闭时围栏保持为代码块,不加载任何运行时
键的完整定义见配置总览。每张图的行为写在围栏正文最前面的 markmap: YAML 头里(initialExpandLevel、colorFreezeLevel、maxWidth 等),属于 Markmap 语法,可用键以 Markmap 文档为准。
限制与常见问题
- 输出是固定 300px 高的内联 SVG:高度由一条
.markmap > svg规则统一,围栏改不了,层级太多时用initialExpandLevel收起或拆成两张图;内联 SVG 也不适用{#id num=}编号与图片缩放。 - 不跟随深浅色:连线颜色由 Markmap 自己的调色板决定,两种模式下都需要检查对比度。
- 没开
params.markmap就只是代码块:不用这个组件的站点不加载任何运行时。 - 右下角工具栏里的「下载 SVG」是浏览器行为,导出的是当前展开状态的快照。
- 大纲里避开
<、>、&、":当前主题版本的markmap围栏会把这几个字符二次转义,节点上会出现>、"这样的字面文本;写链接用[文字](URL),不要用尖括号自动链接。
相关
4.14 - Draw.io
.drawio.svg 当普通图片放进页面,读者鼠标移上去就能点开 Draw.io 编辑器改图。Draw.io 集成没有围栏也没有 shortcode,用的是普通 Markdown 图片。Draw.io 导出时勾上「Include a copy of my diagram」,SVG 或 PNG 里会带一份 mxfile 源码;主题的运行时识别这份副本后,给图片加一个编辑按钮。适合需要读者取走修改的图;只用于展示的图按普通图片处理。
最简例子
写法与普通图片相同,文件名不受限制,.drawio.svg 只是惯例。
这张图嵌着一份 mxfile 副本,因此被包进了 .drawio 容器。鼠标移到图上时,右下角出现一个铅笔按钮;点击后在当前页面盖一层全屏 iframe,加载站点配置的编辑器。
副本检测
运行时的判断依据只有一条:文件内容里有没有 mxfile 字样,与文件名无关。下面这张同样是 SVG、同样是块级图片,但它是手写的,没有副本,也就没有按钮。
带图注
Draw.io 图片走的是普通图片渲染钩子,图片的属性照常可用。加 caption 得到带图注的 figure,编辑按钮仍然出现在图上。
编号成书里的图
加 {#id num=…} 得到一张可交叉引用的编号图,与别的图片一样能被 xref 引用、进入图目录。
编号与交叉引用的完整规则见书籍出版。
SVG 还是 PNG
两种都识别。Draw.io 导出 PNG 时同样能带上副本,存在 PNG 的文本块里,运行时的判断逻辑相同。

文档里优先用 SVG:缩放不失真,文字是真实文本(可被搜索、可被读屏器读取),改动的 diff 也读得懂。图特别复杂、或目标平台不支持 SVG 时用 PNG。只有 PNG 能走 Hugo 的图片处理;SVG 上的处理操作会告警并保留原图,严格构建会拒绝该告警。
编辑流程
按钮依次做三件事。
盖一层遮罩
页面上插入一个全屏的 div.drawioframe,里面是一个 iframe,地址是配置的 drawio_server 加上一串固定参数(embed=1&ui=atlas&proto=json&saveAndEdit=1&noSaveBtn=1)。
把图送进编辑器
编辑器就绪后,运行时把这张图片的内容(含 mxfile 副本)作为 data URL 发进 iframe。这一步不经过你的服务器。
保存与回写
在编辑器里点保存,运行时让编辑器按原格式(SVG 或 PNG)导出,由浏览器下载成同名文件。运行时不写回仓库:把下载到的文件覆盖 content/ 里那一份,再自行提交。
编辑按钮供读者取走图去改,不是站点的在线编辑功能。
编辑器地址
enable: true却没写drawio_server时会告警并关闭编辑;严格构建会因该告警失败。主题不代替站点选择公共服务。- 编辑过程必须留在组织内部时,部署一份自托管编辑器,把地址指向它。
- 公共端点
https://embed.diagrams.net/可用,读者的图会进入第三方页面。
这两个键的完整定义在配置总览。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 普通 <img>(或 <figure>);启用后运行时把带副本的图包进 <div class="drawio"> 并加按钮 |
| 打印 | 图片照常打印;按钮默认隐藏(只在悬停时出现),打印上不会有它 |
| Markdown | 普通 Markdown 图片语法 |
| RSS | 普通 <img>,绝对 URL,没有按钮 |
图片本身在四态里都在,编辑按钮是增量能力。
参数参考
没有专属的围栏或 shortcode 参数。图片属性行沿用图片那一套(caption width height link #id num command options)。
站点参数(hugo.yml):
params.drawio.enable, ,- 关闭时不加载任何脚本,图片就是图片
params.drawio.drawio_server, ,- 编辑器地址;
enable: true时必填
限制与常见问题
- 运行时只在渲染内容含
.svg或.png候选图的页面加载;同一 URL 的图片合并检查,只读取一次以查找mxfile。 - 导出时忘了勾「Include a copy of my diagram」,图就只是一张图,没有按钮。
- 编辑依赖编辑器,且不写回仓库:离线环境里图片正常显示,按钮点了没有反应;编辑器保存等于浏览器下载,替换文件与提交都要手动做。
- 按钮只在悬停时出现:触屏设备上没有 hover,读者不容易发现它,不要把可编辑当成关键功能来讲。
- 配色不跟随深浅色:导出的 SVG 颜色是固定的;把填充设成
none、线条与文字用中性灰,两种模式下都能看(本页这两张图就是这么做的)。
相关
4.15 - ECharts
echarts 围栏里用 YAML 或 JSON 写图表选项,Hugo 构建期校验,浏览器用本地 ECharts 画出跟随深浅色的统计图。echarts 围栏的正文是一段 YAML 或 JSON 的 ECharts 选项对象,不是代码。适用于需要
坐标轴、序列与图例的定量图表;只表达关系与流程时用
Mermaid,只表达顺序与层级时用
Infographic。Hugo 在构建期解析选项;无效输入在
普通预览中告警并保留可读源码,严格发布构建拒绝这条警告。浏览器用随主题分发的
ECharts 绘图,只有用到它的页面加载运行时。
最简例子
一个柱状图只需要三段:xAxis、yAxis、series。下面是本站文档六个栏目各有多少页。
tooltip:
trigger: axis
xAxis:
type: category
data: [简介, 快速上手, 创作内容, 组件, 定制站点, 维护管理]
yAxis:
type: value
name: 页数
series:
- name: 页数
type: bar
data: [4, 4, 8, 22, 15, 7]两种格式都接受,YAML 不需要引号与逗号,写起来更短。缩进写错、正文解析成数组而不是映射,构建在这一行失败,不会输出一张空白图。
多序列折线
series 是数组,多一项就是多一条线;legend 让读者单独隐藏其中一条。下面是 PostgreSQL 各大版本的发布年份,以及按社区五年支持策略推算的终止年份。
tooltip:
trigger: axis
legend:
data: [发布年份, 支持终止]
grid:
left: 56
right: 24
top: 48
bottom: 40
xAxis:
type: category
name: 大版本
data: ["9.6", "10", "11", "12", "13", "14", "15", "16", "17", "18"]
yAxis:
type: value
min: 2015
max: 2031
name: 年份
series:
- name: 发布年份
type: line
smooth: false
data: [2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025]
- name: 支持终止
type: line
lineStyle:
type: dashed
data: [2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030]版本号要加引号:YAML 里不带引号的 10 是数字,9.6 也是;作为分类轴的标签它们必须是字符串。
饼图与环形图
radius 给两个值就是环形图。下面是 OINK 的 29 个 shortcode 按用途的构成。
tooltip:
trigger: item
formatter: "{b}:{c} 个({d}%)"
legend:
bottom: 0
series:
- type: pie
radius: [42%, 70%]
itemStyle:
borderRadius: 6
borderWidth: 2
label:
formatter: "{b} {c}"
data:
- { value: 14, name: 核心组件 }
- { value: 10, name: Book 编号与索引 }
- { value: 3, name: 发布与下载 }
- { value: 2, name: OpenAPI }{b} {c} {d} 是 ECharts 的模板占位符(名称 / 数值 / 百分比),写在字符串里即可,不需要函数。
高度与通栏
height 默认 400px,接受 px rem em vh vw %;full=true 去掉正文的宽度限制,让图铺满内容区。适用于数据点多、标签长的图。
tooltip:
trigger: axis
grid:
left: 40
right: 16
top: 24
bottom: 32
xAxis:
type: category
data: [i18n, 分类法, 字体令牌, 内容契约, 导航, 运行时, 侧栏图标, 搜索, 动作, 命令面板, 双语文档, 阅读, 发布物, 下载, Landing, Book, 迁移, 键盘, 页尾, 输出, 金样本]
yAxis:
type: value
name: 脚本数
series:
- type: bar
data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]无效高度(360、36pt)在普通预览中告警并使用默认值;严格发布构建拒绝这条警告。
深浅色
不写 theme 时,图按读者当前的配色模式初始化;切换配色时图原地重绘,不刷新页面。容器尺寸变化时自动 resize。把本页切到深色,上面每张图的底色与文字随之改变。
写定 theme 则固定配色,两种模式下都是同一套:
xAxis:
type: category
data: [HTML, 打印, Markdown, RSS]
yAxis:
type: value
series:
- type: bar
data: [1, 1, 1, 1]运行时内置的只有 dark;其它 ECharts 主题要先用 echarts.registerTheme() 注册才能在这里引用。没有品牌要求时不写 theme,让图跟随站点配色。
回调:$fn:
围栏是数据,不能带 JavaScript。某个选项需要函数时(提示框格式化、数据驱动的颜色),在选项里写字符串 "$fn:名字",再把这个名字注册到 window.OinkEchartsFunctions:
tooltip:
trigger: axis
formatter: "$fn:pageShare"
xAxis:
type: category
data: [简介, 快速上手, 创作内容, 组件, 定制站点, 维护管理]
yAxis:
type: value
series:
- type: bar
data: [4, 4, 8, 22, 15, 7]鼠标悬停在任意一根柱子上,提示框里是该函数拼出的句子。名字未注册时该选项解析为 undefined,图按未设置该项绘制,构建与运行都不报错。脚本与围栏放在同一页的相邻位置,便于一起改动。
这段脚本属于站点代码,按代码审查对待。字符串模板({b} {c} {d})能表达的格式不写成函数。
数据位置
围栏正文是字面量。Hugo 不在其中展开 shortcode、front matter 变量或 data/ 目录里的文件,数字写在围栏里。代价是数据不能共享,收益是图表源码与数据一起进入 Git,diff 能看出改动了哪个数值。
数据经常变动(版本矩阵、发布物清单)时不做成图:改用表格,或发布与下载页中由 data/ 驱动的组件。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-echarts"> 里一个画布容器加一段 application/json 选项,本地 ECharts 画图 |
| 打印 | 不画图,输出 <pre class="td-echarts-source"> 包着的围栏源码 |
| Markdown | 原样保留 echarts 围栏与选项源码 |
| RSS | 与打印相同,只有源码 |
图上的结论要在正文里写一遍:打印与 RSS 输出里没有图。
参数参考
围栏属性行(```echarts {…}):
height, ,- 只接受非负数字加
pxrememvhvw%;其它写法告警并使用默认值 theme, ,- 固定使用某个 ECharts 主题,从此不再跟随站点配色;内置只有
dark full, ,true去掉正文宽度限制,图铺满内容区class, ,- 透传给容器,交给站点 CSS
style、on* 与未知属性会告警并忽略。围栏正文不能解析成 YAML/JSON 映射时告警
并渲染为源码;严格发布构建拒绝所有这些警告。选项键本身是 ECharts 的,以
官方选项手册为准。
没有站点级参数:ECharts 不需要在 hugo.yml 里开关,用到时才加载。
限制与常见问题
- 围栏里不能写 JavaScript:需要函数时通过
$fn:桥接,未注册的名字解析为undefined,没有报错。 - 围栏不读外部数据:
data/目录、front matter 与 shortcode 都引用不到,数字写在围栏里。 - 打印与 RSS 里只有源码,结论要写进正文。
- YAML 的类型转换:分类轴上的
10、9.6、on、yes会被解析成数字或布尔值,需要引号。 - 颜色不是唯一的区分手段:多序列图同时区分线型或标记形状,两种配色模式下都要检查图例对比度。
相关
- Infographic — 表达结构与顺序的信息图,不是统计图
- 表格 — 数据少、需要精确读数时用表格
- Mermaid — 关系图与流程图
- 代码块 — 围栏属性行的通用规则
4.16 - Infographic
infographic 围栏挑一个 AntV 模板,把标题与条目渲染成流程、时间线、漏斗、网格或层级信息图。infographic 围栏挑一个 AntV 模板,把「标题 + 一串条目」渲染成信息图。适用于表达顺序、层级与对比这类结构。需要坐标轴与数值精度时用 ECharts,需要条件分支的流程时用 Mermaid。围栏正文是数据,在 GitHub 上仍是一段可读的文本。
最简例子
第一行是 infographic 模板名,其后是一个 data 块:title 是标题,items 下面每个条目至少要有 label。
infographic list-row-simple-horizontal-arrow
data
title 一次文档改动的三步
items
- label 写
desc 先写中文 .zh.md
- label 校
desc 构建零告警,例子真渲染
- label 发
desc 补英文对等页,提交 PR缩进决定结构,两个空格一级。标签要短,说明放 desc。
时间线
sequence-timeline-* 系列把条目排成一条时间轴,label 是时间点,desc 是事件。
infographic sequence-timeline-simple
data
title PostgreSQL 近五个大版本
items
- label 2021
desc 14:并行查询与逻辑复制的一轮改进
- label 2022
desc 15:MERGE 语句
- label 2023
desc 16:逻辑复制可以从备库进行
- label 2024
desc 17:增量备份与 JSON_TABLE
- label 2025
desc 18:异步 IO 子系统漏斗
sequence-funnel-simple 画逐步收窄的阶段。下面是主题的五个发布状态:互不等价,走完最后一个才是上线。
infographic sequence-funnel-simple
data
title 一次主题发布要经过的五个状态
items
- label 源码完成
desc 代码写完,仅此而已
- label 已验证
desc 主题检查脚本与站点测试套件全绿
- label 已发布
desc 不可变的签名标签,能从 Go 代理拉到
- label 已文档化
desc 文档站钉住了这个标签
- label 已部署
desc 生产环境运行的就是这个版本网格卡片
条目之间没有先后关系时用 list-grid-*,它把条目排成网格而不是队列。
infographic list-grid-compact-card
data
title 同一页内容的四种输出
desc 每个内容组件都要在这四态里给出可用的结果
items
- label HTML
desc 交互式,按需加载运行时
- label 打印
desc 折叠展开,去掉缩放与复制按钮
- label Markdown
desc 纯文本,按字节比对金样本
- label RSS
desc 静态,与打印同源带数值的条目
条目上加 value,能表达比例的模板(饼、环、进度)会用到它。
infographic chart-pie-donut-plain-text
data
title 29 个 shortcode 的构成
items
- label 核心组件
value 14
- label Book 编号与索引
value 10
- label 发布与下载
value 3
- label OpenAPI
value 2层级与手绘风格
条目下面可以再嵌 children,hierarchy-mindmap-* 把它画成两层的结构图。顶层的 theme 块换整张图的风格,type 取 light、dark 或 hand-drawn。
infographic hierarchy-mindmap-level-gradient-compact-card
theme
type hand-drawn
data
root
label 主题仓库
children
- label layouts
desc 模板
children
- label _markup
desc 渲染钩子
- label _partials
desc 外壳与工具
- label assets
desc 资源
children
- label scss
desc 令牌与组件样式
- label js
desc 浏览器运行时
- label third_party
desc 随主题分发的库theme 属于 DSL,不是围栏属性。它不跟随站点的深浅色:写 type dark 的图在浅色页面上也是深底。两种配色模式下都要检查对比度。
挑模板
模板名是 结构-变体 的组合,同一个结构有多个视觉变体。常用的几类:
| 结构前缀 | 表达什么 | 例子 |
|---|---|---|
list-row-* list-column-* |
一排 / 一列并列的条目 | list-row-simple-horizontal-arrow |
list-grid-* |
网格,条目之间无先后 | list-grid-compact-card list-grid-badge-card |
list-pyramid-* sequence-funnel-* |
逐层收窄 | sequence-funnel-simple |
sequence-timeline-* sequence-roadmap-vertical-* |
时间线与路线图 | sequence-timeline-simple |
sequence-steps-* sequence-snake-steps-* |
有序步骤 | sequence-steps-simple |
compare-binary-horizontal-* compare-quadrant-* |
二元对比与四象限 | compare-binary-horizontal-simple-vs |
hierarchy-mindmap-* hierarchy-structure-* |
层级(配合 children) |
hierarchy-mindmap-level-gradient-compact-card |
chart-pie-* chart-bar-* chart-column-* |
带 value 的示意图 |
chart-pie-donut-plain-text |
relation-network-* relation-dagre-flow |
网络与流向(配合 relations) |
relation-dagre-flow |
选能表达清楚关系的最小形式。完整图库见 AntV Infographic 图库,模板名与随主题分发的版本一一对应。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-infographic"> 里一个画布容器加一段 DSL,本地 AntV 运行时画成 SVG |
| 打印 | 不画图,输出 <pre class="td-infographic-source"> 包着的 DSL 源码 |
| Markdown | 原样保留 infographic 围栏与 DSL |
| RSS | 与打印相同,只有源码 |
图上的信息要在正文里写一遍:打印与 RSS 输出里只有那段 DSL。
参数参考
围栏属性行(```infographic {…}):
height, ,- 非负数字加
pxrememvhvw%;其它写法告警并使用auto full, ,true去掉正文宽度限制class, ,- 透传给容器
style、on* 与未知属性会告警并忽略;空 DSL 正文告警并不渲染。严格发布构建拒绝
所有这些警告。
DSL 的顶层键(属于 AntV,不是主题):
infographic/template- 模板名,第一行
datatitle、desc、items(也可以是sequencescomparesnodesvaluesrelationsroot,取决于模板结构)、orderthemetype(light/dark/hand-drawn)、palette、colorPrimary、stylize等width/height- DSL 层的画布尺寸,一般交给围栏属性
height design- 逐部件的细调,少用
items 里每个条目可用 label、desc、value、icon、children、group、id。DSL 的完整定义以 AntV Infographic 文档为准;随主题分发的版本与校验值记在主题仓库的 VENDOR.json 里。
限制与常见问题
- 模板名写错不会让构建失败:Hugo 只检查围栏属性,DSL 由浏览器运行时解析,模板不存在时容器里显示一行错误文字。改动模板名后在页面上确认。
- 不跟随深浅色:
theme写在 DSL 里,两种配色模式下都要检查对比度。 - 打印与 RSS 里只有 DSL,关键结论要写进正文。
- SVG 不是语义结构:屏幕阅读器读到的顺序未必是排版顺序。标题、列表、表格能表达的内容优先用它们。
- 标签要短:长文本在窄屏下会被截断或挤压,改动后在手机宽度下确认。
相关
4.17 - 画廊
gallery 围栏把一组相关截图排成响应式网格,每张可带说明或链接,并复用页面的图片缩放对话框。画廊(Gallery)把一组相关图片排成响应式网格,围栏里每行一张图。适用于同一件事的几个视图:几张截图、几种状态、几套配色。单张图用图片;相互之间没有顺序与对比关系的图片不适合放进同一个画廊。
最简例子
围栏里一行一张图,语法是 Markdown 的 。
替代文字必须写:它是这一项的标题、读屏器唯一能读到的文字,也决定这张图是否参与缩放。列数没有参数,网格随容器宽度自适应,窄屏减列。
加说明
图片后面用 # 起头写说明,显示在图下方。说明是纯文本,里面的 Markdown 按字面显示;要一个字面井号写 \#。

默认外壳:侧栏、正文、目录

OINK 的上游 Docsy,内容模型一脉相承

发布页由 data/download 里的事实生成,不联网
说明长短可以不一致:网格按最高的一项对齐,说明换行不影响相邻的图。图片先被解析,替代文字与路径里的 # 不需要转义。
每项一个链接
行尾的 {link=…} 让这一项成为链接,站内路径、相对路径、http(s): 都可以。
带链接的项不参与缩放,点击已有别的含义。同一个画廊里两种项可以混排:有链接的打开页面,没有链接的打开大图。
图片来源
来源解析顺序与普通图片一致:页面资源(页面包里的同目录文件)→ 全局资源 assets/ → 静态路径 /images/… → 远程 URL。本地资源带上固有尺寸,加载时不跳版;远程图构建期不下载,也取不到尺寸。

assets/images/… 下的图,可以做构建期处理

static/images/… 下的图,原样发布
页面 / 全局资源无法解析时按静态路径保留,与显式静态路径相同;主题不检查静态路径 与远程 URL 是否存在。
装饰图与缩放
替代文字留空表示这是装饰性图片:没有标题,读屏器跳过,也不参与缩放。
图片缩放是站点级开关,默认关闭。本页在 front matter 中开启了它,上面每张有替代文字、没有链接的图都可以点开看大图(Esc 关闭,焦点回到原处)。

装饰性配图,不参与缩放

有替代文字,可以点开
画廊没有自己的缩放运行时,复用整页共用的那个对话框。页面上没有可缩放的图时,运行时不加载。细节见图片 · 缩放。
加 class 与分标签页
class 可以加在整个围栏上(写在语言后面)或某一项上(行尾),主题不解释它,原样透传给站点 CSS。围栏带 tab=(以及 group= value=)时成为一组标签页里的一页。

默认配色

跟随系统或手动切换
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <ul class="td-gallery">,每项一个 <li>;符合条件的图带 data-td-image-zoom 标记;全部懒加载 |
| 打印 | 同一组图堆叠排列,没有缩放标记 |
| Markdown | 原样输出 gallery 围栏 |
| RSS | 与打印相同的静态堆叠 |
画廊不加载 JavaScript。
参数参考
行语法  [# 说明] [{key=value …}]:
,- 必须顶在行首。
alt是这一项的标题;留空表示装饰图 src,- 页面资源 / 全局资源 / 静态路径 / 远程 URL
# 说明,- 纯文本,显示在图下方;
\#是字面井号;不能为空 {link=…},- 让这一项成为链接,因而不可缩放
{class=…},- 给这一项加站点 CSS class
围栏属性:
tab, ,- 让这个画廊成为一个标签页
group/value, ,- 标签页分组与同步值;必须与
tab同时出现 class, ,- 透传给站点 CSS
没有 columns、caption、title 属性。坏行或坏属性会告警,只丢弃无效部分或该行,
并给出围栏内行号;严格发布构建拒绝这条警告。
限制与常见问题
- 只有围栏一种形态:没有
{.gallery}列表标记,也没有 shortcode。代价是源码在 GitHub 上不渲染成图片,收益是四态输出与缩放资格由主题保证。 - 不能指定列数,也不裁成统一宽高比:网格按视口自适应,图片按原始比例排列。
- 没有幻灯片、轮播与上一张 / 下一张:缩放对话框一次显示一张。
- 不下载远程图:构建期没有网络请求,远程图在浏览器加载前尺寸未知,可能跳版。
- 说明不解析 Markdown:需要富文本时写在画廊下方的段落里。
相关
4.18 - 徽章
徽章(Badge)是紧跟在名字旁边的行内状态标签:Beta、已弃用、v0.5、需自建服务。适用于一两个词能说完的状态;作者只选语义 tone,颜色由主题决定,浅色与深色模式下的对比度都有保证。状态需要解释、操作步骤或截止日期时,改用正文或提示块。
最简例子
text 是唯一必填参数,必须是非空字符串。
五种 tone
只有这五个取值,没有自定义颜色。
默认 信息 已支持 实验性 已弃用
不写 tone 时使用 neutral。其它取值在普通预览中告警并使用 neutral;警告带
源码位置,严格发布构建会失败。
夹在句子里
徽章是行内元素,跟在名字后面,不占单独一行。
params.ui.image_zoom 默认关闭 打开后,
有替代文字的块级图片可以点开看大图。PlantUML 需自建服务
与 Draw.io 需自建服务 没有配置服务端点时告警并保持关闭,
而不是连接公共服务。
标题旁边
标题里不要写 shortcode。 Hugo 先生成目录、后替换 shortcode,所以徽章在标题上渲染正常,目录里却会留下一段 Hugo 的内部占位符文本。把状态写进标题下面的第一段:
OpenAPI 页面
0.5 新增 徽章紧跟在标题下方,目录保持干净, 锚点链接分享出去也不会带上徽章文字。
表格单元格里
对照表里用徽章标状态,比整列写「是」「否」更容易扫读。
| 组件 | 形态 | 状态 |
|---|---|---|
| 提示块 | > [!NOTE] |
稳定 |
| 画廊 | ```gallery 围栏 |
稳定 |
| PlantUML | ```plantuml 围栏 |
需自建服务 |
image shortcode |
— | 已移除 |
列表与步骤里
- 安装 Hugo Extended ≥ 0.160.1
- 从 OINK Starter 创建站点,修改
hugo.yaml里的baseURL hugo server预览 1313 端口
卡片里
卡片有自己的 badge 参数(纯文本,固定在标题右侧);卡片正文里可以放徽章 shortcode。
一行 hugo mod get 完成安装 需要 Go
不联网的机器也能构建 手动升级
可点击的徽章
加 link 后徽章变成链接(<a>),站内路径、相对路径、http(s):、mailto: 都可以。
链接非法时普通预览告警并丢弃链接,保留普通徽章;严格发布构建拒绝这条警告。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 无链接时 <span class="td-badge td-badge--<tone>">,有链接时 <a class="td-badge …"> |
| 打印 | 同 HTML,静态行内元素 |
| Markdown | **Beta**,有链接时 [**Beta**](/…) |
| RSS | 同打印 |
不加载 JavaScript。徽章不是实时状态区域,新增徽章不会触发读屏器播报。
参数参考
text, ,- 必填,非空。读者看到的文字
tone, ,neutralinfosuccesswarningdangerlink, ,- 设置后徽章变成链接
只接受命名参数。没有 icon、class、color、outline、size 参数。无效输入
会告警并采用安全结果:未知参数忽略,空 text 不渲染,非法 tone 回退
neutral,不安全链接被丢弃。严格发布构建会拒绝每条此类警告。
限制与常见问题
- 颜色不是唯一的含义载体:tone 是补充,文字要自己说清楚。
{{< badge text="🔴" >}}对读屏器没有信息。 - 没有图标参数:需要图标时改用卡片或提示块。
- 文字要短:徽章不换行地跟在名字后面,超过五六个字的内容写进正文。
- 同一处不超过三枚:连排的徽章会盖过它修饰的名字。
- 徽章只有 shortcode 一种形态,没有原生 Markdown 写法;纯 Markdown 阅读器里它退化成加粗文字。
相关
4.19 - 按键
kbd 写快捷键:一个 shortcode 接一串按键名,输出语义化的按键序列,打印与 Markdown 输出里同样可读。按键(Kbd)把读者要按下的键与正文区分开。适用于快捷键与组合键:一个按键一个位置参数,主题负责画框、补分隔符,并给读屏器一个可读的序列。命令名、选项名与要输入的文本用行内代码,它们不是物理按键。
最简例子
按 Ctrl 加 K 打开命令面板。
参数必须加引号,一个按键一个位置参数。缺少、空白或命名参数会告警,普通预览不渲染 无效按键;严格发布构建拒绝这条警告。
单个按键
一个参数对应一个键,符号键按原样写。
Escape 关闭对话框; / 进入搜索; t 切换亮色 / 暗色; l 循环切换语言。
组合键
多个参数按顺序渲染,中间补 +。这个加号对辅助技术隐藏,读屏器读到的是本地化的连接词。
⌘ 加 Shift 加 P 与 Ctrl 加 Shift 加 P 是同一个动作。 需要按字面的加号时,把它当成独立的一个按键:Ctrl 加 + 放大页面。
平台差异
按键名写读者键盘上印的标签:macOS 写 ⌘,Windows / Linux 写 Ctrl。不要把两个平台合进同一个序列,Ctrl/⌘ 这类写法读屏器无法正确朗读。在句子里说明平台,或分成标签页。
macOS 按 ⌘ 加 K,Windows 与 Linux 按 Ctrl 加 K。
快捷键表
速查表是按键最常见的位置。下面是本站生效的一部分全局键:
| 按键 | 作用 |
|---|---|
| Ctrl 加 K | 打开命令面板(macOS 是 ⌘ 加 K) |
| / | 面板的完整搜索态 |
| t | 切换亮色 / 暗色 |
| q / e | 上一篇 / 下一篇 |
| w s a d | 在侧栏树里上下移动、折叠、展开 |
| Escape | 从侧栏树回到正文 |
全站快捷键的完整清单见键盘导航。
步骤里
- 按 Ctrl 加 K 打开命令面板
- 输入
>进入纯命令态,或输入关键词搜索 - 用 ↑ ↓ 选中一项,Enter 前往
- Escape 关闭,焦点回到按下之前的位置
原始 <kbd> 标签
Markdown 里写原始的 <kbd> 标签得到同样的样式,GitHub 也这么渲染。区别是分隔符与无障碍序列要自己维护:单个键两种写法都可以,组合键用 shortcode。
按 F5 刷新;在编辑器里按 Ctrl+S 保存。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <span class="td-kbd-sequence"> 包着每个键一个 <kbd>;可见的 + 对读屏器隐藏,另有一个本地化连接词 |
| 打印 | 同 HTML,静态 |
| Markdown | 纯文本 Ctrl + K、⌘ + Shift + P |
| RSS | 同打印 |
没有 CSS 与 JavaScript 时,操作说明仍然可读。
参数参考
位置参数 1..n, ,- 至少一个,每个都必须非空且加引号;顺序就是显示顺序
只接受位置参数。没有 separator、label、platform、class、size 这些命名参数:Hugo 的 shortcode 不允许在一次调用里混用位置参数与命名参数。
限制与常见问题
- 一个序列表示同时按下的一组键:先按 A 再按 B 这类连续操作写成两个 kbd 加一句说明(先按 Escape,再按 Enter)。
- 不做平台检测:页面不会按访客的操作系统把
Ctrl换成⌘。 - 不做按键映射与录制:菜单路径、手势、游戏杆不在范围内。
- 漏写引号会让构建失败:
{{< kbd Ctrl K >}}里的Ctrl不是字符串参数。 - 不用它标命令:
hugo server写成行内代码,Ctrl是按键。
相关
4.20 - 引用
三个 shortcode 各做一件事:include 把另一个文件的内容放进当前页面,param 打印一个页面或站点参数,comment 丢弃一段内容。适用于跨页复用的片段与散落在多页的常量:同一段安装步骤出现在三页时用 include,版本号出现在几十页时用 param,改一处即可。只在一页出现的内容写在那一页。
最简例子
include 只有一个必填参数 file:
被引的文件是一段普通 Markdown,放在 assets/ 下:
渲染结果与写在本页里相同:代码块有复制按钮,提示块是提示块。
把 OINK 安装到一个已有的 Hugo 站点,三条命令:
hugo mod get 需要本机安装 Go;用离线归档或 submodule 时不需要。
当前发布版本是 v1.0.0。
被引的文件不是一篇独立页面:它不出现在侧栏、不参与翻译配对、没有自己的 URL。
文件位置
file 按下面的顺序解析,第一个命中的胜出:
| 顺序 | 找哪里 | 写法 |
|---|---|---|
| 1 | 当前页面的页面资源(页面包里的文件) | file="config.yaml" |
| 2 | 全局资源 assets/ 下的文件 |
file="snippets/dsn.txt" |
| 3 | content/ 下的文件:/ 开头是内容根目录,否则相对当前页面所在目录 |
file="notes/caveat.md"、file="/shared/notice.md" |
三处都找不到,或路径里含 .. 时,引用会告警并不输出。严格发布构建拒绝这条警告:
引用只能在 content/ 与 assets/ 中取文件。
引 Markdown 片段时写文件在磁盘上的真名。有一个陷阱只属于第 1 步:Hugo 把带语言后缀的页面资源(如 notice.zh.md)按去掉后缀的名字挂在页面上,向页面包索取 notice.md 拿到的是已渲染的 HTML 而不是源码,Markdown 输出里会出现 <div class="td-code">。assets/ 与 content/ 下写什么名字就取什么文件,没有这层转换。非 Markdown 文件(.yaml、.sh、.txt)也没有这个区别。
本页两种语言各引一份自己的片段:中文引 assets/parts/install-oink.zh.md,英文引 assets/parts/install-oink.md。片段放在 assets/ 下而不是页面包里,两种语言就都按写下的名字取到源码。
引入代码文件
加 code=true 让文件按代码块渲染,lang= 指定高亮语言。引用仓库里的真实配置文件,文档与实际文件不会不一致。
代码块与围栏走同一条渲染管线:高亮、行号、复制按钮都有。围栏属性(title=、collapse、hl_lines=)传不进来,需要它们时把文件内容写成普通代码块。
片段内容
片段是页面级 Markdown,在当前页面的上下文里渲染:提示块、表格、列表、图片、步骤与 shortcode 都可以用。上面那段片段结尾的「当前发布版本是 v0.8.1」,是片段里的 {{< param version >}} 在本页展开的结果。
一个片段被两页引用时,两页各自渲染一遍,各自生成标题锚点与代码块 ID,互不冲突。
安装命令、连接串、支持矩阵、法务声明:会变动、且变动时必须处处同步的内容。只在一页出现的内容写在那一页。
插入站点参数
param 打印一个参数:先查本页 front matter,查不到再查站点配置(Hugo 的 .Param 规则)。
本站发布版本 v1.0.0,版权起始年 2026,
本页 front matter 里写了 pigsty_pg_major: 18,这里取到 18。
嵌套键用 . 连接,copyright.from_year 取的是 params.copyright.from_year。参数不存在,
或者值是 map / 列表而不是标量时,告警并不输出;严格发布构建拒绝这条警告。
在命令、表格与链接里插参数
param 的输出是转义后的纯文本,可以放进代码围栏、表格单元格与链接地址。安装命令里的版本号适合这么写:
| 项目 | 值 |
|---|---|
| 当前版本 | v1.0.0 |
| Hugo 下限 | 0.160.1 |
站点参数在哪里定义、有哪些可用,见配置总览;页面参数见页面参数。
构建期删除的注释
comment 的内容在 HTML、打印、Markdown、RSS 四种输出里都不出现。HTML 注释不同:它留在页面源码里,也会进入 llms.txt。
PostgreSQL 18 起 pg_stat_io 拆分了 WAL 统计。
升级前先在测试库上验证监控面板。
上面两段之间有一段注释,查看页面源码也找不到它。
输出形态
| 输出 | include(Markdown) |
include code=true |
param |
comment |
|---|---|---|---|---|
| HTML | 片段渲染成正常内容 | 高亮代码块 + 复制按钮 | 转义后的纯文本 | 无 |
| 打印 | 同 HTML | 同 HTML,无复制按钮 | 同 HTML | 无 |
| Markdown | 片段的源码原样输出 | 源码围栏 | 值本身 | 无 |
| RSS | 同 HTML | 同 HTML | 同 HTML | 无 |
Markdown 输出里片段是源码而不是 HTML,片段里的 shortcode 保持 {{< param version >}} 的原样。这与「Markdown 输出保留源码」一致,不是漏渲染。三个 shortcode 都不加载脚本。
参数参考
include(只接受具名参数):
file, ,- 解析顺序见文件放在哪;含
..、文件缺失、空值时告警并不输出 code, ,true时按代码块渲染;带引号的code="true"会告警并按普通内容引入lang, ,- 代码语言;没有
code=true时告警并忽略
其它参数名会告警并忽略,消息带文件名与行号;严格发布构建拒绝这条警告。
param(一个位置参数):
参数名, ,- 嵌套键用
.连接;先页面 front matter 后站点params;缺失或非标量时告警并不输出
comment 没有参数,成对使用,{{< comment >}} 与 {{< /comment >}} 之间的内容整段丢弃。
限制与常见问题
include不是模板:不能向片段传变量、不能条件引入、不能给引入的代码块加围栏属性(title=、collapse)。按平台分版本时写两个片段配标签页。- 片段的语言要自己维护:
include不做语言回退。中文页引中文片段,英文页引英文片段,两份文件并列存放(install-oink.zh.md与install-oink.md)。 param只打印标量:结构化数据(版本矩阵、下载列表)用data/目录里的数据配对应组件渲染。comment不是「暂时不发布」:内容每次构建都被丢弃,临时下线整页用draft: true。- 不把
include当目录页:一页引入十个片段时,读者需要的是十条链接。
相关
4.21 - Asciinema
asciinema 把一段 .cast 录像渲染成页面里的终端播放器。适用于命令行流程的演示:终端里的文字仍然是文字,可以选中复制,一段六分多钟的安装过程约 190 KB。图形界面的操作用截图或视频,本组件只播放终端录像。播放器与样式随主题分发,构建期不下载、运行期不连 CDN,只有用到它的页面、且只在 HTML 输出里加载这套运行时。
最简例子
只有 file 是必填的:
images/install.cast — /images/install.cast
这段录像是 Pigsty 在一台 Debian 机器上的单机安装,120×36 的终端,约 6 分 40 秒。文件在本站的 static/images/install.cast,路径写站点根路径。放在 assets/ 下也写相对路径:主题先在资源里查找,找不到再当成站点根路径。不写 title 时,窗口标题显示 file 的值。
窗口标题与主题
title 设置窗口标题,theme 设置配色:
Pigsty 单机安装 — /images/install.cast
theme 默认 auto:跟随站点的深浅色,浅色用 td-light,深色用 td-dark,读者切换配色时播放器就地重挂一次。要固定成某套终端配色时,可选值是播放器自带的 asciinema、dracula、gruvbox-dark、monokai、nord、seti、solarized-dark、solarized-light、tango,以及主题提供的 td-light / td-dark。固定的主题不跟随深浅色,深色站点配 solarized-light 的对比度不合适。终端字体不用单独设置:播放器使用站点的代码字体,与页面上的代码块一致。
速度、起点与封面
长录像用三个参数控制起点:speed 设倍速,startAt 跳过开头,poster 决定未播放时定格的画面。
从第 60 秒开始,两倍速 — /images/install.cast
speed 与 startAt 是数字(秒),poster 用播放器的 npt: 记法定位时间点,npt:1:30 是第 1 分 30 秒。上面这个播放器停在第 90 秒的画面,点播放从第 60 秒开始。
idleTimeLimit 把静默段压缩到最多 N 秒。这段录像在录制时已经压缩过(.cast 头里是 idle_time_limit: 0.5),此处不必再设。只有录制时没有限制静默时长的文件才需要它。
尺寸与适配
播放器默认按容器宽度缩放(fit="width"),终端的行列数来自 .cast 文件头。cols / rows 可以覆盖它:
只留 16 行高 — /images/install.cast
比录像本身小的行列数会裁掉内容,上面这个只显示 36 行里的 16 行。cols / rows 用于修正录像头里的错误尺寸,不是排版工具。要让播放器变矮,重录一次小终端。
fit 的四个值:width(默认,按宽度缩放)、height(按高度)、both(两个方向都装下)、none(不缩放,按字号原样显示,宽终端会溢出)。
循环与预加载
loop 播完自动重播,preload 在页面加载时取回 .cast,点播放不必等待:
循环播放:登录后的第一分钟 — /images/install.cast
autoplay="true" 让页面打开即播。不建议使用:系统的「减少动态效果」偏好只关闭播放器控件的过渡动画,不阻止自动播放。确实需要自动播放时,配上 loop、很短的内容,并且一页只放一个。
放进步骤里
录像放在某一步旁边:文字说明要做什么,录像展示实际输出。
-
安装依赖,获取安装脚本:
-
执行安装,全程约六分钟:
pig install — /images/install.cast
-
打开
http://<节点地址>:3000,用admin / pigsty登录 Grafana。
一页可以放多个播放器,脚本与样式只加载一次。
录制 cast 文件
主题只负责播放。用 asciinema 的 asciinema rec --idle-time-limit=2 --cols=100 --rows=28 install.cast 录制,asciinema play install.cast 本地回放确认。
- 终端宽度控制在 100 列以内,窄屏上仍可读;录制前先
clear。 - 录制前清理密钥:
.cast是纯文本,录像里的每个字符都能grep到,提交前检查一遍。 - 文件放进
static/images/或页面包并提交进仓库,不引用外站的.castURL。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | <div class="td-asciinema"> 窗口外框 + 播放器;播放器 CSS/JS 与运行时按需加载,一页一次,且只在这一种输出里 |
| 打印 | 一行带标题的静态链接,地址可见;不加载播放器,也不加载任何运行时 |
| Markdown | 一个纯 Markdown 链接 [标题](/images/install.cast)——没有组件标记,也没有配置块 |
| RSS | 同样的纯链接 |
录像不能是唯一的信息来源。关键命令与关键输出要在录像旁边用文字或代码块写一遍:离线读者、llms.txt 的抓取方与打印读者拿到的是这个链接和你写的文字,而不是终端会话本身。
参数参考
file, ,- 具名或第一个位置参数;先按全局资源找,找不到当站点根路径;
http/https地址原样使用,其它 scheme 告警并不渲染 title, ,- 窗口标题
theme, ,auto跟随站点深浅色;或td-lighttd-darkasciinemadraculagruvbox-darkmonokainordsetisolarized-darksolarized-lighttangofit, ,widthheightbothnone;其它值告警并使用widthcols/rows, ,- 覆盖终端行列数;比录像小会裁掉内容
speed, ,- 播放倍速
startAt, ,- 起播位置
idleTimeLimit, ,- 静默段最多播这么久
poster, ,- 未播放时定格的画面,
npt:分:秒 autoplay, ,- 页面加载即播;不建议
loop, ,- 循环播放
preload, ,- 页面加载时就取回
.cast pauseOnMarkers, ,- 播到章节标记处暂停
markers, ,- 章节标记;见下面的限制,标签目前到不了播放器
布尔类参数比较的是文本 true:loop="true" 与 loop=true 都表示开启,其它值表示关闭。其余参数一律告警后继续:fit 非法时用 width,speed 非数字时用 1,startAt 非数字时用 0,cols、rows、idleTimeLimit 与标记时间非数字时忽略。它们都不会中断普通构建,也都会让带 --panicOnWarning 的发布关卡失败。
限制与常见问题
markers的标签会丢失:主题把时间:标签的列表拼成一维数组交给播放器,播放器只接受成对写法,时间轴上会多出没有标签的标记点。标记时间不是数字时会告警并跳过该标记。需要章节时用录像旁边的文字列表。- 播放器需要 JavaScript:浏览器禁用脚本时只剩窗口外框。打印、Markdown 与 RSS 给的是链接,见输出形态。
- 录像不进搜索:站内搜索索引页面文字,录像里出现过的命令搜不到。
- 不引用远程
.cast:http与https地址会被接受,页面因此依赖一个外站;其它 scheme、协议相对的//host或空值都会告警,组件不渲染。 - 控制单段长度:超过五六分钟的录像少有人看完,长流程拆成几段短录像,各配一段文字。
相关
5 - 定制站点
本栏目覆盖站点级配置:hugo.yml 里的参数、data/ 下的数据文件、assets/ 下的样式入口。单个页面的写法与 front matter 见创作内容。
按改动目标查找
| 改动目标 | 对应页面 |
|---|---|
| 站名、Logo、favicon | 品牌外观 |
| 配色、深浅色模式、字体 | 品牌外观 |
| 顶栏菜单与下拉 | 导航与菜单 |
| 侧栏宽度、图标密度、目录深度 | 布局与页面类型 |
| 首页与落地页 | 首页与落地页 |
| 全文检索与索引范围 | 全文检索 |
| 命令面板里的条目 | 命令面板 |
| 快捷键 | 键盘导航 |
| 新增一门语言 | 多语言 |
| 多版本站点与归档横幅 | 多版本 |
| 标签与分类 | 分类体系 |
| 编辑本页、最后修改、贡献者 | 仓库与页面信息 |
| 打印与整章导出 | 打印支持 |
llms.txt 与每页 .md 输出 |
Agent 支持 |
| 某个参数的类型与默认值 | 配置总览 |
评论、分析与部署需要接入外部服务,见维护管理。
5.1 - 配置总览
站点参数的唯一归属页。主题读取的每个键在下面某张表里有一行,给出类型、默认值与一句说明,并链接到讲它的指南页。指南页只给可粘贴的片段,不重复定义。页面级参数(front matter)见页面参数。
表格按功能分组,每组一个 ##,锚点可以引用,例如 /zh/docs/customize/config/#sidebar。默认值一栏空着表示主题没有默认值:不配置该功能就不生效。
hugo.yml 的分层
OINK 站点配置有四类键,改哪一层取决于改动目标:
| 层 | 例子 | 谁定义的 |
|---|---|---|
| Hugo 原生顶层键 | baseURL title languages markup outputs taxonomies module |
Hugo 本身,行为见 gohugo.io |
params 顶层 |
logo offline_search github_repo version page_width comments |
主题读取的站点级选项 |
params.ui.* |
navbar_enabled sidebar_width_min typography pager_types |
外壳、导航与阅读界面 |
params.<运行时> |
mermaid plantuml drawio markmap |
各内容运行时自己的开关与端点 |
最小的可用配置只需要前两层:
配置原则
-
主题默认保守,只写要改的键。交互功能(本地搜索、图片缩放、评论、反馈、深浅色菜单)默认关闭,主题不替站点做策略决定。从一份「完整配置」逐条删减,比按需添加更容易留下用不上的键。
-
没有主题总开关。不存在
oink.enabled,也没有params.oink.*命名空间,更没有在「Docsy 外壳」与「OINK 外壳」之间切换的选项。这一页查不到的开关即不存在。 -
非法值告警并回退到文档里写明的默认值。
params.ui.typography: solarized报invalid params.ui.typography "solarized" (allowed: technical | system) -- using "technical",站点照常构建;footer_style: thin、page_width: huge、section_index: grid同理。一个笔误因此只降级一个设置,而不是让hugo server下每个 URL 都返回 HTTP 500。它也不会因此静悄悄上线:所有发布关卡都带--panicOnWarning构建,那条警告在那里仍然是硬失败。 -
有一条警告保留取值而不是丢弃它。主题读出的
theme_color若在它自己的画布上低于 AA 正文对比度(4.5:1),颜色照常生效 —— 自定义画布或品牌强制色是作者的决定 —— 但会说出来,并打印可以让它闭嘴的ignoreLogsid。把它当建议而不是拒绝:要么换个更深的颜色,要么加一行配置,在你做出选择之前发布关卡会一直卡住构建。只有解析不出来的十六进制才会被真正丢弃,那种情况和其他非法值一样回退到默认配色。 -
主题自身从不中断构建。它的模板里没有任何
errorf:每个非法值都走上面的告警并回退。需要外部端点的功能——PlantUML、Draw.io、Algolia——缺少端点时告警并保持关闭,因为主题不会代为连接公共服务;残缺的上游署名告警并略去整条声明,因为半条读起来和完整的一模一样。真正会中断构建的来自 Hugo 而非主题:解析不到目标的内容引用,以及低于module.hugoVersion.min的 Hugo 版本。
页面级覆盖优先级
Hugo 的 .Param 查找让大部分参数可以逐页覆盖,优先级从高到低:
- 页面自己的 front matter;
- 祖先分区
_index.md里的cascade(离页面越近越优先); - 站点
params。
写进 front matter 时要去掉 ui. 前缀。
站点上的 params.ui.reading_time 在页面里就写成 reading_time。front matter 里出现 ui:
块的话,里面的键没有人读,也没有人报错——某个设置看着没生效时,先对照页面参数核一遍键名。
分区级用 cascade 一次设定整棵子树:
覆盖用于真实的内容差异。逐页重建一套视觉系统的配置,会在主题升级后失配。
三项 goldmark 前置
Hugo 不会 把主题模块的 markup 配置合并进站点,这三项必须写在站点自己的 hugo.yml 里,否则属性行、组件 HTML 与数学公式都不工作:
缺 attribute.block 时,{.fields} {.steps} {caption=…} 会原样显示成文字;缺 passthrough 时 \(x\) 不会变成公式;缺 unsafe 时步骤与卡片的结构会被转义。
renderer.unsafe: true 同时允许 Markdown 正文里的原始 HTML 通过,面向的是受信任的作者,不是投稿过滤器。内容来自不可信来源时,审查应放在提交流程里。
站点身份与品牌
Hugo 原生顶层键:
title,- 站名,显示在顶栏、
<title>与页脚 baseURL,- 生产域名;子路径部署时带上路径段
copyright,- 版权行的兜底值,
params.copyright未设时按 HTML 原样渲染 enableGitInfo, ,- 打开后才有「最后修改」与 commit 信息
enableRobotsTXT, ,- 生成
robots.txt enableEmoji, ,- 允许
:smile:简码
主题参数:
params.logo, ,- 品牌图标,可指向
assets/资源或static/路径,见品牌外观 params.wordmark,- 横向字标;设置后顶栏用它替代「图标 + 站名」
params.description,- 站点描述,页面没有
description时作为 meta 兜底 params.copyright,- 字符串按 Markdown 渲染;map 接受
authorsfrom_yearto_year(present表示今年) params.footer_center_info, ,- 页脚中间的行内 Markdown,设为空字符串即隐藏
params.author,- RSS 的作者;map 接受
name与email params.ui.theme_color,#rgb/#rrggbb十六进制色,为外壳的强调底着色;正文链接与行内代码不受影响 —— 见品牌外观params.ui.theme_color_dark, ,- 强调色的暗色一半;省略时从
theme_color提亮派生,直到在暗色画布上达到 AA
favicon 没有参数:主题按约定名扫描 static/(favicon.ico favicon.svg favicon-NxN.png apple-touch-icon.png apple-touch-icon-NxN.png),见品牌外观。
外壳类型与栏目根
外壳按 页面 type 生效,不看路径。文档可以放在任意目录,再用 cascade 给它 type: docs。
params.ui.shell_types, ,- 哪些 type 使用带侧栏的阅读外壳,见布局与页面类型
params.ui.docs_section, ,- 文档栏目的根目录名,只用于导航解析
params.ui.blog_section, ,- 博客栏目的根目录名
params.ui.docs_sidebar_root, ,section时 docs 页的侧栏根是文档栏目;home时是站点首页。非法值告警并回退params.ui.quick_links, ,- 命令面板空查询时列出的顶层菜单 identifier,见命令面板
params.ui.sidebar_root_enabled, ,- 允许子分区用
sidebar_root_for: self自成一棵侧栏树 params.ui.sidebar_root_menu, ,- 侧栏顶部显示栏目切换器;只有一个入口时退化为普通链接
params.ui.section_index, ,- 栏目首页子页列表样式:
list或cards,可按分区覆盖 params.ui.section_index_columns, ,section_index: cards时的列数
博客
七个键决定博客栏目的样子。它们作用于 params.ui.blog_section 指定的栏目,每一个都能通过博客根目录的 front matter 或 cascade 按栏目覆盖。
params.ui.featured_image, ,- 文章正文里怎么渲染自己的题图:
none不渲染,banner在标题上方框出一张 16:9 的图,wash把它铺在文章头部背后、只留十分之一的不透明度,hero把它作为外壳自己的通栏背景铺开并把开头下移——单页与栏目列表页都一样。用的就是这一页在卡片与og:image里已经在用的那张图,两处不会打架。没有题图的文章在任何模式下都不渲染任何东西 params.ui.blog_index, ,- 博客栏目列表页的形态:
list是行列表,cards是内容卡片网格,卡片带 16:9 题图、日期与栏目行,以及三行摘要,table是每篇一行的紧凑表格——整个栏目一次列全,不按年分组,也不分页。按年分组、分页与manual_link在list与cards下行为一致 params.ui.blog_index_columns, ,blog_index: cards时的列数;md 到 xl 之间恒为两列,md 以下一列,不受此值影响params.ui.blog_index_size, ,list与cards索引每页的文章数;table形态总是列全。12 能被 2、3、4 整除,卡片行不会缺角params.ui.blog_index_toggle, ,- 让读者从索引工具栏在列表、卡片、表格之间切换。默认关闭,因为它会把三种形态都放进文档——隐藏的那些不加载图片,但标记是真实存在的
params.ui.toc_style, ,- 右栏的呈现方式:
fixed是钉在视口上的面板,flow是跟随内容流、从文章开头处开始、滚动后才钉住的宽面板 params.ui.toc_taxonomies, ,- 右栏的分类词云。既没有目录也没有词云的右栏不会渲染任何东西
作者与系列是 taxonomy 而不是参数,见分类法与写博客。
顶栏与页脚
params.ui.navbar_enabled, ,- 是否渲染站点顶栏,可用页面顶层
navbar_enabled覆盖,见导航与菜单 params.ui.navbar_autohide, ,- 顶栏收到视口上方,指针进入唤醒区才出现;小于 768px 或粗指针时不生效
params.ui.footer_style, ,fat多列网格 + 版权行,slim只有版权行,none不渲染。非法值告警并回退params.ui.dark_mode, ,true同时启用深色调色板与主题控件;只要控件写dark_mode: { show_menu: true }params.ui.breadcrumb, ,- 面包屑;设为
false关闭。顶层分区本来就省略只有一级的面包屑 params.ui.page_context_menu.enable, ,- 标题旁的页面操作拆分按钮
params.ui.page_context_menu.assistant_links, ,- 显示「在 ChatGPT / Claude 中打开」;读者点击时完整 URL 会离开本站
params.ui.page_context_menu.links, ,- 自定义外部操作,
url支持{url}{title}{markdown_url}占位符 params.ui.github_stars,- 顶栏 GitHub 徽标上的星数,本地常量,不发请求
params.ui.alt_site,- 单语言站在页脚显示的姊妹站链接,必填
label与绝对http(s)的url
胖页脚的列数据来自 data/footer/<语言>.yaml,不是参数,见导航与菜单。
侧栏
params.ui.sidebar_menu_compact, ,- 只展开当前分支与邻近条目
params.ui.sidebar_menu_foldable, ,- 允许读者展开/折叠分区
params.ui.sidebar_menu_truncate, ,- 一个分区最多渲染的条目数,超出截断
params.ui.sidebar_cache_limit, ,- 页数达到此值后,按语言、导航根与有效设置复用可见的中性导航标记;浏览器补 active 状态
params.ui.sidebar_width_min, ,- 桌面端拖拽调宽的下限,像素
params.ui.sidebar_width_max, ,- 拖拽调宽的上限,像素
params.ui.sidebar_item_overflow, ,ellipsis长标题省略,wrap换行params.ui.sidebar_icon_policy, ,- 图标密度:
all全部、groups只有根与有子页的节点、none全不显示。非法值警告并回落all params.ui.sidebar_expand_levels, ,- 默认展开的树层级数
params.ui.sidebar_headings, ,- 只对
type: book生效:在侧栏当前行下展开标题分支;整数取值 2–4,true等于 2 params.ui.sidebar_enabled, ,- 左侧栏;设为
false关掉,通常按页面而不是按站点设置 params.ui.taxonomy_icons,- 按分类复数名指定右栏分组图标,例如
tags: fa-solid fa-tags
侧栏怎么用见布局与页面类型;目录树本身由 content/ 的结构决定,见组织内容。
目录 TOC
右栏大纲的层级由 Hugo 原生配置决定,主题只控制跟踪行为:
markup.tableOfContents.startLevel, ,- Hugo 原生:收录的最高标题级别
markup.tableOfContents.endLevel, ,- Hugo 原生:收录的最低标题级别
params.ui.scroll_spy, ,- 1.x 静默兼容 no-op;普通外壳运行时始终跟踪当前大纲标题,此键不加载资源
单页隐藏大纲用 front matter notoc: true,见页面参数。
翻页与页尾
页尾组件顺序固定为分享 → 反馈 → 页面信息 → 翻页 → 评论,五者独立开关;反向链接在右栏目录旁。
params.ui.share, ,- 页尾分享目标,按给定顺序渲染,取值来自
xblueskymastodonfacebooklinkedinreddithackernewstelegramwhatsapplinepinterestweibochatgptclaudeemailcopy。为空则不出现分享栏。每一项都是纯粹的 intent 链接——没有 SDK、没有 iframe、没有第三方脚本、没有分享计数,见写博客。未知目标告警并丢弃 params.ui.pager_types, ,- 哪些 type 显示上一页/下一页;单页用 front matter
pager: false退出。未知 type 告警并丢弃 params.ui.annotation, ,- 正文末尾的「最后修改」与出处区块;上游署名由页面的
upstream_link一族键驱动,见页面参数 params.ui.backlinks, ,- 在右栏目录旁以「反链」组列出链接到本页的页面,构建时从普通链接派生,见导航与菜单
params.ui.translation_notice, ,- 权威版本的语言代码,译文页据此显示一条指回原文的说明;页面写
translation_notice: false退出 params.ui.reading_time, ,- 页面标题下显示阅读时长
params.ui.book_draft_banner, ,- Book 草稿页开头额外加一条横幅
搜索与命令面板
本地搜索默认关闭;打开后命令面板才会出现(顶栏放大镜、Cmd/Ctrl 加 K、/、\)。
params.offline_search, ,- 生成每语言一份本地索引并启用命令面板,见全文检索
params.offline_search_on_serve, ,hugo server预览时也构建索引,预览行为与线上一致;站点极大时设false跳过以加快本地重建params.offline_search_index, ,- 索引范围,逐级累加:
titleheadingsummarycontent。非法值告警并使用content params.offline_search_summary_length, ,summary档摘录截断的字数params.offline_search_max_results, ,- 结果条数上限,同时约束 Lunr 与中文子串兜底
params.ui.landing_search, ,layout: landing页面是否保留搜索入口params.ui.command_palette.commands, ,- 自定义命令,每条二选一:
url或内置action;见命令面板 params.gcs_engine_id,- Google 可编程搜索引擎 ID,启用后引入外部服务
params.search.algolia,- Algolia DocSearch,必须显式给出
appIdapiKeyindexName,缺一则告警并保持 DocSearch 关闭
自定义命令的每条记录只接受 id title description icon keywords url action 七个键;id 必须匹配 ^[a-z][a-z0-9_-]*$,且不能与内置动作 ID 重名。分语言的标题写在 languages.<lang>.params.ui.command_palette.commands。
键盘
params.ui.keyboard_nav, ,- 单键导航(WASD/方向键走树、j/k 跳标题、q/e 翻页、面板与外壳开关)。设为
false后运行时不进包,见键盘导航
图片缩放
params.ui.image_zoom, ,- 允许正文图片点击放大;页面用 front matter
image_zoom覆盖。非布尔告警并回退
哪些图片会成为缩放候选见图片。
字体排版
params.ui.typography, ,technical用随主题分发的 Inter / Chakra Petch / IBM Plex Mono;system只用平台字体栈,不请求品牌字体。非法值告警并回退params.ui.fonts,- 为
uibodyheadingcodedisplaymetaprint七个角色指定字体族。主题校验名称但不加载字体文件;每份列表都应以通用字体族收尾 params.page_width, ,- 外壳整体宽度:
normalwidefull,可逐页覆盖 params.reading_width, ,- Book 页正文的阅读行宽:
slimnormalwide,不影响外壳
读者系统已有字体,或者站点已经用 @font-face 声明时,可以直接写
params.ui.fonts。随站点分发字体文件与更底层的排版调整仍走 SCSS/CSS 入口,见
品牌外观。
评论与反馈
params.comments.enable, ,- 站点级评论开关,页面用 front matter
comments覆盖,见启用评论 params.comments.type, ,- 目前只有
giscus会真正渲染 params.comments.giscus.repo,- 承载讨论的 GitHub 仓库,必填
params.comments.giscus.repoId,- 仓库 ID,必填
params.comments.giscus.category,- 讨论分类名,必填
params.comments.giscus.categoryId,- 讨论分类 ID,必填
params.comments.giscus.mapping, ,- 页面与讨论的映射方式
params.comments.giscus.term,mapping为specific或number时的讨论标题或编号;不设置时不输出这个属性params.comments.giscus.strict, ,- 严格标题匹配
params.comments.giscus.reactionsEnabled, ,- 显示主贴表情
params.comments.giscus.emitMetadata, ,- 向父页面发送讨论元数据
params.comments.giscus.inputPosition, ,- 输入框在评论列表上方还是下方
params.comments.giscus.theme, ,- giscus 主题,
auto跟随站点深浅色 params.comments.giscus.lightTheme, ,- 浅色模式下使用的 giscus 主题或自定义 CSS URL
params.comments.giscus.darkTheme, ,- 深色模式下使用的 giscus 主题或自定义 CSS URL
params.comments.giscus.loading, ,- iframe 加载策略
params.comments.giscus.lang, ,- giscus 界面语言。不设置时中文站解析为
zh-CN/zh-TW/zh-HK,其它语言取主语言代码,giscus 不支持则回落en params.comments.giscus.ariaLabel, ,- 评论区容器的
aria-label;默认值是英文,多语言站点需按语言各写一份 params.comments.giscus.errorMessage, ,- 加载失败时显示的文字;默认值是英文,多语言站点需按语言各写一份
params.ui.feedback.enable, ,- 页尾「这页有帮助吗」两个按钮;无后端,有
gtag时记录结构化事件 params.ui.feedback.reasons, ,- 选「否」后展开四个可选原因
四个 giscus 必填项缺任意一个,评论区就不渲染:不报错,也不出现。
仓库链接与页面信息
params.github_repo,- 内容仓库 URL,解析「编辑本页」「查看历史」「新建子页」「提文档 issue」,见仓库与页面信息
params.github_project_repo, ,- 产品仓库 URL,用于「提项目 issue」与顶栏 GitHub 入口
params.github_branch, ,- 编辑链接指向的分支
params.github_subdir,- 内容站在 monorepo 里的子目录
params.path_base_for_github_subdir,- 源路径重写;map 形式接受
from与to params.github_url, ,- 已移除,改写
params.github_repo。那份负责提示替代键名的迁移登记表已经删掉,所以旧键现在只是一个没人读的键 params.ui.lastmod_commit, ,- 「最后修改」后面附什么:
subjectcommit 标题、hash短哈希、none不附。非法值告警并回退 params.images, ,- 站点级社交卡片:页面自己没有封面时用它填
og:image;只进元数据,不会渲染成列表缩略图 params.upstream_source, ,- 声明了
upstream_link的页面默认使用哪个data/upstreams记录;页面 front matter 可以覆盖 params.upstream_modified, ,- 上游材料是否经过改编的站点默认值;页面可以覆盖,没有
upstream_link时不渲染署名 params.default_featured, ,- 已移除,改写
params.images或栏目cascade里的images。同上,旧键现在只是一个没人读的键
内容运行时
Mermaid、KaTeX、ECharts、Infographic、Asciinema、Swagger UI 与 Redoc 按内容自动检测,只有用到它们的页面、且只在该页的 HTML 输出里加载,没有站点开关。需要开关或外部端点的只有这几个:
params.markmap, ,- 站点级启用思维导图围栏,见思维导图
params.mermaid,- 透传给
mermaid.initialize()的配置;键名全小写,深色模式自动覆盖theme params.plantuml.enable, ,- 启用 PlantUML 围栏,见 PlantUML
params.plantuml.svg_image_url,- PlantUML 服务的 SVG 端点,启用时必填,缺失则告警并保持 PlantUML 关闭
params.plantuml.svg,- 用内联 SVG 而不是
<img>渲染 params.drawio.enable, ,- 启用
.drawio.svg图片的编辑按钮,见 Draw.io params.drawio.drawio_server,- Draw.io 编辑器地址,启用时必填,缺失则告警并保持 Diagrams.net 关闭
params.highlight_classes, ,- 代码高亮输出 Chroma class;设
false回到 Hugo 的行内样式 params.ui.code_copy, ,- 代码块的复制按钮;设为
false全局去掉,围栏上的copy=仍然优先
数学公式不需要参数,只需要 passthrough 前置。
输出格式
主题声明自定义输出格式,但 不替站点打开:要哪种就在 outputs 里写哪种。成本
较高的聚合输出与机器可读输出始终需要显式选择。
| 格式 | 产物 | 说明 |
|---|---|---|
HTML |
index.html |
交互形态,必选 |
markdown |
index.md |
每页的纯 Markdown 版本,页面操作里的「复制 Markdown」「查看源码」依赖它,见 Agent 支持 |
LLMS |
llms.txt |
主题声明的纯文本格式,通常只挂在 home |
LLMSFULL |
llms-full.txt |
顶层栏目 opt-in:按侧栏阅读顺序拼接同一份逐页 Markdown,每种语言一份全文包 |
NAVJSON |
navigation.json |
首页 opt-in:每种语言把侧栏 / 翻页使用的导航权威序列化一次,由 schema/nav.v1.schema.json 校验 |
print |
_print/index.html |
主题声明的整分区打印页,见打印支持 |
BookManifest |
book.json |
Book 根 opt-in,向 EPUB/PDF 打包工具交接的 JSON;本身不是电子书 |
RSS |
index.xml |
Hugo 原生,挂在 section 上让每个栏目都有订阅源 |
LLMSFULL 与 BookManifest 写在对应顶层栏目的 front matter outputs 中,
NAVJSON 写在 outputs.home。完整示例与限制见 Agent 支持
和书籍出版。
打印输出的两个参数:
params.print.toc, ,- 打印页开头生成目录;设为
false不生成 params.print.section_break_wordcount, ,- 打印页中一节多少词以上才另起一页
多语言与版本
语言用 Hugo 原生的 languages 块定义,主题只读它建立的翻译关系:
defaultContentLanguage, ,- 不带路径前缀的首要语言
languages.<lang>.label,- 该语言的自称,显示在语言菜单里
languages.<lang>.locale,- 完整 locale,用于
<html lang>与 SEO languages.<lang>.weight,- 语言顺序,也是点击语言图标时的循环顺序
languages.<lang>.title,- 该语言的站名
languages.<lang>.direction, ,- RTL 语言设为
rtl
写作侧的对等文件、锚点对齐与缺译回退见多语言。
版本相关参数:
params.version,- 当前站点变体的版本标识(不一定是 Git ref),见多版本
params.version_menu, ,- 版本菜单的标题
params.version_menu_pagelinks,- 切版本时先尝试目标站点的同一路径
params.versions,- 版本条目:
versionurlkind,name: '---'是分隔线 params.archived_version,- 顶部显示「这是归档版本」横幅
params.url_latest_version,- 归档横幅里指向最新版的链接
params.time_format_blog, ,- 博客日期格式,按语言覆盖
params.time_format_default, ,- 其它日期格式,按语言覆盖
其它
通过生成式 Schema 获得编辑器补全
主题在其 schema/ 目录下携带两个生成的 JSON Schema:校验站点 hugo.yaml 的
site-params.schema.json 与校验页面 front matter 的
front-matter.schema.json。它们是主题自身 hugo.yaml 默认值(注释即悬浮文档)
与参数扫描注册表的投影;主题 CI 会重新生成并在漂移时失败,因此它们永远不会与你
pin 的主题版本相左。
配合 VS Code YAML 扩展,在设置中映射站点 Schema:
把 URL 里的 main 换成你的发布 tag,与 go.mod 的 pin 保持一致。front matter
补全取决于你的 Markdown 工具链,用同样方式指向 front-matter.schema.json 即可。
front-matter Schema 刻意不带类型约束,因为 share、theme_color 这类键在常规
类型之外还接受裸布尔退出。
验证配置变更
改完配置跑一次严格构建:
输出 Total in … 且没有 ERROR / WARN 才算通过。常见报错与原因:
| 报错片段 | 原因 |
|---|---|
invalid params.ui.typography |
预设只有 technical 与 system |
invalid footer_style … (allowed: fat | slim | none) |
页脚形态写错,报错会指出是哪个页面 |
invalid page_width … (allowed: normal | wide | full) |
页宽写错 |
invalid params.ui.section_index … (allowed: list | cards) |
栏目首页样式写错 |
invalid params.offline_search_index |
索引范围只有 title heading summary content |
params.plantuml.enable requires an explicit params.plantuml.svg_image_url |
开了 PlantUML 却没给端点 |
params.drawio.enable requires an explicit params.drawio.drawio_server |
开了 Draw.io 却没给服务地址 |
params.search.algolia requires explicit appId, apiKey, and indexName |
Algolia 三项必须齐全 |
params.ui.image_zoom must be a boolean |
写成了字符串 "true" |
theme_color … is not a #rgb or #rrggbb hex color |
值不是十六进制颜色,保留默认配色 |
theme_color … reads at about N:1 against the theme's … canvas |
建议性告警:颜色照常生效,消息里带着让它闭嘴的 id |
theme_color_dark … has no theme_color to pair with |
只设了暗色一半而没有有效的 theme_color;该值被忽略,两种模式都保留默认配色 |
command … must define exactly one of url or action |
自定义命令同时给了 url 和 action,或两个都没给 |
invalid params.ui.sidebar_icon_policy …; using all |
只是警告,但取值拼错了 |
配置改动还要至少验证三件事:每种语言各一页、缺译页的回退、生产 baseURL 下的链接(子路径部署容易漏)。
主题声明的 Hugo 下限是 0.160.1。OINK 的持续测试工具链固定为 Hugo Extended
0.165.0;配置改动只使用这个固定版本测试一次,不再运行版本矩阵:
下限版本写在主题的 hugo.yaml 与 theme.toml 里,站点自己的
module.hugoVersion.min 应与它一致。它仍是消费站兼容性声明,不再是第二个常规 CI
测试项。
相关
5.2 - 品牌外观
本页覆盖站点外观:站名与 Logo 写在 hugo.yml,配色与字体走 SCSS 入口,页宽与页脚形态是参数。前提是站点已能构建(十分钟上手)。
需要改动的文件有四个:hugo.yml、static/ 下的图标、assets/scss/_variables_project.scss、assets/scss/_styles_project.scss。不要改主题目录里的文件:主题是 Hugo Module,升级时整个目录会被替换。
站名
站名出现在顶栏、浏览器标题与页脚。多语言站每种语言各写一个:
顶层 title 是兜底,languages.<lang>.title 优先。
Logo 与字标
主题默认使用自带的 assets/icons/logo.svg。替换步骤是把图标文件放进站点的 assets/ 或 static/,再在配置里指向它。
params.logo是方形图标,顶栏、侧栏与页脚共用。放在assets/下会经过 Hugo 资源管线(可指纹化),放在static/下按原样发布;两种写法都是相对assets/、static/根的路径。params.wordmark是横向字标。设置后顶栏用它替代「图标 + 站名」,窄屏放不下时回落到params.logo。不设置则保持「图标 + 站名」。
源 SVG 应紧贴图形边缘裁切,否则各处尺寸对不齐。SVG 必须带 viewBox,颜色继承 currentColor,或者在深浅色下都有足够对比度。
本站两个参数都不设:顶栏用主题自带的 assets/icons/logo.svg 搭配以展示字体渲染的站名。
favicon
favicon 没有参数。主题扫描站点 static/ 目录里的约定文件名,发现哪个就在每个页面输出对应的 <link>:
| 文件 | 生成的链接 |
|---|---|
static/favicon.ico |
rel="icon" |
static/favicon.svg |
rel="icon" type="image/svg+xml" |
static/favicon-32x32.png |
rel="icon" 带 sizes,按尺寸升序输出 |
static/apple-touch-icon.png |
rel="apple-touch-icon" |
static/apple-touch-icon-180x180.png |
rel="apple-touch-icon" 带 sizes |
够用的最小组合是 favicon.ico + favicon.svg + apple-touch-icon.png。带尺寸后缀的文件必须是正方形(NxN),否则不会被识别。
这些文件用任意图形工具生成即可。主题不需要 Node.js,Hugo 只发布 static/ 里已经存在的文件。
Web App Manifest 一类的额外 head 元数据不在扫描范围内,用 layouts/_partials/hooks/head-end.html 钩子自行输出;要改变发现规则本身(换目录、增加文件名),在站点 layouts/ 下覆盖 layouts/_partials/favicons.html。
主色与配色
配色分两层:Bootstrap 的语义色(编译期 Sass 变量)和 OINK 的品牌层(运行期 CSS 自定义属性)。
先改语义色,它决定按钮、链接、提示块的色调:
这个文件在 Bootstrap 与 OINK 默认值 之前 加载,是覆盖 Sass 变量的位置。需要引用 Bootstrap 已定义的变量或 map 时,改用 _variables_project_after_bs.scss。
品牌层是一组 CSS 自定义属性,浅色和深色 必须成对覆盖,否则一种模式下会漏色:
可覆盖的品牌属性有 --td-brand-elev(浮层底色)、--td-brand-silk(次要文字)、--td-brand-copper 与 --td-brand-copper-dim(强调色与它的弱化版)、--td-brand-line-strong(分隔线)、--td-brand-header-bg(顶栏背景)、--td-brand-shadow-sm / --td-brand-shadow-md(阴影)、--td-brand-mark-from / --td-brand-mark-to / --td-brand-mark-gradient(品牌渐变)。
分区主题色
上面的品牌配色决定整站的颜色。theme_color 是它旁边一件更小的乐器:一个十六
进制色,为外壳的强调底着色,让读者不用被告知也知道自己身在站点的哪一块。
它按分区写比按站点写有用得多。写进分区根的 cascade,整个分区就有了身份 ——
藏青的文档、紫色的博客、橙色的教程 —— 而站点默认仍是品牌色:
Hugo 会把这些 cascade 值同时解析到分区首页与子页,因此只声明这一对即可。同一对 解析结果既驱动页面强调色,也驱动根切换器里该分区的图标。
它作用于:侧栏选中行、以及指针划过其它行时那一层更灰的底、hover 淡铺、 页面目录的药丸与那条会走的轨道和光点、指针落在其上的 Book 章节小标题、标签与 徽章的 hover、内容卡片 hover 时的外边、分享按钮 hover 时的实心底、文本选中、 焦点环,以及侧栏根切换器里每个分区的图标。
它刻意不作用于:正文链接、外链、行内代码。这些是阅读约定,不是品牌表面 —— 一页密集的标识符在任何分区都该读成「代码与正文」,链接在哪里都该看起来像链接。 这也是强调色单独占一个自定义属性、而不是去重刷 Bootstrap 链接色的原因。
暗色一半是可选的。省略时,从亮色向白提亮,直到在暗色画布上达到 AA 正文对比度,
所以只填一个颜色的作者不可能产出不可读的暗色配色。派生结果不再符合期望的品牌
色相时,再自己指定暗色一半。亮色才是主键:单独设置 theme_color_dark,或者把它放在
一个非法的 theme_color 旁边,两种模式都不会着色 —— 主题会发出警告并保留默认配色,
而不是只给暗色模式上色。
彩色栏目里的某一页可以用主题的裸布尔惯例谢绝颜色:front matter 写
theme_color: false 即让该页退出继承的栏目色(含继承的暗色一半),静默回到默认
配色,不产生警告。其他非十六进制取值(数字、true、颜色名)都会告警。
主题会把你的颜色放在它自己的画布上读,低于 AA 正文对比度(4.5:1)就告警。
颜色照常生效:自定义画布或品牌强制色是你的决定。告警里带着能让它闭嘴的
ignoreLogs id;而发布构建带 --panicOnWarning,所以在你要么调深颜色、
要么关掉检查之前,关卡会一直卡住。
这个检查是拿颜色对着页面画布读的。有些交互表面会同时把它用作文字与半透明淡铺; 例如可点击的实心徽章在 hover 时,是强调色文字压在 12% 的同色淡铺上。这一对比 画布检查更紧。如果颜色只是刚好过线,还要检查这些表面,必要时再调深一档。
Hugo 按键合并参数:某一页在同时设了 theme_color_dark 的分区里只覆盖
theme_color,会继承那个暗色。要么两个都覆盖,要么都不覆盖。
深浅色模式
主题默认 不显示 深浅色控件。开启方式:
开启后顶栏出现一个主题控件:点击在浅色与深色之间切换,悬停或键盘聚焦展开「跟随系统 / 浅色 / 深色」。读者的选择存在浏览器本地,没有选择时跟随 prefers-color-scheme。切换脚本在首屏绘制前设置好 data-bs-theme,不会出现主题闪烁。
只要深色调色板、不要控件时写 dark_mode: { show_menu: false, enable: true };dark_mode: false(默认)两者都不启用。
自定义组件在两种模式下都要给出可读的悬停、聚焦、禁用、选中状态,正文对比度至少 4.5:1、大号文字 3:1。
字体
字体有两档预设,在构建期决定,不涉及 JavaScript:
technical(默认):界面与正文用随主题分发的 Inter(可变字重,拉丁 / 西里尔 / 希腊 / 越南语子集,中文与 emoji 落到平台字体),标题装饰用 Chakra Petch,代码用 IBM Plex Mono。字体文件都是本地的,不请求 Google Fonts。system:界面、展示、元数据、打印与等宽角色全部回到平台字体栈,浏览器不请求品牌字体。字体文件仍随主题分发,只是不被引用。
非法取值告警并回落到 technical,普通 hugo server 照常可用;发布门禁开着 --panicOnWarning,这类告警在那里才是硬失败。选中的值写入 <html data-td-typography="…">,可在浏览器中确认。
自定义字体
字体角色是七个 CSS 自定义属性,覆盖它们即可,不必查找组件选择器:
| 属性 | 配置键 | 用在哪 |
|---|---|---|
--td-ui-font-family |
ui |
导航、控件与界面文字 |
--td-body-font-family |
body |
正文与博客 |
--td-heading-font-family |
heading |
正文标题 |
--td-code-font-family |
code |
代码与终端 |
--td-display-font-family |
display |
字标与展示型大标题 |
--td-meta-font-family |
meta |
技术标签与元数据 |
--td-print-font-family |
print |
打印正文 |
ui 是主字体:body 经它解析,heading 又经 body 解析,所以只写 ui 一行,界面、正文与标题一起换掉。
在配置里换
只是想换一套字体族,不必碰 SCSS,写 params.ui.fonts 即可:
这里写的是字体族名,不是字体文件。主题不会因为这个键去下载或加载任何字体:所写的族必须是读者机器上已有的,或者站点自己在样式表里 @font-face 声明过的。所以每个列表都要以通用族(sans-serif、monospace、serif)收尾——读者没有你写的字体时,落到那里。
取值只放行纯粹的字体族语法:带引号的名字、裸标识符、允许前导连字符(-apple-system),以及任何文字系统写成的名字(苹方 合法)。分号、花括号、括号、url()、尖括号一律不过关。未知角色或不合法取值只告警并单独丢弃,同一份 map 里其余的行照常生效。什么都不设时,<head> 里连这个 style 元素都不会出现。
该块在样式表之后输出,这正是作者字体能在同等优先级下压过 typography 预设的原因。
在样式表里换
要自带字体文件,或者只给某一类内容换字体,仍然走样式表。把 .woff2 放进站点 static/webfonts/,在项目样式里声明字面,再改写角色:
角色按 CSS 规则继承,只给某类内容换字体也不必复制组件选择器:
等宽字体要带中文兜底,否则中英混排的代码块会对不齐:
从 Docsy 迁移过来的站点不必改写法。旧的 Sass 变量仍然喂进对应角色,写在 _variables_project.scss 里照样生效,优先级高于预设默认值:
| 旧 Sass 变量 | 喂给的字体角色 | 说明 |
|---|---|---|
$td-fonts-serif |
--td-ui-font-family / --td-body-font-family |
Docsy 的界面字体栈,赋值给 $font-family-sans-serif |
$font-family-sans-serif |
--td-ui-font-family / --td-body-font-family |
项目给出自己的栈时,technical 预设不再把 Inter 放在它前面 |
$font-family-base |
--td-ui-font-family / --td-body-font-family |
Bootstrap 的正文变量,经 --bs-body-font-family 进入角色 |
$headings-font-family |
--td-heading-font-family |
不设置时标题继承正文角色 |
$font-family-code |
--td-code-font-family |
代码、终端与 pre / code / kbd |
$td-font-family-monospace |
--bs-font-monospace |
赋值给 $font-family-monospace |
$font-family-monospace |
--bs-font-monospace |
system 预设下,项目的显式取值优先于平台等宽栈 |
Docsy 的三个 Google Fonts 变量 $td-enable-google-fonts、$td-google-font-name 与 $td-web-font-path 主题已不再读取。它们留在 _variables_project.scss 里不影响构建,也不产生任何效果:随主题分发的是 Inter、Chakra Petch 与 IBM Plex Mono,两档预设都不向 Google Fonts 发请求。打印角色 --td-print-font-family 跟随正文角色,主题不为纸张单独提供字体。
YAML 里只接受字体族名。远程字体 URL 与任意 CSS 都不接受:字体文件与样式必须是可审查的本地输入,一次普通构建不会因为字体发出任何网络请求。
页宽
page_width 控制外壳整体宽度,可逐页或按分区 cascade 覆盖。Book 页另有一个
reading_width(slim / normal / wide),改的是正文阅读行宽,不是外壳。
两个键取值非法都会在普通预览中告警并回退;带 --panicOnWarning 的发布构建会失败。
页脚
fat(默认):多列链接网格 + 版权行;slim:只有版权行;none:不渲染页脚。
页面 front matter(含分区 cascade)可以覆盖它,本站的文档栏目用的是
footer_style: slim。无法识别的取值在普通预览中告警并回退到 fat,严格发布构建
拒绝这条警告。
多列网格的数据在 data/footer/<语言>.yaml,写法见导航与菜单。配了 fat 但没有数据时自动降级成 slim,可以先开启再补内容。
params.copyright 接受 Markdown 字符串,或 authors / from_year / to_year 三键的 map(present 表示今年)。footer_center_info 是页脚中间的行内 Markdown,显式设为空字符串即隐藏中间区域。
SCSS 入口与不该做的事
站点的 SCSS 覆盖进入主题的同一个样式包,生产构建仍然只有一份带指纹与完整性校验的样式表。三个入口文件放在站点 assets/scss/ 下:
| 文件 | 什么时候用 |
|---|---|
_variables_project.scss |
在 Bootstrap 与 OINK 默认值之前设置 Sass 变量($primary、字体变量) |
_variables_project_after_bs.scss |
设置依赖 Bootstrap 已有定义的变量或 map |
_styles_project.scss |
在主题组件样式之后写选择器与 CSS 自定义属性 |
编译顺序是:Bootstrap 函数 → 项目变量 → OINK 默认值与 Bootstrap → Bootstrap 之后的项目变量 → OINK 组件与品牌层 → 项目样式。
CSS 接口有明确边界。字体那一节的七个字体角色与 --td-brand-* 品牌属性是公开接口,主题在小版本之间保持它们的名字与含义。组件别名(如 --td-asciinema-font-family)只承诺在该组件范围内有效,未在文档中记录的 --td-shell-* 一类变量是实现细节,随时可能改名或消失。
不该做的事:
- 不改主题目录里的任何文件(
hugo mod会覆盖); - 不单独
@import主题的内部 partial,它们不是公开的 Sass 接口,导入顺序可能变化; - 不为了改一个颜色去覆盖
baseof.html。有设计变量就用变量,没有再写作用域尽量小的选择器; - 不引用远程样式表或字体 CDN。
需要额外的第三方 CSS 时,用 layouts/_partials/hooks/head-end.html 钩子发布本地资源,不在 Markdown 里写 <link>。
验证
- 构建输出
Total in …,没有 ERROR / WARN; - 页面源码里
<html>上有data-td-typography="technical"(或所选的预设); - 浏览器中顶栏显示自己的 Logo 与站名,标签页图标是自己的 favicon;
- 切到深色模式再看一遍正文、表格、提示块、代码块与焦点框。配色改动容易只在一种模式下验证过;
- 换一种语言,确认站名随之切换。
字体是否已替换,用浏览器开发者工具查任意一段正文的 font-family:应当是自己声明的字面,而不是 Inter。
相关
5.3 - 首页与落地页
首页不是模板,是一份数据:data/home/<语言>.yaml 里的 sections 列表决定页面从上到下有哪些分区,每个分区的内容在同一份文件里按名字取。普通页面加 layout: landing 也能用同一套分区。
分区全部在服务端渲染。价格、star 数、截图、头像、下载状态都必须在 Hugo 启动前就存在于仓库中,没有分区会在浏览器里取数据。
从 Docsy 的 blocks/* 首页迁移过来的站点要重写首页:主题没有 blocks/cover、blocks/section、blocks/feature 这些 shortcode,保留它们会让构建报 template for shortcode "blocks/cover" not found。两条出路是本页讲的 data/home/<语言>.yaml,或者给一个普通页面加 layout: landing。
首页的数据来源
首页的内容文件只留标题与描述:
分区数据按语言分文件:
首页数据
- data/
- home/
- en.yaml英文站首页
- zh.yaml中文站首页
- home/
查找顺序是 data/home/<当前语言>.yaml → data/home/en.yaml → 单语言站点的 data/home.yaml。
文件结构只有两层:一个 sections 列表,加上被列表引用的同名键。
这是本站首页的写法,完整文件见仓库的 data/home/zh.yaml。
最小可用首页
粘贴下面这段,替换文字与链接即可发布。链接写成不带前导斜杠的站内路径,主题会补上当前语言前缀(docs/start/ → /zh/docs/start/)。
Hero
Hero 是首屏,唯一一个带大标题与配图的分区。
不写 title_lines 时用 title,两者都没有时用站点标题。配图是 CSS 背景图,alt 有值时容器带 role="img",无值时对辅助技术隐藏。
align: center 是纯文字的居中首屏:文案块加宽居中,标题自动平衡换行,note
挪到按钮下方。两者同时出现时,普通预览会告警并回退到 start 以保留图片;严格
发布构建拒绝这条警告。
分区注册表
22 种分区,名字用连字符(旧数据里的下划线会被规范化)。除 Hero 之外,每种都共用 eyebrow / title / desc(或 text)三个抬头字段与一个 class。
| 类型 | 放什么 |
|---|---|
hero |
首屏:大标题、按钮、跟随主题的配图 |
metrics |
数字事实,可选计数动画与来源链接 |
capabilities |
左右交替的能力叙事 + 专用视觉面板 |
principles |
编号的产品原则 |
cards |
通用卡片集合:功能、场景、入口 |
logo-wall |
工具与伙伴,网格或纯 CSS 跑马灯 |
gallery |
截图墙 |
testimonials |
引语与署名 |
contributors |
人、角色、头像与链接 |
faq |
折叠或平铺的问答 |
markdown |
一段自由 Markdown |
cta |
结尾的行动号召 |
pricing |
价格档位卡片 |
pricing-compare |
档位功能对比矩阵 |
command-box |
一条可复制的命令 |
steps |
有序流程,可带命令 |
timeline |
带日期的里程碑 |
code-plate |
展示面板里的代码 |
preview |
一段 Markdown 源码与它渲染出来的样子并排 |
case-study |
案例:指标 + 引语 + 出处 |
download |
一个或多个 data/download/ 记录 |
bar-chart |
不用图表 JS 的数值对比 |
写错类型名不会静默消失:构建时给一条 unknown section type 警告并跳过该分区。CI 里加上 --panicOnWarning 即变成构建失败。
常用分区的最小写法
卡片与能力面板是最常用的两种。cards 用 columns 控制列数:
capabilities 是一屏一条能力,右边配一块结构化的视觉面板,visual.type 只能是 shell、components、code、image、card 五种之一:
这些片段摘自主题仓库的可执行回归夹具
tests/site/data/landing/demo/en.yaml,字段名可照抄。
download 分区消费的就是发布与下载页里那份 data/download/<key>.yaml,不引入第二套版本模型。
任意页面做落地页
普通内容页加两行 front matter 即成为落地页:全宽画布,保留顶栏、命令面板与页脚,去掉侧栏与目录。
数据放在与首页平行的目录下,同样按语言分文件:
落地页数据
- data/
- landing/
- pricing/
- en.yaml
- zh.yaml
- pricing/
- landing/
非首页落地页按这个顺序查找数据。全部找不到时,普通预览告警并渲染没有分区的 Landing 外壳;严格发布构建拒绝这条警告:
- 页面 front matter 里的
sections; data/landing/<key>/<精确语言>.yaml;- 单文件
data/landing/<key>.yaml里的精确语言条目; - 英文或无语言后缀的记录。
数据量小时可以写在 front matter 里,但 landing: 与 sections: 互斥:
分区条目写法
sections 的每一项可以是一个类型名字符串,也可以是一个 Map:
| 键 | 作用 |
|---|---|
type |
分区类型;省略时用 key 当类型 |
key |
从哪个键取数据,默认与 type 同名;同一种分区用两次时用它区分 |
data |
内联数据,不再到顶层查找键 |
id |
分区的锚点 ID,默认由 key / type 生成 |
enabled: false |
停用这个分区,保留数据 |
partial |
换成站点自己的 partial。属于本地模板约定,不是可移植的 Landing 数据 |
多语言与本地事实
叙事文字优先分语言文件(zh.yaml / en.yaml)。共享的事实记录也可以在字段级回退:<字段>_<精确语言> → <字段>_<主语言> → <字段>,语言标签里的 - 规范化成 _。中文站解析 title_zh_cn、title_zh、title。不接受 camelCase 后缀。
分区里的显示文字是站点数据,不是主题的 i18n 字符串。只有跑马灯暂停、定价状态这类主题自带控件用翻译键。多语言站点的整体配置见多语言。
落地页外壳上的几个可选事实也是本地的,写在 hugo.yml 里,运行时不会去取它们:
页脚不属于首页数据:它读 data/footer/<语言>.yaml(单语言站点用
data/footer.yaml),本站两种语言各一份。data/home/<语言>.yaml 里残留的
footer 键会在普通预览中告警并忽略,--panicOnWarning 会拒绝它并提示新位置。
写法见导航与菜单。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 完整的静态分区内容,再按需加载 landing.js 做渐显、计数、复制与主题图片切换 |
| 打印 | 内容保留,跑马灯之类的动态面变成静态网格,控件移除 |
| Markdown | 标题、正文、列表、表格与代码,不带组件 class |
| RSS | 不输出 Landing 分区 |
禁用 JavaScript 后服务端文档仍然完整。跑马灯的副本轨道不进无障碍树,暂停用的是不依赖 JavaScript 的复选框;读者开启减少动态效果偏好时,移动与渐显关闭。
验证
- 构建零告警:
hugo --printPathWarnings --panicOnWarning。类型写错、数据键不存在、landing与sections同时出现都在这一步暴露。 - 打开首页与落地页,逐个分区对照数据文件,每种语言各看一遍。
- 禁用 JavaScript 后刷新:内容仍在,只是没有动效。
- 深浅色各看一遍,确认
image.light/image.dark都给对。 - 部署到子路径时,确认站内链接与图片都带上了前缀。
相关
5.4 - 导航与菜单
本页覆盖读者在页面之间移动的入口:顶栏菜单、栏目切换器、面包屑、页面操作、上一页 / 下一页与页脚。侧栏树与目录属于布局与页面类型。
导航没有第二套信息架构:顶栏来自 Hugo 的 menus.main,侧栏来自 content/ 的目录结构。主题不读 docs.json、navigation.yaml 一类的并行导航树。
顶栏菜单
顶层入口写在各语言的 menus.main 里:
weight 越小越靠前。pageRef 指向站内页面,url 指向外链;外链自动加 target="_blank" 与 rel="noopener noreferrer",并带一个外链角标。identifier 是配置里引用这个入口的稳定标识(quick_links、sidebar_root_menu 按它匹配),name 按语言翻译,identifier 不翻译。
菜单项也可以挂在页面 front matter 上,适用于「这一页本身就是一个顶层入口」:
顶栏右侧的 GitHub 入口 不是 菜单项,它来自 params.github_project_repo(未设时回落 params.github_repo)。标识为 github 的菜单项会被菜单区跳过,写了也不显示。改变这个入口的目标要改仓库参数,见仓库与页面信息。
下拉菜单
用 Hugo 的 parent 建立父子关系,只支持一级子项:
- 每个条目都是独占一行的一个图标加一个标题,整个面板是一列宽度适中的
纵向列表。子项的
params.description只是配置数据,面板不会渲染它。 - 父级本身是一个普通链接:悬停或键盘聚焦展开面板,点击或回车进入父级页面。没有单独的展开箭头,触屏读者落到父级页面,该页正文同样列出这些链接。
- 键盘:向下箭头展开并聚焦第一项,Esc 关闭并把焦点还给链接,点击面板外部关闭。
- 0.5 的
params.columns参数已退役:设置它会发出构建警告,面板保持单列。 - 再深一层会发出构建警告并降级成静态分组标题,不会 生成三级悬浮菜单。更深的层级放进侧栏。
菜单图标
小于 lg 时菜单项只剩图标,每个顶层入口都应有一个。图标按这个顺序解析:
- 目标页面 front matter 里的
icon; - 菜单项自己的
params.icon; - 按 identifier / 分区名匹配的内置默认值(
docsblogexamplescommunityaboutdownloadgithub等); - 都没有时用
fa-solid fa-link。
图标写成一对 Font Awesome class,主题本地提供免费版字体:
标签菜单
顶层入口指向 taxonomy 页面(/tags/、/categories/)时不需要手工配置子菜单:面板自动渲染「标签 + 数量」的 chip 网格,按数量降序排列。
分类怎么启用见分类体系。
顶栏控件
顶栏高 50px,从左到右是:品牌(Logo 或字标)、菜单区、搜索、版本、语言、主题、GitHub。首页和 Landing 页面最右侧还固定保留抽屉菜单按钮。顶栏在所有布局上渲染;文档、博客和分类页使用相同控件,但没有这个 Landing 抽屉。
顶栏分为桌面完整形态与紧凑图标形态:
| 视口 | 状态 |
|---|---|
lg 及以上 |
完整:品牌、带文字的菜单项、各工具控件;首页/Landing 最后是抽屉按钮 |
小于 lg |
紧凑:品牌保留,其余全部右对齐成图标 |
小于 md |
顶栏右侧只留搜索与抽屉按钮;版本、语言、主题与快捷键帮助仍在页脚最底层栏中 |
各控件的开关不在这里:搜索图标要 params.offline_search(见全文检索),版本菜单要 params.versions(见多版本),语言菜单在配置了两种及以上语言时自动出现(见多语言),主题控件要 params.ui.dark_mode(见品牌外观)。
自动隐藏
开启后顶栏离开正常流、停在视口上方,指针进入原位置上方 60% 的中间区域(或键盘焦点进入)才滑出,并且覆盖在正文之上,不把正文顶下去。左右各 64px 不属于唤醒区,避免盖住折叠后的侧栏与大纲恢复按钮。
小于 768px、粗指针或纯触屏时自动停用,顶栏始终可见。页面 front matter 顶层的 navbar_autohide 或分区 cascade 可以逐段覆盖。
关闭顶栏
也可以只关闭某一页或某一段:
关闭后主题补回原本由顶栏承担的界面:移动端子导航、侧栏顶部的品牌与搜索行、大纲轨道上的工具按钮。这个开关适用于必须独占视口的页面,不作为常规排版偏好。本站的文档栏目使用它:文档页依靠侧栏导航,顶栏是多余的一行。
栏目切换器
侧栏顶部那一行是栏目切换器,决定当前显示哪棵树。入口集合按顺序去重构造:所有顶级栏目 → 全站所有 sidebar_root_for: self 的分区 → 当前解析出的根。
让一棵大子树自成一个根(带版本的 API 参考、独立手册),在它的 _index.md 里:
self 让这个分区索引与它的后代都用这棵新树;children 把索引留在父树里,只约束后代。让某个顶层分区不出现在切换器里,在它的 front matter 里设 sidebar_root_menu: false。
只有一个入口时切换器退化成一个无边框链接,两个及以上才是下拉菜单。切换器下面的树仍然把栏目首页本身作为第一个链接:切换器选一棵树,根链接选一篇文档。
面包屑与页面操作
普通内容页标题上方是面包屑行,这一行右端同时承载页面操作。顶层分区省略只有一级、与标题重复的面包屑,操作按钮的位置不变。
面包屑标签取本地化的 linkTitle,层级与侧栏一致。
页面操作菜单
页面操作是标题行末尾的拆分按钮:左半边一键复制本页 Markdown(成功后变成绿色对勾),右侧箭头展开完整菜单。菜单分两组,上半组是取走内容,下半组是改动与产出:
| 操作 | 出现条件 |
|---|---|
| 复制 Markdown 文本 | 站点开了 markdown 输出格式 |
| 在 ChatGPT 中打开 | page_context_menu.assistant_links: true |
| 在 Claude 中打开 | 同上 |
| 查看 Markdown 源码 | markdown 输出格式 |
| 查看编辑历史 | params.github_repo 能解析出源文件路径 |
| 编辑本页 | params.github_repo |
| 新建子页面 | params.github_repo |
| 提交文档 issue | params.github_repo |
| 提交项目 issue | params.github_project_repo |
| 打印整个分区 | 分区开了 print 输出格式 |
助手入口默认关闭:读者点击时,完整的当前 URL(含 query 与 fragment)会随本地化提示词发给第三方,页面正文不上传。开启前确认 URL 里没有敏感信息,并在隐私说明里披露这个边界。页面可以用布尔型 front matter assistant_links 收紧站点策略,不能反过来替站点开启。
自定义外部操作排在菜单最后,url 支持三个已 URL 编码的占位符:
可用占位符:{url}(页面完整地址)、{title}(页面标题)、{markdown_url}(Markdown 版地址)。
在博客根分区及其一级子分区上,左半边变成 RSS 订阅链接,菜单里仍保留「复制 Markdown 文本」。没有 Markdown 输出的页面去掉左半边,箭头变成带文字的「操作」按钮。
这些操作同时是命令面板里的条目。
翻页器
正文末尾的上一页 / 下一页是两个文本链接,顺序与侧栏可见树一致:根页 → 第一篇 → 直到最后一篇。根页没有上一页,末页没有下一页。站点提供 data/docs_nav.json 时,这棵显式树同时决定翻页顺序,以及该文件声明过的 docs / book 栏目的栏目索引顺序——侧栏、翻页器与索引不会再把同一批子页排出三种顺序。文件没有声明的栏目,以及没有这个文件的站点,仍然沿内容树走。见布局与页面类型。
pager_types 只接受 docs、book、blog 三个值,其它取值告警并丢弃。单页退出用 front matter:
同一份顺序也写进 <head>:有上一页 / 下一页时输出 <link rel="prev"> 与 <link rel="next">,供浏览器与爬虫识别阅读序列。
翻页只在 HTML 输出中生效。打印、Markdown 与 RSS 既没有翻页链接,也没有这两个 rel 关系。
翻页器是页尾四件套的第三件(反馈 → 页面信息 → 翻页 → 评论),顺序固定,四者独立开关。
反向链接
右栏可以列出有哪些页面链接到这一页:一个带链接图标的「反链」组,排在目录下方、分类标签云上方,默认展开;低于 xl 断点时,它随目录一起进入侧栏抽屉。从搜索落到这一页的读者由此看到哪些页面认为它值得指向,也看到它在站点其余部分里的位置。默认关闭,由站点打开:
单页用 front matter 覆盖,分区用 cascade 覆盖它下面的所有页面:
索引在构建时从作者本来就在写的东西里派生:页面源码里的普通 Markdown 链接,以及 ref / relref shortcode。没有新语法要学,没有内容要迁移,也不需要 JavaScript——列表就在 HTML 里。扫描前先剥掉代码围栏与行内代码;指向同一个目标的多个链接合并成一条;自链接、外链、mailto: 与同页锚点都不计入。判断目标页面时去掉 fragment,每种语言各有一张互不相干的图,中文页面不会出现在英文页面下面。条目按稳定页面路径排序,同样的内容每次构建出同样的顺序;没有任何页面链进来时整个区块不渲染——没有标题,也没有空容器。
前八条直接可见,其余折进原生的「再显示 N 条」disclosure,避免被大量引用的页面把右栏撑满,其中不涉及 JavaScript。每一条都带来源页面的描述,悬停时显示。
读源码有一处已知遗漏:写在自定义 shortcode 参数里或原始 <a href> 里的 URL 不会成为一条边;解析不出来的目标被静默丢弃,不发告警。它是导航增强,不是链接检查器,查断链仍然要用链接检查器。
非布尔取值告警并回落到关闭,hugo server 照常可用,加了 --panicOnWarning 的构建会停在这里。
页面的 Markdown 输出带同一份列表,前缀是「反链:」。RSS 省略它,print 输出格式连同整个右栏一起省略。
本站全站开启了它:看本页右栏的「反链」组就是实际效果;被引用最多的配置总览一页,列出了四十多个入链,其中大部分收在「再显示 N 条」里。
页脚
页脚形态由 params.ui.footer_style 决定(fat / slim / none,见品牌外观)。fat 的多列链接网格读 data/footer/<语言>.yaml。它不是菜单,主题没有 menus.footer:
brand.name与brand.logo不写时回落到站点自己的品牌名、Logo 与字标;tagline与slogan渲染 Markdown。- 站内
url相对当前语言根解析;external: true在新标签页打开并带rel="noopener noreferrer"。 - 网格列数等于数据里的列数。
- 单语言站可以使用
data/footer.yaml。 - 配了
fat但没有数据时自动降级成slim,可以先开启再补内容。
fat 页脚的版权行右端有一个折叠箭头,收起或恢复它上方的链接栅格。默认展开,读者的选择存在 localStorage 的 td-footer-collapsed 键里,跨页面保留;slim 与 none 没有这个按钮,它也与专注模式无关。
只要页脚有渲染,最底层栏右侧就固定保留同一组图标:版本、语言、主题、快捷键帮助。各菜单向上展开;版本按钮只显示分支图标,完整版本名仍保留在选项中。fat 页脚的折叠箭头排在这四项之后。侧栏不再重复这组控件,footer_style: none 则连同页脚一起移除底栏。
版权行与中间那句说明由参数控制,见配置总览。
验证
改完导航要检查这几处:
- 构建没有
Navbar menu … supports one interactive child level警告;出现它说明菜单嵌了三层; - 桌面端:父级菜单点击进入父级页面,悬停展开面板,Esc 关闭面板;
- 窗口缩到
lg以下:每个顶层入口仍有图标,没有图标的项在这个宽度下是空白; - 缩到
md以下:首页与 Landing 顶栏右侧只剩搜索和抽屉按钮;版本、语言、主题与快捷键帮助固定在 footer 最底层栏; - 侧栏顶部的切换器列出所有顶级栏目,当前项有选中标记;
- 任意文档页按 E / Q 翻页,顺序与侧栏一致,页面源码里有对应的
rel="prev"/rel="next"; - 打开反向链接后,
grep td-backlinks public/<某个被链接的页面>/index.html能找到这个区块,而没有页面链进来的页面里完全没有这段标记; - 打开页面操作菜单,确认该出现的项都在,不该出现的没有(例如未配置
github_project_repo时的「提交项目 issue」)。
相关
5.5 - 布局与页面类型
本页覆盖页面骨架:有没有侧栏、侧栏多宽、目录收几级、栏目首页是列表还是卡片。内容放在哪个目录见组织内容,这里只讲外壳。
规则是 外壳看 type,不看路径。文档可以放在 content/ 下的任何位置,只要给它 type: docs。
外壳类型
params.ui.shell_types 列出哪些 type 使用带侧栏的阅读外壳:
| type | 外壳 |
|---|---|
docs |
文档外壳:左侧栏(栏目切换器 + 目录树)+ 正文 + 右栏大纲 |
book |
文档外壳,另加编号目标、reading_width 阅读行宽与草稿横幅 |
blog |
文档外壳,侧栏默认展开,标题行左半边是 RSS |
swagger |
文档外壳,正文交给 Swagger UI 或 Redoc,见 API 文档 |
| 其它 type | 普通页面:顶栏 + 单栏正文 + 页脚,没有侧栏 |
分类页与标签页(taxonomy / term)不在这张表里,但也走同一套外壳。
给一棵子树指定 type 用 cascade,这是把文档放在任意路径的做法:
栏目根只是导航起点
这两个键 不决定外壳,只告诉主题文档树与博客树的根在哪,用于解析侧栏根、快捷入口与默认图标。上面 content/handbook/ 的例子照样有文档外壳,docs_section 保持 docs 不影响它。
需要让 docs 页的侧栏根变成站点首页,而不是文档栏目时:
取值只有这两个。其它值在普通预览中告警并使用 section,严格发布构建通过
--panicOnWarning 拒绝这条警告。
文档挂在站点根
以文档为主的站点可以把 docs 分区发布到 URL 根路径,源码仍然放在 content/docs/ 下。这需要三段配置一起给出。
第一段用 Hugo 原生的 permalinks 去掉 URL 里的 docs/ 段:
第二段让物理站点根索引仍可作为链接目标,但不再争抢同一个输出路径。每种语言的站点根索引(content/_index.md、content/_index.zh.md)都要写:
第三段把侧栏根声明为站点首页,让侧栏与翻页共用同一棵树:
docs_sidebar_root: home 之后,站点首页的所有顶层分区都会进入这棵树。博客、社区、下载这类不属于阅读序列的概览分区,在自己的 _index.md 里设 toc_root: true 退出,它们既不出现在树里,也不成为翻页目标:
文档此时与博客、社区等分区共享 URL 根路径。构建加 --printPathWarnings,发布前解决所有重复目标。
落地页
任意页面加 layout: landing 即使用落地页布局:顶栏 + 分区拼装的正文 + 页脚,没有侧栏。数据写法见首页与落地页。
landing_search: false 会把搜索入口从落地页外壳里去掉,其它页面不受影响。
侧栏
侧栏树来自 content/ 的目录结构,按 weight 排序,有 linkTitle 时用它作为标签。可调的是密度与尺寸:
sidebar_menu_compact只展开当前分支及邻近条目;设为false时整棵树全展开。sidebar_menu_foldable允许读者手动展开 / 折叠分区。博客栏目默认展开;某个分区要默认收起,在它的_index.md里写sidebar_expanded: false。sidebar_expand_levels是默认展开的层级数。sidebar_menu_truncate是单个分区最多渲染的条目数,避免上千页的目录把 HTML 撑到不可用。sidebar_width_min/sidebar_width_max是桌面端拖拽调宽的上下限(像素)。读者调整后的宽度存在浏览器本地,双击分隔条恢复默认。sidebar_item_overflow默认ellipsis(长标题省略),中文长标题多的站点可以改wrap换行。
达到 sidebar_cache_limit 后,有相同有效设置的页面可以共享一份中性渲染树。没有
JavaScript 时它仍然可见且可导航;外壳运行时只补当前路径并展开其祖先。页面或
cascade 覆盖会选择对应的缓存变体;启用 sidebar_headings 的 Book 页面仍使用自己
的页面专属树。
折叠状态、宽度与滚动位置保存在读者本地,按语言隔离。小于 md 时侧栏变成带遮罩的抽屉。
单页去掉侧栏用 front matter:
显式导航树 data/docs_nav.json
侧栏树默认从 content/ 推导。站点也可以给出一份显式导航清单,三个条件同时成立时主题改用它渲染:
- 站点存在
data/docs_nav.json且其中有sections键; - 页面的 type 是
docs或book; - 解析出的侧栏根不是站点首页。
文件是一棵嵌套的节点树。每个节点用 page 指向内容路径,url 是它的链接,children 是子节点;active_path_by_url 记录每个 URL 对应的祖先链,供当前项高亮使用:
URL 在比较前去掉语言前缀,一份文件服务所有语言。
这棵树同时决定翻页顺序,侧栏与上一页 / 下一页不会出现两种排序。sections 为空数组
时告警并回退到内容树;page 指向不存在的页面时告警并跳过该项。严格发布构建拒绝
任一警告。带 manual_link 的占位节点与 sidebar_divider 分隔行留在侧栏里,但不会
成为翻页目标。
适用场景是导航顺序由外部工具生成的站点,例如从 Sphinx toctree 迁移过来、需要冻结既有章节顺序的手册。顺序由 content/ 的 weight 维护时不需要这个文件。
侧栏图标密度
页面 front matter 里的 icon 会出现在侧栏。叶子页全部带图标会降低可读性,用密度策略控制:
| 取值 | 效果 |
|---|---|
all |
每个有图标的条目都显示(未设置时的兼容默认值) |
groups |
只有根节点和有子页的节点显示图标 |
none |
侧栏不显示条目图标 |
非法取值只发警告并回落到 all,不让构建失败。本站使用 groups。
在侧栏里展开标题
Book 页可以在侧栏当前行下展开 h2–h4 分支,便于在长章节内跳转:
整数指定展开到第几级(2–4),true 等于 2(只展开 h2),false 关闭。取值超出
范围时普通预览告警并关闭标题分支,严格发布构建拒绝这条警告。只对 type: book
的页面生效,且只在侧栏当前行下展开。
目录 TOC
右栏大纲由 Hugo 从 Markdown 标题生成,收录层级是 Hugo 原生配置:
普通外壳运行时始终跟踪当前标题,无需额外开关。大纲绘制连续轨道、高亮当前区段
并标出位置。读者可以整体折叠右栏,状态存在本地。小于 xl 时右栏隐藏,大纲内容
移进侧栏抽屉。
旧的站点键 params.ui.scroll_spy 与页面键 scroll_spy 在整个 1.x 期间仍作为静默
兼容 no-op 接受。两个布尔值生成相同的大纲,也不加载额外运行时;只有未来的破坏性
版本才会删除这两个键。
单页隐藏大纲用 front matter notoc: true。
只有进入 Hugo 目录的标题才出现在大纲里:Markdown 型 shortcode({{%/* … */%}})输出的标题会进,普通 shortcode({{</* … */>}})输出的通常不会。结构性标题应留在 Markdown 里。
栏目首页样式
带 _index.md 的分区会自动列出子页。两种样式:
list(默认):每个子页一个标题 + 描述段落;cards:网格卡片,读子页的title(或linkTitle)、description与icon。
可以按分区覆盖。非法取值在普通预览中告警并回退,发布门禁带
--panicOnWarning 时拒绝这条警告:
相关的页面级开关:no_list: true 不列子页;simple_list: true 只输出一个无描述的项目符号列表;子页设 hide_summary: true 把自己从列表里去掉。不要手写子页清单:手写的清单会与侧栏失同步。
页宽
normal 是常规阅读宽度,wide 放宽正文栏,full 铺满视口。可以逐页或按分区覆盖;宽表格、大图与 API 参考页常用 wide:
Book 页另有一个 reading_width(slim / normal / wide),改的是正文本身的
阅读行宽,不动外壳。两个键取值非法都会在普通预览中告警并回退,严格发布时失败。
顶栏与页脚开关
顶栏与页脚属于逐页的布局决定,写在 front matter 顶层(不在 ui 下),可以用分区 cascade 一次设定:
验证
- 构建输出
Total in …,没有 ERROR / WARN; - 新建的
type: docs页面有左侧栏。没有则检查 cascade 是否覆盖到该页,以及shell_types是否包含这个 type; - 拖动侧栏分隔条,刷新后宽度保留,双击恢复默认;
- 窗口缩到
md以下时侧栏变成抽屉且可关闭,缩到xl以下时大纲移进抽屉; - 栏目首页的卡片数量与侧栏子页数量一致;
page_width: wide的页面比相邻页面宽;- 文档挂在站点根时,
hugo --printPathWarnings没有重复输出路径的告警。
相关
5.6 - 全文检索
OINK 的搜索是本地搜索:Hugo 在构建时给每种语言生成一份 JSON 索引,读者的浏览器下载它,在本地完成检索。不需要爬虫、账号、CDN,也不需要联网。主题默认不启用,一行配置即可开启。
搜索的入口是命令面板,打开方式与面板的其余内容见命令面板。
打开本地搜索
这一个键决定索引、Lunr 运行时与搜索对话框是否进入页面。三个条件同时成立时页面才带上它们:
params.offline_search为真;- 页面是首页,或者用了外壳布局(
docs/book/blog/swagger,见布局与页面类型),或者是开着params.ui.landing_search的落地页; - 当前输出不是打印。
任何一条不成立,构建就不往这个页面里放对话框、索引引用与 Lunr。这些资源不是被隐藏,而是不生成。
hugo server 下索引默认 也会生成,预览行为与线上一致。站点极大、每次改动都重建全站索引明显拖慢预览时,把它关掉:
控制索引体积
offline_search_index 决定每个页面往索引里写多少内容,因此同时决定两件事:读者能否搜到正文里的词,以及第一次搜索要下载多大的文件。
| 取值 | 索引进去的内容 | 什么时候用 |
|---|---|---|
title |
标题、标签、分类、search_keywords |
只靠标题定位的超大站 |
heading |
上面这些 + 页内各级标题 | 标题写得足够具体时 |
summary |
上面这些 + 描述与摘要 | 千页级站点;本站使用这一档 |
content |
上面这些 + 全文纯文本 | 默认值,几百页以内适用 |
其它取值在普通预览中告警并使用 content;严格发布构建因
invalid params.offline_search_index 失败。
offline_search_summary_length 是结果行里摘要的截断长度(默认 70),offline_search_max_results 是结果条数上限(默认 10)。这几个键的完整定义在配置总览。
读者搜第一个词之前要先下载整份索引。超过这个量级就把 offline_search_index 从 content 降到 summary。
调整排序
页面在 front matter 里影响自己的排名:
search_keywords 是额外的匹配词,可以写一个字符串,也可以写数组。它是这两个键里更有用的一个:读者搜 pg 或 GUC 即可命中标题只写着「PostgreSQL 参数」的页面。检索时关键词的权重仅次于标题,高于正文。
search_boost 是最终得分的正数乘子,默认 1.0,作用在文本匹配得分之上。1.5 不会把页面固定在第一位,只让它在本来就匹配的结果里前移。零、负数与非数字会告警并按 1.0 处理。
整节的默认值用 cascade 一次设定:
页面自己写的值覆盖继承来的值。本站 docs/ 下的页面按这种方式使用 search_keywords:每页列出中文说法、英文原词与配置键名。
把页面挡在索引外
search_exclude 是唯一写法。已移除的 exclude_search 与 excludeSearch 不再被
读取,因此不能保护页面;迁移检查器会报告它们。正文为空的页面不进索引。
不该公开的内容不要放进站点,也不要用 search_exclude 保护它。
中文与 CJK
Lunr 不能可靠地给中文分词。面板在查询里检测到 CJK 字符时整条切到子串匹配:逐篇比对标题、关键词、页内标题、描述、正文,命中哪一层给哪一层的分,最后同样乘上 search_boost。两条路径的排序规则一致。
三点需要知道:
- 中文查询是 子串 匹配。搜「主从复制」只命中连续出现这四个字的位置,搜「复制主从」没有结果。
search_keywords对中文站的收益因此最大:把读者可能使用的同义说法、英文原词、缩写都写进去。- 输入法组字期间面板不重算结果,文字上屏后才检索,中文输入不会逐字母刷新结果。
中文搜不到内容时,先确认中文页面进了中文那份索引(见下面的验证),再考虑分词问题。
可选:在线搜索
本地搜索之外,主题保留了两个在线搜索集成,默认关闭。同一时间只启用一种:配置了多个入口时构建告警 You have more than one site-search option configured。
启用在线搜索意味着接受对应服务的抓取方式、可用性与隐私边界,这些应写进站点的隐私说明。
Algolia DocSearch
三个值必须都显式写出,缺一个构建中断:OINK 不会回退到其它项目的公共索引。DocSearch 的 JS 与 CSS 随主题内置,不从 CDN 加载,但每次检索请求都发到 Algolia。需要真实的密钥与索引才能工作,此处不渲染。
Google 可编程搜索
还需要给结果准备一个落地页:
搜索框把查询提交到 <baseURL>/search/?q=…,结果由 Google 的脚本在那个页面上渲染,需要访问 cse.google.com。同样需要外部服务,此处不渲染。
验证
-
构建,确认每种语言各生成了一份索引:
开发构建下文件名是
offline-search-index.zh.json,生产构建加指纹,形如offline-search-index.zh.7ab….json。一种语言一个文件,缺少某个文件说明那种语言的页面没进索引。 -
查看索引内容,这是排查「中文搜不到」的第一步:
条目数应接近中文页面数,
keywords与boost字段能看到写进 front matter 的值。 -
打开站点,按 /,分别用一个英文词与一个中文词各搜一次。结果按内容根分组,每组的名字是面包屑的第一段。
-
子路径部署(站点挂在
https://example.com/docs/这类路径下)时,打开浏览器开发者工具的网络面板,确认索引请求带上了子路径。索引请求打到域名根目录并返回 404、页面其余部分正常,是「搜索没结果」最常见的原因。
相关
5.7 - 命令面板
命令面板是站点唯一的模态入口:搜索页面、复制本页 Markdown、切换语言、切换版本、跳转到站点自定义链接,都在这一个对话框里完成。它随本地搜索一起装配:params.offline_search 关闭时,面板连同索引与 Lunr 都不进入页面,见全文检索。
打开面板
| 打开方式 | 打开成什么 |
|---|---|
| 点顶栏或侧栏的搜索框 | 完整搜索态 |
| ⌘ / Ctrl + K | 完整搜索态;再按一次关闭 |
| / | 完整搜索态 |
| 反斜杠键 | 纯命令态(等于预填了 >) |
| f / c | 同上两者,由键盘导航提供 |
在框里输入 > 开头的查询 |
纯命令态 |
/、反斜杠、f、c 都是裸单键,会给输入让行:焦点位于 input、textarea、select 或 contenteditable 中,以及正在用输入法组字时,按键作为普通字符输入。带修饰键的 ⌘/Ctrl + K 没有这个限制,在输入框里也能打开面板。
面板内:↑ ↓ 选择,Enter 执行,Esc 关闭并把焦点交还给打开它的控件。
面板内容
不输入任何内容时,面板按固定顺序列出四组:
| 分组 | 内容 | 谁决定 |
|---|---|---|
| 快速链接 | 顶栏一级菜单里选出的几个入口 | params.ui.quick_links |
| 页面操作 | 复制 Markdown、查看 Markdown 源码、编辑本页、查看修改历史、新建子页、提 issue、打印整节 | 仓库配置与本页是否有 Markdown 输出 |
| 偏好设置 | 切换版本 → 切换语言 → 切换主题 | 站点是否配了多版本、多语言、深浅色菜单 |
| 命令 | 打开 GitHub 仓库,之后是站点自定义命令 | params.github_project_repo(缺省回退到 github_repo)与 ui.command_palette.commands |
偏好设置三项的顺序与顶栏控件一致(版本、语言、主题),面板与顶栏是同一个次序。选中「切换语言」这类项后,面板不立即跳转,而是就地展开可选项,再选一次。
输入文字时,先是页面结果,按内容根分组(分组名是面包屑的第一段,组间顺序跟随顶栏一级菜单的顺序),命令与动作合并成一组排在最后。
以 > 开头时只列命令与动作,不查页面。不确定某个功能在哪个菜单里时用它定位。
不可用的项在能说明原因时仍然列出。站点没有配置仓库地址,「编辑本页」会带着「不可用」的说明留在列表里,而不是消失。
快速链接
快速链接从 Hugo 主菜单里按 identifier 选取,不另写一份清单:
值是 menus.main 里条目的 identifier。不写这个键时默认取文档栏目与博客栏目(params.ui.docs_section 和 blog_section)。菜单本身怎么配见导航与菜单。
自定义命令
站点自己的命令写在 params.ui.command_palette.commands 下,排在内建命令之后,顺序即书写顺序:
上面是本站在用的那一条。字段共七个。写入其它键或无效记录时,普通预览告警并 丢弃该命令,严格发布构建拒绝这条警告:
id必填,小写字母开头,只能用小写字母、数字、下划线和短横线;不能与内建动作 ID 重名。title显示在面板里;description是它下面那行小字;icon是一对 Font Awesome class。keywords是数组,参与匹配但不显示,用于收纳读者可能输入的检索词。url与action有且只能有一个。url只接受http/https的完整地址、站内路径,或#开头的页内锚点;带主机名的地址在新标签打开。action引用一个内建动作 ID。
action: 给内建动作起别名内建动作已经在面板里,再包一层会让同一个功能以两个名字出现两次。
多语言站点把命令写在 languages.<lang>.params.ui.command_palette.commands 下,标题与关键词才能本地化。顺序由默认语言的那份清单决定:其它语言里同 id 的条目只覆盖字段,新增的 id 追加在末尾。各语言的命令顺序因此一致,读者换语言时命令不会换位置。
配置只能给出链接或引用内建动作,不能注入 JavaScript 回调:面板读取的是一份纯数据清单。
页面动作
面板里的「页面操作」与文档标题旁的拆分按钮是同一套实现:同一份动作描述、同一段 URL 生成逻辑、同一个执行器。按钮左半边复制本页 Markdown,右侧箭头展开全部动作。
整组关闭,或只在某些页面关闭:
enable: false 只移除标题旁的按钮,面板里的对应项保留,面板本身就是命令入口。单页用 front matter 的 page_context_menu: false 覆盖。
assistant_links 默认关闭,原因是读者点击时 当前页面的完整 URL(含查询串与锚点)会被发送到第三方,页面正文不会上传。这是站点级的选择,页面 front matter 里的 assistant_links 只能把它收紧,不能替站点打开。
links 是额外的外部动作,只出现在标题旁的菜单里,不进面板:
{url}、{title}、{markdown_url} 三个占位符会被替换成当前页面的值。
「编辑本页」「查看修改历史」「提 issue」这些动作是否可用,取决于仓库相关的配置,见仓库与页面信息;「复制 Markdown」「查看 Markdown 源码」需要页面开了 markdown 输出,见 Agent 支持。
与全文检索的关系
同一个对话框,两条独立的数据来源:
- 页面结果 来自本地搜索索引。索引未生成或下载失败时,面板照常打开、照常执行命令,页面那部分显示「页面索引暂不可用,操作仍可使用」。
- 命令与动作 来自页面里内嵌的一段 JSON 清单,不需要网络。
打印态不装配面板,打印输出里没有它;关闭 offline_search 后同样没有面板,此时 f 与 c 静默,不影响正常输入。
验证
-
构建后确认命令清单进了页面:
没有这一行说明本地搜索没启用,或者这个页面不在外壳布局里。
-
打开站点按下 ⌘/Ctrl + K,什么都不输入:应该看到快速链接、页面操作、偏好设置、命令四组,顺序如上。
-
输入
>:只剩命令与动作。新加的命令应该排在「打开 GitHub 仓库」之后。 -
切到另一种语言重复第 3 步,确认命令的标题变了、顺序没变。
-
打印预览(⌘/Ctrl + P)里不应该出现任何面板痕迹。
相关
5.8 - 键盘导航
OINK 的交互式页面自带一套单键快捷键:WASD 在侧栏树中移动,J K 在标题间跳转,Q E 翻页,另有几个单键切换主题、语言与命令面板。默认开启,所有绑定都给输入让行,可以按站点或按页面关闭。
键盘导航不维护第二套状态:树的展开折叠复用侧栏原有的箭头按钮,逐节跳转读取右栏目录,切换语言与主题复用命令面板的同一批动作。键盘操作的顺序与鼠标操作的顺序因此一致。
侧栏
| 按键 | 行为 |
|---|---|
| W S ↑ ↓ | 焦点移到上一个 / 下一个可见项 |
| A D ← → | 折叠 / 展开分组;叶子节点上 A 跳到父级,D 无动作 |
| Enter Space G | 打开焦点所在的页面 |
| Esc | 退出树,焦点回到正文 |
四个字母键不需要先进入树:焦点还在正文时按 S,以当前页在侧栏里的那一项为起点下移一格并落焦。焦点行整行加深底色,比「当前页」的底色深一档,用于区分当前页与焦点位置。
窄屏侧栏收进抽屉、或桌面侧栏被折叠时,第一次按这四个键先展开侧栏。页面没有侧栏树时静默。
方向键 只在焦点已经进入侧栏后 才作用于树,正文里保持浏览器原生滚动。RTL 语言下 ← → 随阅读方向对调,A D 恒等于「折叠 / 展开」。
阅读
| 按键 | 行为 |
|---|---|
| J K | 沿页面目录跳到下一节 / 上一节 |
| N | 首页专用:跳到下一个顶层分区(首页 J 的助记别名) |
| Q E | 上一篇 / 下一篇 |
| H | 专注阅读模式:隐藏 / 恢复导航外壳 |
J K 的目标序列与右栏目录同源,落点与点击目录一致。跳转是固定 100 ms 的缓动滑行,与距离无关;连续按键不必等上一段动画结束。已经读到某一节内部一段距离后,K 先回到本节起点,再按一次才跳到上一节。页面没有标题时退化为一小段滑动。
Q E 按 侧栏树的可视顺序 翻页,不按日期。栏目入口页本身也是树里的一项,博客的栏目边界因此表现为「上一专栏最后一篇 → 下一专栏入口页 → 下一专栏第一篇」。折叠起来的分支不在这个顺序里:翻页顺序与焦点移动顺序是同一个。页面没有侧栏树时回退到页尾翻页器,没有翻页器时用 <head> 里的 rel=prev/next。
H 在首页只隐藏顶栏与页脚,在文档页同时隐藏左右栏与浮动按钮。状态记录在当前标签页的会话中,首帧之前恢复,用 Q E 连续翻页不丢状态、不闪烁。外壳隐藏时 WASD 不会把焦点送入不可见的侧栏。
外观、语言与路由
| 按键 | 行为 |
|---|---|
| L Y | 循环切换语言(两个键等价) |
| T | 亮 / 暗模式切换 |
| R | 在首页与顶栏的同源一级入口之间循环 |
这三个键在任何交互式页面上都有效,不限于文档外壳。单语言站点的 L、关闭深浅色菜单后的 T、只有一个一级入口时的 R 都静默。R 只在同源的一级菜单项之间循环,外链与顶栏上的工具控件不参与。
搜索与命令
| 按键 | 行为 |
|---|---|
| F 或 / | 打开命令面板的完整搜索态 |
| C 或反斜杠键 | 打开命令面板的纯命令态 |
| ⌘ 加 K 或 Ctrl 加 K | 打开面板;再按一次关闭 |
/ 和反斜杠属于搜索功能本身,关闭键盘导航后仍然可用;F C 是键盘导航提供的别名,指向同一个面板实例。部分非美式键盘布局上反斜杠不易按到,在面板里输入 > 前缀同样进入纯命令态。面板里有什么见命令面板。
保留不占用的键
? 保留不绑定。速查卡挂在页脚最底层栏的问号按钮上,鼠标悬停、键盘聚焦或触摸都能打开,列出当前页面实际可用的按键:单语言站点看不到切换语言那一行。
G G、Shift 加 G 和数字键同样保留,可能用作将来的跳转序列。
快捷键的让行规则
所有绑定都是裸单键,凡是可能和输入或弹层冲突的场合一律禁用:
- 焦点在 input、textarea、select 或
contenteditable区域里; - 正在用输入法组字(中文站的硬约束);
- 按住修饰键时:⌘ 加 C 仍是复制,Shift 加 ↓ 仍归浏览器;
- 命令面板或别的对话框开着,键盘归那个弹层。
评论区在 iframe 中,键事件不冒泡到页面,无需额外隔离。
焦点顺序与无障碍
- 跳转链接:进入页面后第一次按 Tab 出现的就是「跳转到主要内容」,一步跳过顶栏和侧栏。
- 真实焦点:树内导航移动的是真正的 DOM 焦点,不是虚拟光标。屏幕阅读器因此读出链接名与「当前页」标记,Enter 是链接的原生行为,Tab 顺序没有被改写。
- 高对比度:焦点行的底色在
forced-colors模式下失效,退化为系统高亮色描边。 - 减弱动效:
prefers-reduced-motion打开时,逐节跳转与翻页滚动改为瞬时定位,不做滑行。 - 速查卡里的键帽与正文里的按键组件是同一套样式。
关闭
全站关闭:
单页关闭(交互密集的演示页常常需要),或者用 cascade 按整节关闭:
这个键只接受布尔值。写成 "false" 或其它值时,普通预览告警并使用站点默认值;
严格发布构建因 params.ui.keyboard_nav must be a boolean 失败。完整定义见
配置总览。
关闭后运行时不进入 JavaScript bundle,而不是加载后再判断。/、反斜杠和 ⌘ 加 K 属于搜索,仍然可用;页脚折叠链接栅格的箭头不受影响。
验证
-
构建后确认速查卡按钮在页面里:
关闭键盘导航且没开本地搜索时,这个按钮整个不生成。
-
打开一篇文档,光标停在正文里连按 S:侧栏里应该从当前页那一项开始逐项下移,正文不动。
-
按 E 若干次,核对翻页顺序与侧栏从上到下的顺序一致;折叠一个分组再翻,被折叠的页面应该被跳过。
-
点进搜索框,按 J:页面 不应该 滚动,字符正常输入。使用中文输入法输入时同理。
-
系统里打开「减弱动态效果」,再按 J:应该瞬间定位,没有滑行。
相关
5.9 - 多语言
OINK 使用 Hugo 的多语言模型,不额外引入目录约定:配置一个 languages 块,译文与原文并排放在同一个目录里,用文件名后缀区分。以下内容覆盖单语言站点扩展为双语站点需要改动的位置,以及双语站点的两处易错点:资源归属与标题锚点。
启用第二种语言
上面是本站在用的配置。四个字段的作用:
label是语言选择器里显示的名字,用该语言自己的文字书写:写简体中文,不是Chinese。locale是标准语言标签,会进<html lang>、hreflang备用链接和 Open Graph 元数据。weight同时决定语言排序和选择器的轮换顺序,小的在前。params是语言级覆盖:这里没写的键继承全局同名值。日期格式通常需要按语言各写一遍。
默认语言不带路径前缀(英文在 /docs/…),其它语言各占一个前缀(中文在 /zh/docs/…)。默认语言也需要前缀时加 defaultContentLanguageInSubdir: true。这会改变全站 URL,已上线的站点要同时配好重定向。
文件命名与资源
译文与原文并排放置,用后缀区分,Hugo 靠相同的基础文件名把它们认成同一页的两个语言版本:
- content/docs/
- install.md英文
- install.zh.md中文
- _index.md
- _index.zh.md
页面包同理:index.md 与 index.zh.md 放在同一个目录里。
页面包里的资源遵循一条规则:文件名不带语言后缀的资源由所有语言共享,带语言后缀的资源只属于那种语言。
- content/docs/install/
- index.md英文页
- index.zh.md中文页
- topology.webp两种语言都能用
- screenshot.zh.webp只有中文页能用
正文里引用带后缀的资源时 写不带后缀的名字:,Hugo 会按当前语言解析。
这条规则有一个推论:页面包里只有 index.zh.md、没有英文对等页时,不带后缀的资源不会分给中文页,它们归属默认语言,而默认语言在这个包里没有页面。此时所有资源都必须带 .zh. 后缀,本站 docs/ 下的中文页面包即是如此。
哪些内容需要翻译:
- 翻译:
title、description、摘要、菜单标签、标签名、图片 alt、提示块正文、shortcode 里面向读者的参数。 - 保持一致:日期、
weight、别名,以及任何影响路由的元数据。两边不一致会导致侧栏顺序在两种语言下不同。 - 不翻译:命令、配置键、文件名、URL、版本号、产品名、shortcode 名。
按语言分开的配置
三处内容不在 content/ 里,需要各语言各写一份。
菜单 写在各自语言下:
identifier 两种语言必须一致:命令面板的快速链接与搜索结果分组顺序都按它匹配。菜单的完整写法见导航与菜单。
首页数据 按语言取文件:data/home/en.yaml、data/home/zh.yaml。当前语言没有对应文件时回退到 en.yaml;单语言站点用一个 data/home.yaml 即可。见首页与落地页。
界面文案:主题自带 32 份完整界面语言包,即 Docsy 支持的 31 个 locale
文件名,再加通用 zh。每份语言包都以目标语言覆盖 OINK 的全部 192 个键,
不再依赖生成的英文 fallback。zh 与 zh-cn 使用简体中文,zh-tw 使用繁体
中文;完整 locale 与占位符契约见架构。
要改某一条,在站点自己的 i18n/ 下建同名文件,只写要覆盖的键:
如果需要兼容 Hugo 0.160.x,并且地区化中文语言包同时存在,请为非默认的通用 zh
语言保留具体的 locale: zh-CN。从 Hugo 0.161 起,相同配置也可以使用裸
locale: zh。
缺译回退与语言选择器
语言选择器的图标本身是一个链接:点击它按 weight 顺序切到下一种语言(在末尾回到第一种),悬停或键盘聚焦才展开列出全部语言的菜单,触摸屏上菜单不展开,点按即切换。双语站点因此一次点击即可来回切换。
菜单始终列出全部配置的语言,不论当前页有没有译文:
- 目标语言有译文 → 跳到那一页;
- 目标语言没有译文 → 跳到那种语言的 首页。
回退到首页优于把读者送进 404。代价是读者不一定察觉自己被送到了首页,双语站点应当把「每个页面都有对等译文」作为约束来检查,而不是依赖回退。
中文页面不存在时,中文站里就没有这一页:侧栏、搜索索引、翻页顺序都不包含它。
搜索索引也按语言分开:读者在中文页面搜索只命中中文内容。中文查询采用 CJK 子串匹配,细节见全文检索。
标题锚点要对齐
Hugo 从标题文本生成 ID,中文标题生成中文 ID:/docs/install/#prerequisites 与 /zh/docs/install/#前置条件 指向同一个位置,却是两个互不相通的锚点,跨语言的深链、目录与页内跳转都会失效。
做法是在译文标题里显式写出原文的 ID:
两条纪律:
- ID 从 英文页渲染出来的 HTML 里取,不要凭标题文本推断。标题里含行内代码、徽章或 shortcode 时,生成的 ID 与标题文本不一致。
- 中英对应页面的标题数量、顺序、ID 必须一致。确实需要在中文里加一节时,给它一个独立、稳定、不与英文冲突的 ID。
本站用一个脚本把这条约束变成 CI 检查,比对的是渲染后的 HTML 而不是源码:
新页面从建立时就写显式英文 {#id},成本低于事后回补。
从右向左的语言
在语言下声明书写方向:
<html dir> 随之改变,主题额外加载 Bootstrap 的 RTL 样式表。主题自身的 CSS 全部使用逻辑属性(margin-inline-start 而不是 margin-left),镜像布局自动完成。站点自己写的 CSS 同样要用逻辑属性,否则 RTL 下会错位。
验证
-
构建,确认两种语言的产物和索引都在:
-
检查
hreflang:每个页面的<head>里,每种语言各一条rel="alternate",外加一条指向自己的rel="canonical"。 -
在有译文的页面上展开语言选择器并选择另一种语言,确认停在同一篇文档;在没有译文的页面上重复一次,确认落到目标语言的首页而不是 404。
-
两种语言各搜一次同一个概念,确认都有结果。
-
双语站点把标题对齐检查接进 CI,见上一节的脚本。
相关
5.10 - 多版本
产品有多个受支持版本时,文档通常也要分版本。主题提供两项功能:顶栏的版本切换菜单,与旧版本站点上的归档提示横幅。部署布局由站点决定:主题不做跨版本的单次构建,每个版本是一次独立的 Hugo 构建。
版本切换菜单
在 params.versions 里列出要出现在菜单里的版本。这个列表非空时,顶栏工具区出现一个分支图标的菜单,页脚最底层栏出现同样内容的纯图标向上菜单。
菜单项默认显示 version 的值,写了 name 就显示 name。当前项标成选中态,判定方式是条目的 version 等于 params.version,或者条目的 url 等于站点的 baseURL,两者满足其一即可。
没写 url 的条目显示为不可点击的灰项,可用作分节标题;name: '---' 是一条分隔线(分隔线上写 url 会告警)。name 支持行内 Markdown:
同一份列表也是命令面板里「切换版本」的数据来源,菜单与面板不会不一致。
逐页跳转的取舍
version_menu_pagelinks: true 会把当前页面的路径拼到目标版本的 URL 后面,读者切换版本时 停在同一篇文档。
代价是目标版本不一定有这个页面:文档结构在版本间会演进,旧版本没有新增的页面,读者切换过去就是 404。本站关闭这个选项。
单个条目可以覆盖全局设置:
结构稳定时开启,结构变动大时关闭。跳到版本首页多一步操作,仍优于 404。
归档横幅
不再维护的旧版本站点上,向读者说明这是一份快照:
archived_version: true 时,每个文档页与书籍页正文顶部出现一条横幅,写明当前版本已不再积极维护,并给出指向 url_latest_version 的链接。文案随站点语言本地化,无需自行编写;version 是横幅里显示的版本号。
横幅只出现在文档与书籍页面上,博客和落地页没有。
params.version 与 params.versions 的区别
两个键名字相近,职责不同:
params.versions是 一张跨站点的清单:菜单里能跳到哪些版本,各自的地址是什么。它描述的是其它站点。params.version是当前这次构建自己的版本标识。它决定菜单里哪一项被标成选中、归档横幅里显示什么版本号,data/download/*.yaml没写version时也以它兜底(见发布与下载页)。
它不一定是 Git 引用。需要一个能解析的发布 tag(例如安装命令里引用的那个)时,另设一个自己的参数,不要复用 params.version。这两个键的完整定义在配置总览。
多版本部署布局
| 布局 | baseURL |
特点 |
|---|---|---|
| 子域名 | https://v1-9.docs.example.com/ |
各版本相互独立,互不影响;需要给每个版本配 DNS 与证书 |
| 子路径 | https://docs.example.com/v1.9/ |
单域名,SEO 权重集中;需要托管方支持按路径路由到不同产物 |
每个版本是一次独立构建:从对应的 Git 分支或 tag 检出内容,用那一版自己的 hugo.yml 构建,产物发布到对应地址。当前版本的站点把 versions 列全,旧版本的站点在列全之外再加上归档横幅。
baseURL 必须包含那段路径否则搜索索引、页面动作与资源链接都指向域名根目录:页面看上去正常,搜索却没有结果。这是子路径部署最常见的故障,部署细节见发布上线。
验证
-
构建后确认版本菜单进了页面:
params.versions为空或未配置时,菜单整个不生成。 -
看当前版本有没有被标成选中:
一条都没有,说明
params.version与versions里的version字段对不上,或者baseURL与该条目的url不一致(注意结尾斜杠)。 -
逐个访问菜单里的链接。开启
version_menu_pagelinks时,在一篇旧版本不存在的文档上试一次,确认落点可以接受。 -
归档站点上打开任意文档页,横幅应该在正文最上方,语言与站点一致,链接指向最新版本。
-
按 ⌘/Ctrl + K 打开命令面板,「切换版本」列出的应该是同一份清单。
相关
5.11 - 分类体系
目录树只有一条路径,分类体系(taxonomy)给页面加第二条:同一篇 PostgreSQL 备份文档既在「运维」目录下,又能从「备份」标签页找到。启用它只需要 Hugo 的 taxonomies: 配置,术语页、术语卡片、右栏分类云与顶栏分类面板都由主题自动生成,无需编写模板。
本页带着一个分类:标题下面的「分类: 定制站点」一行,以及右栏目录下面那组带计数的芯片,都不需要在页面上写配置。
启用分类法
分类法由 Hugo 决定,主题不额外提供开关。在 hugo.yml 顶层 写 taxonomies:,键是单数名、值是复数名:
这是本站的配置。三点需要注意:
- 写了
taxonomies:之后它就是 完整列表,不是追加。想在自定义分类法之外保留tags/categories,必须把它们一起列出来。 - 复数名同时是 URL 段:
/zh/tags/、/zh/categories/。 - 全部关闭:
disableKinds: [taxonomy, term]。
加一个自己的分类法,例如按产品模块归类:
分类法的显示名:tag tags category categories module modules 这六个键在主题的每个语言文件里都有本地化标题(中文分别是「标签」「分类」「模块」)。其它分类法用复数名的 humanize 结果(products → Products)。要自己定名字,在 content/<复数名>/_index.md 与 _index.zh.md 里写 title / linkTitle,主题会优先用它:
为页面添加标签
front matter 里的键名用 复数名(taxonomies 的值那一列),值始终是列表,只有一项也要写成列表:
整个栏目共用一个分类时,写在栏目首页的 cascade 里,无需每页重复:
本站 docs 的六个栏目都是这样配置的。页面自己写 categories: 会覆盖 cascade,不合并:要在栏目分类之外再加一个,两个都要写出来。
页面上的术语行
文档页与博客页在标题、摘要下面渲染一行已分配的术语,链接指向对应的术语页,本页顶部的「分类: 定制站点」即是。这一行的容器是 .taxonomy-terms-article,按分类法另带一个 .taxo-<复数名> 类,单独调样式时用这两个选择器。
默认列出该页的 全部 分类法,只有 authors 与 series 这两个保留复数除外——它们各自有专门的呈现面(署名行与系列横幅),再列一遍标签等于把同一件事说两遍。在 page_header 里点名,就能把它放回去。
只想显示其中几种、并固定顺序:
主题认识名字的两个分类法
authors 与 series 就是普通的 Hugo taxonomy,按普通方式声明——主题不为它们增加任何参数。主题增加的是各自的一套呈现,所以「声明」本身就是全部开关:
| 复数名 | 声明之后打开了什么 | term 页变成什么 |
|---|---|---|
authors |
文章头部的头像与带链接的名字、列表行上的名字、feed 里每位作者一条 <dc:creator> |
作者主页:显示名取 term 页的链接标题(有 linkTitle 用它,否则用 title),description 是一句话介绍,正文是长介绍,头像取题图解析器为这一页选中的那张 |
series |
正文上方一条横幅,写明系列名、本篇位置、下一篇,以及折在 <details> 里的完整列表 |
系列引言,成员按阅读顺序排列,而不是最新在前 |
两者的完整说明与各自需要的 front matter 在写博客。这里只提两件事:
- 主题刻意不设
data/authors文件。作者主页就是 term 页本身,因此不存在第二份权威跟它打架。 - 系列 term 页是唯一不按时间倒序排列的 term 页。写了
series_weight的成员按升序排在前,其余按日期升序跟在后。term 页没法把顺序交给 Hugo,所以主题自己算一次,两处呈现读同一份结果。
标签页与分类页
每种分类法生成两级页面:
| 页面 | URL | 内容 |
|---|---|---|
| 分类法列表页 | /zh/categories/ |
页头是分类法图标、本地化名(「分类」)与术语数,下面每个术语一张卡片,使用次数多者在前:术语图标(作者则是头像)、术语名与页面数 |
| 术语页 | /zh/categories/定制站点/ |
页头是术语标题与页面数(关闭面包屑时另有一行「分类」kicker 链回列表页),下面按日期倒序列出该术语的全部页面,样式与博客列表一致 |
中文术语的 URL 使用中文字符(浏览器地址栏显示 定制站点,HTML 里是百分号编码),Hugo 不做拼音转写。需要 ASCII URL 时改用英文术语,再在 content/categories/<术语>/_index.zh.md 里用 title 给它一个中文显示名,这是 Hugo 的术语页内容文件机制。
术语页在内容树里没有固定位置,它借用一个:某术语的成员全部位于同一个顶层栏目下时,术语页用那个栏目渲染侧栏树与根链接,读者从文档里点进标签仍留在文档导航中;成员跨栏目时回退到站点级的树。
术语卡片只出现在分类法列表页;术语页上换成右栏的分类云。
右栏的分类云
文档页、博客页与术语页的右栏(目录下面)每种分类法一组,芯片带计数,可折叠。这一组是自动的,没有开关:定义了分类法且当前范围内有术语时就会出现。
分类法列表页与术语页的右栏最上面是分类法切换器:声明的每种分类法一行,带图标、名称与术语数,链向各自的列表页,当前那一行高亮。列表页的分类云按全站统计,并略去自己这一种——它的术语就是旁边的卡片。只有一种分类法的站点不显示切换器。
计数 不是全站计数,而是按顶层栏目统计:先看页面的 type 有没有同名栏目(type: docs 的页面用 /docs/ 这棵树),没有就用页面所在的顶层栏目。博客页上的「标签: release 4」说的是博客里有 4 篇,不是全站有 4 篇。
图标按复数名配置:
categories 与 tags 的默认值就是上面那两个,其它分类法默认 fa-solid fa-shapes。图标是一对 Font Awesome class,与站点其它地方的图标写法一致。
顶栏菜单里的分类面板
主菜单里指向分类法列表页的条目,会自动变成一块术语芯片面板(按用量降序,带计数),无需手写下拉项:
pageRef: /tags 与旧式的 url: /zh/tags/ 都能识别:URL 形式的菜单先解析成本站页面再判断类型,从旧配置迁移时不必改写法。菜单本身的其它写法见导航与菜单。
双语标签
Hugo 的分类按语言分开统计、分开链接:/categories/ 与 /zh/categories/ 是两棵互不相干的树,中文页只进中文那棵。术语要在各自语言的 front matter 里各写一遍:
两条要注意:
- 同一个词在两种语言里写成同样的字符串(例如
release),得到的仍然是/categories/release/与/zh/categories/release/两个术语页,各自只统计本语言的页面。不要为了统一而在中文页里写英文词:右栏芯片会显示英文。 - 分类法的显示名会跟着语言走(上面那六个内置键),但 术语名不会:术语就是你在 front matter 里写的那个字符串,主题不翻译它。英文页里写
高可用,英文站的芯片上显示的就是高可用。
多语言站点的其余部分见多语言。
按内容类型开关
主题没有「文档显示、博客不显示」这类开关,控制点是给哪些页面打标签。本站的做法:
| 内容 | categories | tags | 效果 |
|---|---|---|---|
content/docs/** |
栏目级 cascade(「定制站点」等六个) | 不打 | 术语行只有一行「分类」 |
content/blog/** |
每篇写(release、oink) |
每篇写(Oink、Release) |
术语行两行,右栏两组芯片 |
让整个栏目从分类里消失:删掉栏目首页 cascade 里的 categories,不需要别的配置。让某一页不进分类:在它自己的 front matter 里写 categories: [],空列表覆盖 cascade。
验证
页面上看三处:
- 本页标题下面有一行「分类: 定制站点」;
- 右栏目录下面有按分类法分组的芯片,每枚带计数;
- 打开 /zh/categories/ 能看到每个术语一张卡片,点任一张进入术语页。
命令行上查产物:
主题仓库自带一个针对性检查,验证「不写 taxonomies: 就不生成分类页」与「术语页在中英文下标题正确」两件事:
限制
page_header: []不会 隐藏术语行:空列表被当作未设置,回落到「列出全部分类法」。要去掉这行,就不要给这些页面打标签,或在assets/scss/_styles_project.scss里隐藏.taxonomy-terms-article。- 右栏分类云没有开关,也没有条数上限;术语数量很多的站点应当减少分类法,配置层面没有裁剪手段。
- 术语页没有跨语言对等关系:语言切换在术语页上不保证落到「同一个术语的另一种语言」。
相关
5.12 - 仓库与页面信息
面包屑行右侧的 操作菜单 里与仓库有关的条目,由几个 github_* 参数推导;页尾的「最后修改」信息行来自 git 历史。前提是内容存放在一个 GitHub 风格的仓库里。
四个键接通全部链接
操作菜单里所有跟仓库有关的条目,都由这几个键推导出来:
上面是本站的真实配置。填好之后,本页的操作菜单里这几条指向:
| 菜单条目 | 目标 |
|---|---|
| 编辑当前页面 | …/edit/main/content/docs/customize/repository.zh.md |
| 查阅编辑历史 | …/commits/main/content/docs/customize/repository.zh.md |
| 添加子页面 | …/new/main/content/docs/customize?filename=change-me.md&value=<模板> |
| 提交文档议题 | …/issues/new?title=仓库与页面信息 |
| 提交项目议题 | https://github.com/pgsty/oink/issues/new |
几点约定:
github_repo指向内容所在的仓库,不是主题仓库。写主题仓库会把读者的改动引到错误的位置。省略它时,上表五条全部消失。github_project_repo是第二个仓库,接收产品缺陷而非文档错误的议题。读者难以区分两者时不要配置它。github_branch默认main,填的是内容分支,不是部署分支,也不是 Pages 自动生成的分支。github_subdir是仓库内路径。站点源码在仓库根目录时留空;放在子目录(例如仓库里同时有代码和website/)时填website。
这几个键都可以在站点、单语言、栏目 cascade 或页面 front matter 上设置,内容来自多个仓库时用得到。键的完整定义在配置总览。
内容来自另一个仓库
把一棵子树从上游仓库挂进来时,用栏目 cascade 覆盖仓库参数,再用 path_base_for_github_subdir 告诉主题:先去掉本地路径前缀,剩下的部分接到 github_subdir 后面。
content/reference/api/client.md 因此映射到上游的 docs/api/client.md。
path_base_for_github_subdir 的值是正则;源文件名与本地不同名时改用 from / to 映射,例如把每个栏目的 _index.md 对到上游的 README.md:
OINK 把 .md 与 .zh.md 并排放在同一个目录里,两种语言共用同一个路径前缀,正则里不需要语言目录。改完从叶子页、栏目首页、两种语言各点一次「编辑当前页面」:正则去掉的部分过多时,生成的 URL 看上去合理,实际是 404。
关闭其中几条
菜单里每个条目都带一个稳定的操作 ID:
| 菜单条目 | 操作 ID |
|---|---|
| 复制 Markdown 文本 | copy_markdown |
| 查阅 Markdown 源码 | view_markdown |
| 在 ChatGPT / Claude 中打开 | open_chatgpt / open_claude |
| 查阅编辑历史 | view_history |
| 编辑当前页面 | edit_page |
| 添加子页面 | create_child_page |
| 提交文档议题 | create_issue |
| 提交项目议题 | create_project_issue |
| 打印完整章节 | print_section |
托管服务不支持某条时,用 CSS 隐藏:
命令面板用的是同一批 ID,隐藏菜单条目不会让它从面板里消失。全站用不上的目标应当从配置里省略对应的键,而不是用 CSS 遮盖:CSS 只能隐藏链接,不能把错误的链接改对。
整个菜单也可以按页面关闭,front matter 写 page_context_menu: false,见页面参数。
「添加子页面」预填的新页面模板来自主题的 assets/stubs/new-page-template.md;站点在自己的 assets/stubs/new-page-template.md 放一份同名文件即可替换成自己的骨架。
最后修改时间
这一行的数据来自 git,不是文件的 mtime。打开 Hugo 的 git 支持:
页尾出现「最后修改 2026年8月17日 · <commit 主题> (a1b2c3d)」,commit 部分链到 …/commit/<hash>。lastmod_commit 三个取值:
| 取值 | 显示 |
|---|---|
subject(默认) |
commit 主题 + 缩写 hash |
hash |
commit a1b2c3d |
none |
只有日期,不链 commit |
写别的值时普通预览告警并使用 subject;严格发布构建会因
invalid params.ui.lastmod_commit 失败。
两点注意:
- CI 必须有足够的 git 历史。浅克隆(
fetch-depth: 1)取不到文件的最后一次提交,日期会缺失或错误。GitHub Actions 里设fetch-depth: 0。 - 未提交的文件没有 git 时间。本地预览新写的页面时这一行不出现。
git 历史不可用时不要用构建时间代替「最后修改」,构建时间不是内容的修改时间。
这一行属于 页面信息(Annotation) 组件,默认开启,位置在反馈之后、翻页器之前。整页关闭写 annotation: false。
这一行不是页面信息区块的全部。同一个区块还会渲染两种来源说明,都由页面 front matter 驱动,不需要覆盖模板:
- 上游署名:页面改写自别处时写
upstream_link,配上upstream_name、upstream_copyright、upstream_license、upstream_notice四个必填键,页尾出现一条带作品、版权人、许可证与完整声明链接的署名行;再写upstream_modified: true追加一条「本地已修改」。 - 译文说明:
params.ui.translation_notice写权威版本的语言代码,译文页就显示一条指回原文的说明;以本语言原创的页面写translation_notice: false退出。
这两族键的完整定义见页面参数。
确实需要自定义时,三个覆盖点各管一层:
| 覆盖哪个 partial | 改什么 |
|---|---|
layouts/_partials/annotation-items.html |
增删或重排这些行,保留主题的标记、图标、打印规则与无障碍标签 |
layouts/_partials/page-meta-lastmod.html |
换掉这些行的渲染标记 |
layouts/_partials/page-annotation.html |
换掉整个区块的外层容器 |
页尾的组成
五个组件的顺序是固定的,所有阅读型布局共用一份实现:
| 顺序 | 组件 | 主题默认 | 页面开关 |
|---|---|---|---|
| 1 | 分享 Share | 关(params.ui.share 为空) |
share: false,或页面自己的列表 |
| 2 | 反馈 Feedback | 关 | feedback: true / false |
| 3 | 页面信息 Annotation | 开 | annotation: false |
| 4 | 翻页器 Pager | docs / book / blog 开 | pager: false |
| 5 | 评论 Comments | 配置完整时开 | comments: false |
顺序对应读者读完最后一段之后依次会做的事:把这页递出去、说一句有没有帮上忙、看看它从哪来、翻到下一页、加入讨论。分享排在最前,因为它是唯一朝外的一块,而且一个决定要把文章转给别人的读者,在被问「这页怎么样」之前就已经决定了。分享栏的配置见写博客。
评论的配置在启用评论。
反馈组件
一行问题、两个按钮:「这篇文档解决了你的问题吗?」→ 是 / 否。选「否」再展开四个可选原因。默认关闭:
只给文档栏目开,用 cascade(博客通常只留评论):
行为边界:
- 点击即完成,没有输入框、没有提交按钮、没有登录。
- 选择按「页面 + 语言」写进浏览器
localStorage,读者回访时还能看到并修改自己的选择。 - 站点已有 Google Analytics(
gtag)时,发送docs_feedback事件,字段result(solved/not_solved)、page_path、language;选原因时再发一次,多带reason与refinement: true,便于和首次计数区分。没有 analytics 时组件照常工作,只是不上报,它不需要任何后端。 - 本页启用了评论时,反馈结果下面会多一条「在评论区补充详情」的锚点链接。反馈与 giscus 是两条独立的数据流,主题不会代替读者写评论。
本页在 front matter 里写了 feedback: true(docs 栏目默认关闭),页尾可以看到真实的组件。
贡献者墙
contributors shortcode 渲染一面 GitHub 头像墙,数据来自站点 data/ 目录下的一个文件,不在构建期访问 GitHub:
字段:github 必填并校验为合法 GitHub 用户名;重复时告警并跳过后项,严格发布构建
拒绝这条警告。name 缺省等于 github;role 可选;url 缺省是
https://github.com/<github>;avatar 可选,不填时渲染成首字母占位块,不发任何
网络请求,填写时必须是 http(s):// 或站内根相对路径。
多套名单写多个数据文件,用 data= 指定:
在 Markdown 与 RSS 输出里,头像墙降级成一串 - [@handle](url) — role 的列表。
data/contributors.yaml上面的例子因此不在本页渲染。放一个数据文件进 data/ 就能看到效果。
验证
- 点开本页面包屑行右侧的操作菜单,「编辑当前页面」应该指向
github.com/<你的仓库>/edit/<分支>/<源文件路径>,路径要与仓库里的实际路径逐段对应。 - 从栏目首页(
_index.md)再点一次:栏目首页最容易被path_base_for_github_subdir的正则改错。 - 页尾应有「最后修改」行;本地新建、尚未
git commit的页面没有这一行是正常的。 - 命令行核对生成的链接:
相关
5.13 - 打印支持
单页打印不需要配置:外壳(侧栏、目录、顶栏、按钮)都带 d-print-none,浏览器的 Cmd/Ctrl+P 得到的是一份干净的正文。主题因此没有页面级的「打印本页」按钮。
需要配置的是另一件事:把一整个栏目(或一整本书)连同全部子页面合成一份带目录的连续文档。以下内容覆盖启用方式、打印视图的结构,以及排除页面的做法。
启用整章打印
print 是主题声明的自定义输出格式,主题不替站点打开它。在站点自己的 hugo.yml 里给 section 加上:
这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 print 时要把该类型原本有的格式(HTML、RSS、markdown)一起写全,漏一个就丢一种输出。
开启后,每个栏目多出一个 URL。路径段 _print 在最前面,语言前缀之后:
| 页面 | 打印视图 |
|---|---|
/zh/docs/customize/ |
/zh/_print/docs/customize/ |
/zh/docs/ |
/zh/_print/docs/ |
/zh/blog/release/ |
/zh/_print/blog/release/ |
页面操作菜单里同时出现「打印完整章节」,命令面板里也能搜到同一条(操作 ID print_section)。它打印的是 当前栏目:在 /zh/docs/customize/print/ 这页点它,得到的是整个「定制站点」栏目,不是这一页。
打印视图的结构
打开上面任意一个链接,从上到下是:
- 一条提示条:「这是本节的多页打印视图。点击此处打印。返回本页常规视图。」它带
d-print-none,只在屏幕上出现,不进纸。 - 栏目标题与摘要。
- 全栏目目录,条目编号是
1:、2:、2.1:这样的层级号,链接指向文档内的锚点。 - 每个页面依次排列,标题变成
1 - 配置总览这种「编号 - 标题」,描述作为导语,正文原样渲染。
页面顺序是侧栏顺序(weight),子栏目递归展开。第二页起每页都另起一页;第一页是否另起一页,取决于栏目首页自己的正文是否超过 50 个词:首页只有一句话时不单独占一张纸。阈值可以调整:
不需要那份目录:
也可以只对某个栏目关闭,写在栏目首页 front matter 里:
把某些页面排除在外
纯链接页、只有一段跳转说明的页、体积巨大的截图页进纸意义不大。给它们写 no_print:
它只影响整章打印视图,页面自己的 HTML 与浏览器 Cmd/Ctrl+P 不受影响。侧栏分隔项(sidebar_divider)也自动排除。
组件在打印态的形态
打印是四态输出之一,每个组件都有确定的打印形态。整章打印视图与浏览器打印单个页面,规则一致:能交互的降级成静态,可折叠的一律展开。
| 组件 | 打印形态 |
|---|---|
| 提示块 | 静态块,折叠型(- / + / DETAILS)全部展开;边框转灰、去底色 |
| 标签页 | 标签条消失,所有面板依次展开,每个面板带自己的标题 |
| 代码块 | 去掉复制与展开按钮,取消最大高度与滚动,长行改为自动折行 |
| 表格 | 满宽静态表,取消横向滚动;表头在跨页时重复 |
| 图片 | 图与图注保留,缩放相关的属性被剥掉,宽度收进版心 |
| 画廊 | 网格改为竖排堆叠 |
| 文件树 | 静态面板,目录全部展开,分栏停在构建期宽度 |
| 参数表 | 完整定义列表,两种形态一致 |
| 公式 | 静态渲染的 KaTeX / MathML |
| Mermaid · Markmap · PlantUML | 照常渲染成图:打印视图仍是一张 HTML 页,这几个运行时照常加载 |
| ECharts · Infographic | 降级成围栏源码块,不渲染图表 |
| Asciinema · OpenAPI | 一行带标题的静态链接,录像或规范地址可见;三套运行时都不加载 |
| 卡片 / 步骤 / 徽章 / 按键 | 静态呈现,内容不变 |
页面外壳不进纸:侧栏、目录、顶栏、页面操作菜单、反馈组件、标题旁的锚点链接、行内复制按钮。
上表里靠浏览器端运行时绘制的那三种图(Mermaid、Markmap、PlantUML),触发打印前要确认它们已经绘制完成。
浏览器打印样式
主题自带一层 @media print 规则,单页打印与整章打印共用:
- 纸张
A4,页边距18mm 16mm 20mm;正文10.5pt,强制浅色配色。 - 字体切到
--td-print-font-family这个排印令牌,见品牌外观。 - 标题不与正文分家(
break-after: avoid-page),段落与列表项保留 3 行孤行 / 寡行控制。 - 表格、图片、块引用、提示块、卡片、标签页尽量不跨页断开;代码块允许跨页,但会自动折行而不是截断。
- 链接加下划线、转深蓝色,不会在链接后面打印出 URL 文本。需要这个行为的站点自己加:
- 收起的
<details>一律展开:折叠的提示块与文件树目录在纸上是完整的。
自定义排版写在 assets/scss/_styles_project.scss 的 @media print 块里,不需要改模板。
替换打印模板
需要改结构(例如给每页加页眉、换编号格式)时,覆盖最窄的那个 partial,都在 layouts/_partials/print/ 下:
| Partial | 负责 |
|---|---|
print/render.html |
整章视图的骨架:提示条、目录、递归内容 |
print/page-heading.html |
文档开头的标题与导语 |
print/content.html |
单个页面在整章视图里的呈现 |
print/toc-li.html |
目录里的一行 |
后三个支持 按内容类型 分化:建 print/page-heading-blog.html、print/content-book.html,主题会优先用带类型后缀的那个。
整本书的打印(type: book)走另一条路径:章节编号、图表编号与交叉引用都保持全书连续,见书籍出版。
验证
再看页面:
- 浏览器打开
/zh/_print/docs/customize/,确认目录条数等于栏目页数(减去no_print: true的页)。 - 在这个视图里按
Cmd/Ctrl+P,打印预览里应当看不到提示条、顶栏与任何按钮。 - 找一页含标签页与折叠提示块的(例如标签页),确认预览里所有面板都展开。
- 打印一份 PDF 通读分页情况,阈值不合适时调整
section_break_wordcount。
相关
5.14 - Agent 支持
HTML 页面里有侧栏、脚本与样式,模型读它要先剥掉这层外壳。OINK 让同一份内容再产出一份纯 Markdown:每页一个 .md,站点根目录一份 llms.txt 索引,页面上一个「复制 Markdown 文本」按钮。三者都是构建期产物,没有运行时服务,也不需要内容协商。
这三件事都要站点自己在 outputs 里声明,主题不替站点打开。另有两样同样需要显式打开的产物,服务于一次要读不止一页的 agent:每个栏目一份全文包,每种语言一棵导航树。
每页一份 .md
markdown 是 Hugo 的内置输出格式。把它加进需要的页面类型:
这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 markdown 时要把该类型原本有的格式(RSS、print)一起写全,漏一个就丢一种输出。
URL 规律是在页面 URL 后面接 index.md:
| 页面 | Markdown |
|---|---|
/zh/docs/customize/agents/ |
/zh/docs/customize/agents/index.md |
/zh/docs/customize/(栏目首页) |
/zh/docs/customize/index.md |
/zh/(站点首页) |
/zh/index.md |
每个 HTML 页的 <head> 里同时有一条发现用的链接,抓取工具不必推断 URL:
.md 的内容
不是把渲染好的 HTML 转回 Markdown,而是 你写的源码:front matter 换成一个 H1 标题加一段引用式摘要,其后是正文原文,shortcode 就地展开成各自的 Markdown 形态。
原生 Markdown 形态的组件(提示块、表格、参数表、图片属性行、代码围栏、数据围栏)在 .md 里原样保留源码,模型读到的与你写下的是同一份内容。栏目首页在正文之后还会附一份 Section pages: 子页链接清单。
shortcode 形态各有确定的降级:徽章变成强调文本或链接,按键变成 Ctrl + K,标签页变成一段段 **标签名** 小节,参数表变成条目列表。每个组件页的「输出形态」小节写了它自己那一行。
站点没有开 LLMS 输出时,上面那条 LLMS index: 不会出现:主题不指向未发布的文件。
llms.txt
llms.txt 是站点根目录的一份纯文本清单,告诉模型「这个站有什么、机器可读版本在哪」。给 首页 加上 LLMS 输出格式即可生成:
多语言站点每种语言各一份:/llms.txt 与 /zh/llms.txt。内容是自动生成的站点索引:
三段的来源:Site index 是本语言首页加站点主菜单(menus.main,条目有 Markdown 版就链 Markdown 版,带 description 的顺带写上);Documentation index 是 docs 栏目的子栏目及其下一层页面,缩进表示层级,每行附上该页的 description;Site locales 是站点配置里的全部语言。指向站外的菜单条目(GitHub、issue 跟踪器)会被剔除:它们属于导航外壳,不是本站内容。
改进 llms.txt 的入手处是主菜单与各栏目首页的 description,不是这个模板。
全文包
每页一份 .md 适合已经知道自己要读哪一页的 agent;想通读整本手册的 agent 只能一页页爬。LLMSFULL 输出把这件事压成一个文件:每个顶层栏目一份 llms-full.txt,按阅读顺序装下该栏目的每一页。它是 OINK 0.8.0 的新增能力,栏目不主动要就不生成。
开关在栏目首页自己的 front matter 里,不在站点配置:
front matter 里的 outputs 会整体替换站点级列表,所以要把该栏目原本有的格式写回去:这里漏掉 markdown 或 print,栏目首页就少一种输出。front matter 按语言分开,双语站点要在 _index.zh.md 里同样写一遍,才有中文的全文包。
产物是每种语言一份,落在栏目根下——/docs/llms-full.txt 与 /zh/docs/llms-full.txt。顺序就是侧栏与翻页器呈现的阅读顺序:docs、book 栏目声明了 data/docs_nav.json 显式树时以显式树为准,否则按内容树的 weight。侧栏里藏起来的页面(toc_hide)同样不进包。
每一页前面有一条带来源 URL 的分隔,其后的正文与该页自己的 .md 逐字节相同:
Source: 指向该页的 Markdown 输出;页面没有 .md 输出时回退到它的 HTML 地址。
只有顶层栏目能带全文包。写在更深一层的栏目上会告警——「LLMSFULL output requires a top-level section」——并且什么都不产出:hugo server 照常能用,加了 --panicOnWarning 的发布构建则会停在这里。
只要有栏目开了全文包,llms.txt 就会多出一段 ## Full-text bundles,列出本语言的全部全文包:发现入口仍在 agent 本来就会抓的那个文件里。
本站的文档栏目已经开启:https://oink.pgsty.com/zh/docs/llms-full.txt 是全部中文文档,一次抓取。
导航 JSON
侧栏是站点的目录,读得懂它的 agent 可以先规划路线再抓正文。NAVJSON 输出把它变成数据:每种语言一份 navigation.json,放在语言根目录下。和全文包一样,它是 OINK 0.8.0 新增、默认关闭,由站点在首页打开:
这会产出 /navigation.json 与 /zh/navigation.json。这棵树就是侧栏与翻页器读的那一棵——docs、book 栏目声明了 data/docs_nav.json 显式树时以显式树为准,其余按内容树的 weight:
| 键 | 含义 |
|---|---|
id |
去掉语言前缀的页面路径,同一页在每种语言里 id 相同 |
url |
该语言下 HTML 页面的绝对地址 |
markdown |
该页 .md 的绝对地址,只有页面确实产出 .md 时才有 |
title |
导航标题(linkTitle,回退到 title) |
description |
页面的 description,有才写 |
kind |
真实页面是 home、section、page;占位条目是 external 或 link |
children |
有序子节点,有子节点才写 |
数组顺序就是契约,weight 不会被序列化:顺序已经算好了,消费方再排一次只会与它来源的侧栏对不上。
占位条目保持侧栏里的样子:manual_link 是 external 节点,URL 照作者写的原样带出;manual_link_relref 是 link 节点,引用已经解析好。两者都没有页面身份,因此既没有 id 也没有 markdown。侧栏分隔线与 Hugo 从不渲染的页面会被略去,它们的子节点留在原位。
契约带版本:schemaVersion 是 1,JSON Schema 随主题仓库发布,见 schema/nav.v1.schema.json——要消费这个文件就拿它做校验。站点发布了它时,llms.txt 的站点索引里会列出本语言的 navigation.json。
本站已开启:https://oink.pgsty.com/zh/navigation.json 就是这棵树的实例。
页面上的 Agent 动作
面包屑行右侧的操作菜单里,跟 Agent 有关的是四条:
| 条目 | 做什么 | 出现条件 |
|---|---|---|
| 复制 Markdown 文本 | 抓取本页 .md 写进剪贴板(悬停时预取,点击后无明显等待) |
本页有 markdown 输出 |
| 查阅 Markdown 源码 | 新标签页打开 .md |
本页有 markdown 输出 |
| 在 ChatGPT 中打开 | 带一句提示词跳转到 ChatGPT | assistant_links: true |
| 在 Claude 中打开 | 同上,跳转到 Claude | assistant_links: true |
前两条只要开了 markdown 输出就存在。「复制」是拆分按钮的左半边(剪贴板图标),复制成功后短暂显示一个对勾。
后两条默认关闭,要显式打开:
打开之后的边界:读者点击时,运行时用浏览器地址栏里的完整 URL(含真实域名、查询串与锚点)拼一句提示词,中文站是「请阅读
页面可以收紧站点策略,不能反向打开:front matter 里 page_context_menu: { assistant_links: false } 关掉本页的助手链接;站点没开时页面写 true 不会生效。整个菜单按页关闭用 page_context_menu: false,见页面参数。
命令面板里也能搜到这两条助手动作(用的是同一份动作清单),见命令面板。
按页面退出 .md 输出
在页面 front matter 里重写 outputs。它同样是整体替换,只写要保留的格式:
要保留 RSS、只去掉 Markdown,就把其它格式列全:
自定义输出
主题用 layouts/all.md 渲染 Markdown 输出,用 layouts/index.llms.txt 生成 llms.txt,两种可选输出则由 layouts/list.llmsfull.txt 与 layouts/index.navjson.json 负责。站点在自己的 layouts/ 下放同名文件即可整体替换,但 先考虑更窄的做法:
- 按内容类型:
layouts/blog/single.md、layouts/docs/list.md这样带类型的路径只影响那一类内容,主题的打印模板即按此分化(layouts/blog/single.print.html)。查模板查找顺序确认你的组合。 - 按 shortcode:站点自己的 shortcode 可以加输出格式专属模板,让它在 Markdown 输出里给出更适合机器读的形式。
- 按页面:少数高价值页面手写内容,成本低于改模板。
llms.txt 的内容由站点结构决定,改模板之前先确认问题不在主菜单或 description。替换 index.navjson.json 还意味着接手 nav.v1 契约:你自己产出的内容仍要能通过 schema/nav.v1.schema.json 的校验。
验证
线上或本地预览用 curl:
再检查四处:
- 任一页 HTML 的
<head>里有rel="alternate" type="text/markdown"; - 面包屑行右侧的复制按钮点击后粘贴,得到的是 Markdown 而不是 HTML;
llms.txt里没有指向站外的链接;- 开了这两种输出的话:
llms-full.txt里每一页都以一行Source:开头,同一页在各语言navigation.json里的id相同。
限制
- 主题产出的机器可读表面是四种构建期文件:每页
.md、llms.txt,以及需要显式打开的、每个顶层栏目一份的llms-full.txt与每种语言一份的navigation.json。站点地图仍是 Hugo 自己的sitemap.xml。 - 全文包属于顶层栏目,没有整站一份的
llms-full.txt:想读全站的 agent 按栏目逐个读,清单在llms.txt里。 LLMS、LLMSFULL、NAVJSON都声明为非替代格式,所以它们都不会出现在<head>的alternate链接里,也没有对应的页面操作;它们靠约定俗成的路径与llms.txt里的条目被发现。- 服务端内容协商(同一个 URL 按
Accept: text/markdown返回 Markdown)不属于主题范围,要做在托管层。 - Markdown 输出走 源码 路径:只在浏览器端由 JavaScript 生成的内容(运行时绘制的图表)在
.md里是围栏源码,不是图。
相关
6 - 维护管理
本栏目覆盖内容写完之后的运维事项:在本机预览、构建并部署产物、接入评论与分析、跟随主题版本升级、故障定位。前面五个栏目决定站点的外观与内容,这一栏决定站点能否构建、部署在哪、出问题如何排查。
按任务导航
6.1 - 本地预览
两条命令覆盖日常工作:hugo server 在本机预览改动,hugo 产出可以部署到任何静态托管的 public/。前提是本机安装了 Hugo Extended(不低于 0.160.1);用 Hugo Module 引入主题时还需要 Go。构建不依赖 Node.js、npm 与 PostCSS,它们只服务于本仓库自身的回归检查。
预览服务器
在站点根目录(hugo.yml 所在的目录)执行:
打开 http://localhost:1313/。保存文件后 Hugo 重新构建并刷新浏览器,切换 Git 分支同样触发重建。首次启动较慢:用 Hugo Module 引入主题时,Hugo 要先通过 Go 把模块下载到缓存,之后的启动都走缓存。
常用开关
-D/--buildDrafts,- 把
draft: true的页面也构建出来 -F/--buildFuture,- 把
date/publishDate在未来的页面也构建出来 -E/--buildExpired,- 把
expiryDate已过的页面也构建出来 --disableFastRender,- 每次改动都整站重渲染,不用增量
-M/--renderToMemory,- 只在内存里渲染,不落
public/ -N/--navigateToChanged,- 保存哪个页面,浏览器就跳到哪个页面
--bind,- 监听地址;要让局域网或容器外访问就设
0.0.0.0 -p/--port,- 监听端口
--minify,- 预览也压缩输出,用来复现生产环境下的渲染
--printPathWarnings,- 有两个页面写到同一个目标路径时告警
本站开发时用的组合是:
-DFE 是 -D -F -E 的合写,草稿、未来与过期页面一并构建,写作时新建的页面才可见。
改动没有生效
Hugo 默认开启快速渲染(fast render),只重建它判定受影响的部分。修改布局、配置、data/ 或被 include 引用的文件时,增量判定可能不准,页面看起来没有变化。三步排查:
- 加
--disableFastRender重启,看是否恢复。 - 硬刷新浏览器(
Cmd/Ctrl+Shift+R),排除浏览器缓存。 - 仍未恢复则清缓存后重启。
从其它设备访问
hugo server 默认只监听 127.0.0.1,其它设备访问不到。要在手机或另一台机器上预览:
--baseURL 必须写成对方可访问的地址,否则页面能打开,但 CSS 与搜索索引这类走绝对路径的资源会指向 localhost。
生产构建
部署产物用 hugo 构建,不用 hugo server:
产物写入 public/,该目录可以脱离源码树独立部署。四个开关各管一件事:
--gc- 构建后清掉
resources/_gen里不再被引用的缓存资源 --minify- 压缩 HTML、CSS、JS 与 XML 输出
--printPathWarnings- 两个页面撞到同一个输出路径时告警,多语言站点最常见的静默错误
--panicOnWarning- 遇到第一条 WARNING 就让构建失败
--panicOnWarning 需要单独说明。OINK 的多数降级路径是告警而不是报错:giscus 必填键缺失、params.comments.type 取了不支持的值、Hugo 弃用的配置键,都只打一条 WARNING 然后跳过。CI 日志通常无人逐行阅读,这些问题会带到线上。把这个开关写进构建命令,等于要求零告警才算构建通过。
本站 CI 的构建步骤(.github/workflows/site-checks.yml)是 hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --panicOnWarning,任何一条告警都会让部署停在构建阶段。
baseURL 与构建环境
baseURL 写在 hugo.yml 里,也可以在命令行覆盖:
部署到子路径时 --baseURL 必须带上那段路径,细节见发布上线。
构建环境用 -e / --environment 选择,hugo 默认 production,hugo server 默认 development。这个选择在 OINK 里有三处可见后果:
production下才输出<meta name="robots" content="index, follow">,其它环境输出noindex, nofollow。production下robots.txt是Allow: /,其它环境是Disallow: /。production下才渲染 Hugo 的 Google Analytics 模板,静态资源也才做指纹与 SRI。
预览部署(PR preview、staging)用非 production 环境构建,产物自带不被搜索引擎收录、不上报分析的行为:
容器内预览
容器不是必需的。两种情况适合用容器:团队需要固定工具链版本,或不希望在每台开发机上安装 Hugo。
镜像里装 Go 的原因:用 Hugo Module 引入主题时,Hugo 需要 Go 解析并下载模块。用 submodule、离线归档或直接克隆的站点可以去掉 Go,镜像会小很多。
public/容器里的进程默认是 root,生成的 public/ 属于 root,宿主机上删不掉。共享环境里用 --user "$(id -u):$(id -g)" 映射用户 ID(上面的生产构建命令已经带了)。
镜像不需要 Node.js、npm 与 PostCSS,也不应出现拉取远程浏览器资源的步骤。网络隔离环境需要预先镜像基础镜像与这两个软件包。
清缓存
Hugo 的中间产物分三处,从轻到重依次清:
public/,- 删了页面但线上还在;或用
hugo --cleanDestinationDir让构建自己清 resources/_gen/,- 换了图片处理参数、换了字体或主色,页面还是旧样子
hugo mod clean,- 换了主题版本但解析出来还是旧的;加
--all清整个模块缓存
public/ 与 resources/ 都应该写进 .gitignore,不要提交生成产物。
与主题一起改
同时修改主题与站点时才需要这一节。用 HUGO_MODULE_REPLACEMENTS 把模块临时指向本地 checkout,go.mod 保持不变:
本站的 Makefile 封装了这几条命令,要求主题 checkout 在同级目录 ../oink:
无论用环境变量还是 Go workspace(go work init + HUGO_MODULE_WORKSPACE=go.work),CI 与生产构建都只看 go.mod;go.work 记录的是开发机的路径,不能提交。判定一个发布标签是否可用时,去掉替换、用 go.mod 里的版本单独构建一次。
断网构建验证
网络隔离环境的验收要同时覆盖构建阶段与浏览器阶段。六步:
- 从一份已校验的主题归档与空的模块缓存开始(
hugo mod clean --all)。 - 阻断出站 HTTP、HTTPS 与 Go module proxy。
- 运行生产构建
hugo --gc --minify --printPathWarnings --panicOnWarning。 - 浏览产物里两种语言的页面:文档页、博客页、首页、404。
- 操作搜索、深浅色切换、图表与内容组件。
- 检查子资源来源,确认没有意外的远程主机。
最后一步用主题仓库里的脚本,它不依赖站点的测试框架:
脚本扫描四种输出里的每个 href / src / srcset / poster 与表单 action,要求它们是站内相对路径或 http / https / mailto / tel,并拒绝行内 on* 事件处理器与 javascript: URL。指向别的主机的 <iframe> <script> <link> <img> <video> <audio> <embed> <object> <source> 一律报错,站点确实要嵌入第三方内容时加 --third-party 放行,多域名语言配置用 --allow-host 追加首方主机。
一次通过只证明当次提交与当次环境。每个主题候选版本、每次随附依赖更新之后都要重跑一遍。
验证
一次干净的生产构建应该是这样:
看到 Total in … 且没有 ERROR / WARNING 才算通过。然后确认:
- 日志里没有 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤。出现了说明配置里混进了上游 Docsy 的流程。
public/下有sitemap.xml、robots.txt,robots.txt是Allow: /。- 开了本地搜索的站点,
public/根下有offline-search-index.<语言>.json。 - 用
hugo server打开代表性页面:一个文档页、一个博客页、首页、404,两种语言、两种配色都看一遍。
构建失败或结果不对,去排错与检查。
相关
- 发布上线 — 把
public/发到 GitHub Pages、Cloudflare 或别处 - 排错与检查 — 构建、语言、搜索、平台四类常见故障
- 从零建站与其它安装方式 — Hugo Module / submodule / 离线归档的取舍
- 配置总览 —
hugo.yml里每个键的定义
6.2 - 发布上线
OINK 站点的产物是一个纯静态目录,任何能托管静态文件的地方都能部署,不需要 Node 运行时、服务端渲染或构建插件。托管商一侧只有三件事:用正确的 Hugo 版本执行一条命令、发布 public/、让 baseURL 与最终访问地址一致。
前提是本机已经能完成零告警的生产构建。
确定 baseURL
baseURL 是最常见的故障源,失败方式也隐蔽:页面能打开,但搜索索引 404、页面操作链接指向错误位置、部分资源加载失败。
部署到域名根目录:
部署到子路径(https://example.com/docs/)时,路径必须写进 baseURL:
也可以在构建时覆盖,让同一份源码部署到不同位置:
canonifyURLs 修子路径Hugo 的 canonifyURLs 默认 false,保持这个默认值。OINK 的模板与内容链接都基于 baseURL 解析:路径不对是 baseURL 不对,打开 canonifyURLs 会把本来正确的相对链接一起改写,让问题更难定位。
判断是否配对,看构建后搜索索引的请求路径:浏览器应当去 <baseURL>/offline-search-index.zh.json 取索引,取到别处就是 baseURL 不对。
选一个托管商
源码托管在 GitHub 时,一份 Actions 工作流就够:构建在 Actions 里执行,产物通过
Pages 部署 API 发布,不需要维护 gh-pages 分支。OINK Starter 已经包含下面的文件;
只有手工组装站点时才需要复制。
这是 OINK Starter 内置的工作流。几处不能删:
fetch-depth: 0— 站点开了enableGitInfo时,「最后修改时间」和贡献者信息要读完整 Git 历史,浅克隆会让它们为空。setup-go+go mod download— Hugo Module 方式引入主题时,Hugo 需要 Go 才能解析模块。用 submodule 安装主题的站点改成submodules: recursive,用离线归档的站点把themes/oink/提交进仓库,这两步都可以去掉。GOWORK: off与HUGO_MODULE_WORKSPACE: off— 防止本地开发用的go.work意外参与 CI 构建,保证 CI 验证的是go.mod里固定的那个公开标签。--baseURL "${{ steps.pages.outputs.base_url }}/"— 项目站点的 URL 形如https://<OWNER>.github.io/<REPO>/,configure-pages会把它算出来,不用手写。--panicOnWarning— 有告警不发布。
在仓库 Settings → Pages → Build and deployment 里把 Source 设为 GitHub Actions,推一次 main,在 Actions 标签页查看第一次运行。
自定义域名在同一设置页的 Custom domain 里填写,并按提示配置 DNS,随后把
hugo.yaml 里的 baseURL 换成这个域名。发布流程需要产物里带 CNAME 文件时,
把它放进 static/CNAME,Hugo 会原样复制到 public/。
OINK Starter 内置 .github/workflows/cloudflare-pages.yaml,使用 Direct Upload。
严格构建留在 GitHub Actions,Wrangler 把同一份 public/ 产物上传到 Cloudflare
Pages 项目。
- 创建一个 Direct Upload Pages 项目。项目名默认与仓库相同,也可用仓库变量
CLOUDFLARE_PROJECT_NAME覆盖。 - 添加仓库 secrets:
CLOUDFLARE_ACCOUNT_ID与CLOUDFLARE_API_TOKEN。token 需要 Account → Cloudflare Pages → Edit 权限。 - 手动运行一次 Deploy to Cloudflare Pages。设置仓库变量
CLOUDFLARE_PAGES_ENABLED=true后,每次推送main才自动部署。 - 规范 URL 默认是
https://<project>.pages.dev/;自定义域名成为生产地址时设置CLOUDFLARE_SITE_URL。
workflow 固定 Hugo Extended 0.165.0,从 go.mod 读取 Go 版本,关闭本地模块
workspace,并在上传前用 --panicOnWarning 构建。这是 Starter 用户最可复现的推荐路径。
Cloudflare Git integration 仍然是另一种有效模式:构建命令设为
hugo --gc --minify --printPathWarnings --panicOnWarning,输出目录 public,Hugo
固定 0.165.0,Go 固定 1.27。同一个项目只用 Git integration 或 Direct Upload
workflow 其中一种。预览部署仍不等于生产证明;它要按自己的 URL 重建并保持不收录。
Netlify — 构建命令 hugo --gc --minify,发布目录 public,环境变量 HUGO_VERSION。同样的设置可以写进仓库:
用 submodule 安装主题就打开递归 submodule 检出;用 Hugo Module 就要求构建环境有 Git 和 Go。生产与预览应使用同一个 Hugo 版本,除非预览环境本来就是用来测升级的。
Vercel — 同样的三件事:构建命令 hugo --gc --minify、输出目录 public、环境变量 HUGO_VERSION。它同样不需要安装 npm 依赖。
任何静态服务器(Nginx / Caddy) — 把 public/ 的内容整个铺上去:
站点是纯静态的,没有需要转发给应用服务器的路径。
对象存储 — Hugo 自带 deploy 命令,把目标写进配置即可:
构建之后执行 hugo deploy:它比对远端与 public/ 的差异,只上传变化的文件,并在给了 cloudFrontDistributionID 时使 CDN 缓存失效。不带 --target 时用第一个目标,--dryRun 先看要改什么。两个前提:Hugo 二进制带 withdeploy(hugo version 的输出里能看到),云厂商凭据由标准环境变量或配置文件提供(AWS 上先用 aws s3 ls 确认)。
离线打包 — 网络隔离环境里,在能联网的机器上构建,把产物打成一个包带过去:
构建时就要用目标环境的 baseURL,产物里的绝对链接不能在解包之后再改。
托管商没有 Go — 用 Hugo Module 引入主题需要构建环境有 Go。平台不提供时,改用 Git submodule(构建前执行 git submodule update --init)或离线归档(把 themes/oink/ 提交进仓库),见从零建站与其它安装方式。
预览部署不要被收录
Hugo 的 -e / --environment 只选择构建期行为,不改变站点内容,但 OINK 有三处会跟着它变:production 环境才输出 <meta name="robots" content="index, follow">、才让 robots.txt 变成 Allow: /、才渲染 Google Analytics 模板。PR preview、staging 这类构建不要用 --environment production:
出来的产物自带 noindex, nofollow 与 Disallow: /,也不会向分析服务上报数据。
内容安全策略
主题自带的运行时、字体与图标都是同源资源,严格的内容安全策略(CSP)因此可行。主题不提供一份通用策略:需要哪些指令由站点启用了什么决定。
改变所需指令的地方有五处:
- 作者写的行内 HTML 与行内脚本,
renderer.unsafe: true之下由作者负责。 - ECharts 的
$fn:回调:回调函数由站点注册到window.OinkEchartsFunctions,注册脚本的来源要进script-src。 - 分析脚本:站点自己插入的那段脚本与它上报的目标。
- 远程 API 规范与自建图表服务:落在
connect-src与img-src。 - giscus:
script-src与frame-src要一起放行。
从只覆盖已审查功能的最小策略起步,逐项放行:不需要回调时让 ECharts 选项保持纯数据,审查作者写的行内脚本,只为站点主动启用的集成添加远程来源。产物里的子资源来源可以先用断网构建验证里的脚本扫一遍。
验收清单
部署完成后按这张表走一遍。前四项是构建期的,后面几项要在真实 URL 上查。
零告警构建- 构建命令带
--printPathWarnings --panicOnWarning,日志里有Total in … baseURL正确- 页面源码里
<link rel="canonical">指向真实生产地址(含子路径) 站点地图<baseURL>/sitemap.xml可访问;多语言站点是一个索引,指向/en/sitemap.xml、/zh/sitemap.xmlrobots<baseURL>/robots.txt是Allow: /并带Sitemap:行;预览部署应该是Disallow: /搜索索引- 浏览器能取到
<baseURL>/offline-search-index.<语言>.json,站内搜索有结果 Markdown 输出- 任一页面 URL 后面加
index.md能取到纯文本(站点在outputs.page里开了markdown时) llms.txt- 站点在
outputs.home里开了LLMS时,首要语言与每种已启用语言根都能访问llms.txt 已启用语言- 每种语言的文档页、博客页、首页都能打开,语言切换落到对应页面而不是首页
外观与交互- 深浅色切换、打印视图、代表性组件(提示块、标签页、代码块复制)正常
404- 访问一个不存在的路径,看到站点自己的 404 页
sitemap.xml、robots.txt、.md 与 llms.txt 这几项的开关在配置总览,Agent 输出的细节见 Agent 支持。
回滚
静态站点的回滚就是重新发布上一个已知可用的 commit,不要在生产上手工改文件。
- GitHub Pages:在 Actions 里找到上一次成功的
Deploy to GitHub Pages运行,点 Re-run all jobs;或者git revert出问题的提交再推一次。 - Cloudflare Pages / Netlify / Vercel:在部署列表里选上一个成功的部署,用平台的 Rollback / Publish deploy 把它重新设为生产版本。
- 自建静态服务器:保留上一份
tar.gz,解压覆盖。离线打包里给产物加日期后缀就是为了这一步。
问题出在主题升级而不是内容时,回滚的是 go.mod 里固定的版本,见版本升级。
相关
6.3 - 启用评论
OINK 的评论走 giscus:每个页面对应一条 GitHub Discussion,读者用 GitHub 账号登录后发言,维护者在 GitHub Discussions 里审核与管理。主题不提供自建评论后端,也不内置 giscus 以外的服务商。
前提是一个公开的 GitHub 仓库,访客读不到私有仓库的 Discussions。
启用评论的页面会从 https://giscus.app 加载脚本和 iframe,网络隔离环境里用不了。它默认关闭,只在显式打开时才加载。站点有隐私政策时,这条外部数据边界应当写进去。
准备 GitHub 仓库
-
选一个公开仓库存放评论线程,可以就是站点源码仓库。
-
在仓库 Settings → General → Features 里勾选 Discussions。
-
为该仓库安装 giscus GitHub App。未安装 App 时访客无法评论或表态。
-
选一个 Discussion 分类。giscus 推荐 Announcements 类型:只有维护者与 giscus bot 能在该类型下新建 Discussion,读者不会误开话题。
仓库 ID 与分类 ID 是公开标识符,不是凭据。不要往 Hugo 配置里放 personal access token、OAuth secret 或密码。
生成配置
打开 giscus.app,按表单填仓库、映射方式和分类,页面下方会生成一段 <script>。把里面四个属性抄进 OINK 配置:
data-reporepodata-repo-idrepoIddata-categorycategorydata-category-idcategoryId
映射方式(mapping)决定哪个页面对应哪条 Discussion。OINK 默认 pathname,适合发布路径稳定、同一个仓库要服务多个域名或预览环境的站点。开始收集评论之后再改 mapping 或移动页面,giscus 会去找另一条 Discussion:已有评论不会被删除,但页面上再也找不到它们。映射方式要在上线前定好;确实要改 URL 时,同时保留重定向或重命名 Discussion。
全站启用
把生成的标识符写进站点配置:
上面是本站正在使用的配置。repo、repoId、category、categoryId 四个键缺一不可:任何一个缺失或只有空白字符,Hugo 打一条 WARNING 并跳过 giscus,构建不会失败,因此生产构建要带 --panicOnWarning。type 目前只接受 giscus,写别的值同样是告警加跳过。params.comments 的键名与 Hextra 同形,从 Hextra 迁来的配置可以照搬。
其余的键(strict、reactionsEnabled、emitMetadata、term、lang、lightTheme、darkTheme、ariaLabel、errorMessage)都有默认值,完整定义见配置总览。功能开关既可以写 YAML 布尔值,也可以写 giscus 风格的 0 / 1。
按页开关
front matter 里的 comments 可以从任一方向覆盖全站开关,离页面最近的值优先。
只给某些页面开评论。全站关掉但保留完整仓库配置,再让选中的页面显式打开:
只关掉某些页面。全站开着,让不适合讨论的页面退出:
整个栏目统一设置用 cascade。本站在 content/docs/_index.zh.md 的 cascade 里写了 comments: true,本页底部因此有一个真实的 giscus 评论区。
站点同时配了 services.disqus.shortname 时,giscus 优先:giscus 生效即抑制 Disqus,comments: false 同时关掉两者,giscus 必填键不全则告警跳过、由 Disqus 兜底。
多语言文案
giscus 的界面语言自动跟随当前 Hugo 语言:简体、繁体、香港繁体分别映射到对应的 giscus locale,不支持的语言回退英文。只有自动选择不合适时才显式设 lang。
需要翻译的是 OINK 一侧的两句文案:评论区的无障碍标签与加载失败提示。它们按语言配置,与全局仓库配置合并:
语言层只需要写差异部分,repo / repoId / category / categoryId 留在 params.comments 里就够了。
跟随深浅色
theme: auto 时,giscus iframe 跟随 OINK 的深浅色切换按钮和浏览器的 prefers-color-scheme,读者切换主题时评论区一起变。
需要更贴合站点配色时,用 lightTheme / darkTheme 分别指定两套 giscus 主题,取值是 giscus 内置主题名或站点自己托管的 CSS。本站用的是后者:
theme 写成固定主题名时不再跟随切换。
giscus 的 iframe 从 giscus.app 加载,要读站点上的这个 CSS 文件需要 CORS 允许。本站在 hugo.yml 的 server.headers 里给本地预览加了 Access-Control-Allow-Origin: '*';线上由托管商的响应头配置决定。
隐私与 CSP
- OINK 不会索取或保存读者的 GitHub 密码与访问令牌,登录与发帖全程在 giscus / GitHub 一侧完成。
- 评论初始化脚本是主题自带的同源资源,只加入启用了评论的页面,未开评论的页面没有这段脚本。
loading: lazy时,读者滚动到评论区附近才加载 iframe。- 站点有严格的内容安全策略时,
script-src和frame-src都要放行 giscus,合并进现有策略而不是替换其它指令(总则见内容安全策略):
外部脚本加载失败或没能创建 iframe 时,OINK 结束加载状态并在实时状态区域显示 errorMessage,不会让页面停在「加载中」。
验证
然后逐项确认:
- 打开一个应该有评论的页面,页面底部出现 giscus,显示「使用 GitHub 登录」,界面语言是当前页面的语言。
- 切换 OINK 的深浅色,评论区跟着变(
theme: auto时)。 - 打开设置了
comments: false的页面,确认那里既没有 giscus 也没有其它评论组件。 - 发一条测试评论,回到 GitHub 看指定分类下是否出现了对应的 Discussion,并且能在 GitHub 上管理。
首次评论或表态创建 Discussion 之前,浏览器控制台提示「找不到 Discussion」是正常现象。
出问题时按这个顺序查:构建日志里的 WARNING(四个必填键)→ params.comments.enable 与 type → 页面 front matter 的 comments → 仓库是否公开、Discussions 是否开启、giscus App 是否安装 → 浏览器控制台与响应头(CSP 是否拦了 giscus.app)。找不到已有评论线程,先恢复原来的 mapping 和页面路径。
相关
6.4 - 分析与 SEO
主题默认不加载任何分析、表单或广告脚本,不配置就没有对外请求。接入需要显式配置,并把这条外部数据边界写进站点的隐私说明。SEO 一侧相反:canonical、hreflang、robots meta、Open Graph 与 Twitter 卡片由主题逐页生成,需要你做的是把 baseURL 与每页的 description 写对。
接 Google Analytics
用 Hugo 内置的服务配置,填 GA4 的 measurement ID:
主题只在 production 环境渲染这段脚本(hugo 构建默认 production,hugo server 默认 development)。本地预览与预览部署因此不上报数据,不需要另加开关。
不要同时设置已经弃用的顶层 googleAnalytics 键。不需要分析时删掉整段配置,不要填一个假 ID。
配上之后,页面浏览量与事件会发给 Google。严格的同源内容安全策略也需要为它放行,见内容安全策略。这是站点决策,不是主题默认。
接其它分析服务
Plausible、Umami、Matomo 这类服务只要求插入一段脚本。主题提供两个注入点,在站点仓库里建同名文件即可,不用改主题:
layouts/_partials/hooks/head-end.html,- 分析脚本、cookie 同意脚本、主题没提供的 meta 标签
layouts/_partials/hooks/body-end.html,- 只影响交互、不影响首屏的第三方代码
hugo.IsProduction 这一层不要省:没有它,每个人的本地预览都会向你的统计上报数据。
这是有意的:cookie 同意脚本必须先于分析脚本运行,才能真正拦住它。
「这篇文档解决了你的问题吗」反馈组件是另一件事:默认关闭,不发网络请求,配置见仓库与页面信息。
页面描述
<meta name="description"> 按这个顺序取值,取到第一个非空的就停:
- 页面 front matter 的
description - Hugo 计算出的页面摘要(
.Summary) - 站点配置里的
params.description
每页写一句 description 是唯一需要作者做的 SEO 动作。它同时用于三处:搜索引擎的摘要、栏目首页的卡片副标题、站内搜索的结果预览。
多语言站点要给每种语言各写一句,不要把英文描述抄到中文页上。站点级默认值也是分语言的:
canonical 与 hreflang
主题为每个页面输出一条 canonical 和一组 hreflang 备用链接,不需要配置:
hreflang 的语言代码来自各语言的 locale(本站是 en-US / zh-CN),链接来自 Hugo 的译文关系。上面英文那一条指向站点首页而不是对应的英文页:本页没有英文对等文件,Hugo 找不到译文时回退到目标语言首页。这是预期行为,也可以用来判断译文关系有没有被 Hugo 认出来。
canonical 由 baseURL 拼出。baseURL 配错时 canonical 会把搜索引擎指向不存在的地址,比构建失败更难发现。上线前照发布上线的验收清单查一遍。
多语言的完整配置在多语言。
社交卡片
主题调用 Hugo 内置的 Open Graph 与 Twitter 卡片模板,标题、描述、URL、语言、站名都是自动的:
要让分享出去的链接带图,在 front matter 里给 images:
给全站一张兜底图就把同样的键写进 params:
有图时 twitter:card 从 summary 变成 summary_large_image,并多出 og:image 与 twitter:image 两条。本站两处都没有设置,上面的渲染结果里因此看不到图片相关的标签。
站点地图
Hugo 自动生成,多语言站点生成的是一个索引:
站点级默认值和页面级覆盖都是 Hugo 原生的:
changefreq 与 priority 是提示不是承诺,搜索引擎可以忽略。值得做的是发布前确认草稿、私有内容与非规范副本没有进入站点地图,并且每种语言的那份都生成了。
robots.txt 与不收录
Hugo 只在站点配置里打开开关时才生成 robots.txt:
主题提供的模板按构建环境给出两种结果,不需要你写内容:
页面里的 robots meta 跟着同一个开关走:production 且不是打印输出时是 index, follow,否则是 noindex, nofollow。预览部署不要用 --environment production 构建,非 production 自带不收录的行为。
主题没有按页 noindex 的开关。某一页不该被收录时,可靠的做法是不发布它(draft: true,或用 Hugo 的 _build 选项)。既要发布又不想被收录,就用 head-end.html 钩子自己输出;主题已经输出了一条 robots meta,两条同时存在时如何合并由搜索引擎决定。
收录检查
上线一两周后,按这个顺序确认搜索引擎看到的东西和你以为的一致:
- 抓取权限:访问
<baseURL>/robots.txt,确认是Allow: /而不是Disallow: /。 - 页面清单:访问
<baseURL>/sitemap.xml,点进语言子地图,看页面数量对不对。 - 收录数量:在搜索引擎里查
site:你的域名,数量级对得上就行,不必逐页核对。 - 规范地址:搜索结果应当落在 canonical 指向的 URL 上,而不是带
?参数或旧域名的版本。 - 主动提交:在 Google Search Console / Bing Webmaster Tools 里加上站点并提交
sitemap.xml的地址,比等着被爬快。
搜索元数据补不了内容本身的问题:单薄、重复、过时的页面,写再好的 description 也一样。
验证
在产物里查这几项:
浏览器里再确认一次:打开一个代表性页面,看开发者工具的网络面板,没接分析的站点不应有指向第三方域名的请求。
相关
6.5 - 版本升级
升级 OINK 是换一个固定的模块版本,再确认站点仍能零告警构建。内容多数不用改; 0.4 shortcode 改成当前 Markdown 原生形态时,有一套默认干跑的迁移工具,不必手改 几百个文件。
升级会改变渲染结果。先建一个升级分支再动手,回退的代价就是丢弃一个分支。
先看发布注记
每个版本的变更、破坏性改动与升级要点都写在发布注记里,升级前先读一遍目标版本那篇:
- 本站的 项目博客 里的 release 系列
- GitHub 上的 Releases 页面
注记说明这次要不要改内容、有没有配置键被移除、默认行为有没有变化。跳过这一步的代价是升级后对着一个变了样的页面猜原因。
升级 Hugo Module
生产站点固定发布标签或不可变 commit,不跟随分支,也不用 @latest:
最后一条要能看到解析结果是那个标签本身,而不是伪版本(v0.0.0-2026...-abcdef)或 main。固定的版本落在 go.mod 里,跟着代码一起提交:
make dev 和 make check 会仅对当前命令设置 HUGO_MODULE_REPLACEMENTS,使用同级的主题 checkout。判定某个发布标签是否可用时使用不带替换的 make build,否则验证的是本地那份代码。
其它安装方式各一句。Git submodule:用 git submodule update --remote themes/oink 拉到新 ref,再提交 submodule 指针。离线归档与克隆:把 themes/oink/ 整个换成新版本的解压结果,确认 theme: 的值仍与目录名一致。三种方式的取舍见从零建站与其它安装方式。
升级后必做
三件事一起做了:清掉可能过期的缓存、用新版本重新构建、把任何告警变成失败。
--logLevel info 是为了看见 Hugo 的弃用提示。Hugo 的弃用分两级:先是 WARN 级提示(仍可使用),下一个版本变成 ERROR(构建失败)。带上 --panicOnWarning 相当于提前一个版本发现它们,把修复的时间留给自己。
构建通过之后,人眼再过一遍:首页、一个文档页、一个博客页、404、两种语言、两种配色、打印视图,以及站点自己定制过的地方。
内容迁移工具
0.4 的一批 shortcode 已换成当前 Markdown 原生形态。主题仓库带了一个只依赖 Python 标准库的工具做这件事:
用它的时候记住四条:
- 干跑是默认行为,只有
--write才落盘。先干跑,读 diff,再写。 - 重跑一次应该零改动。第二次
--write还报改动,说明有转换不收敛,停下来看那几个文件。 - 围栏里的文字不动,文档站里示范旧写法的代码块不会被误伤。
- 表达不了的构造原样保留,并附
file:line与原因列出,作为手工处理清单,不是失败。
只想先转某一类时用 --only,键名见下表最后一列:
改完重新构建一次(带 --panicOnWarning),并逐页看渲染结果:工具保证语法正确,不保证语义符合预期。
0.4 → 当前语法映射
{{%/* alert color= title= */%}}、{{%/* details */%}}、{{%/* pageinfo */%}}、手写<details><summary>,callout{{</* tabpane */>}}+{{%/* tab header= */%}}、{{</* code-group */>}}+{{</* code-tab */>}},tabs{{</* filetree */>}}与filetree/folder、filetree/file,filetree{{</* gallery */>}}与gallery/image,gallery{{</* echarts */>}}、{{</* infographic */>}},datafencedoc-cards/doc-card、nav-cards/nav-card、card/cardpane、doc-carousel,cards{{</* imgproc */>}}、{{</* image */>}},image{{</* readfile file= */>}},include围栏属性,{filename="x"}fencetitle{{</* badge outline= */>}},badge{{</* example */>}}+ 围栏、{{</* book-figures kind="tbl" */>}},eg{{%/* _param x */%}}、iframe、conditional-text、blocks/*、netlify、不带 kind 的xref,reportonly
每个新写法长什么样、有哪些参数,去组件里对应的那一页。
从 Docsy 迁移
OINK 是 Docsy 的硬分支:内容模型、td- 命名、Sass 变量、大部分 front matter 都还在。迁移的核心动作是删掉站点里复制的公共外壳,让主题的实现接管,而不是重写正文。
-
固定目标版本。在
go.mod里换成 OINK 的发布标签,或者用完整的版本化归档。评估期可以用不提交的go.work指向本地 checkout。 -
清点覆盖项。把
layouts/、assets/、static/下每个站点级文件归成四类:公共外壳的副本(验证后删)、OINK 已提供的组件(删或机械重命名)、品牌定制(保留,缩到最小 hook)、业务专属数据与交互(留在站点)。按引用关系删,不要清空layouts/:首页、下载页这些地方可能还在调用你要删的 partial。 -
搬配置。
title、languages.*、github_repo、github_branch、page_width、params.ui.*全部留在原来的语义位置,OINK 没有另起一套命名空间。搜索与 Logo 这类只要打开对应的键:hugo.ymlDocsy 的驼峰式检索键在 OINK 中已改名:
offlineSearch、offlineSearchIndex、offlineSearchMaxResults、offlineSearchOnServe、offlineSearchSummaryLength一律改为下划线形式。这一步要自己盯着改——那份「中断构建并报出新键名」的迁移登记表已经删除,旧键现在只是一个没人读的键,检索会一声不响地保持关闭。 -
字体与样式的兼容点。站点的
assets/scss/_variables_project.scss里那些 Docsy Sass 变量仍然生效,会作为字体角色的种子值,不用为了升级把它们删掉:$td-fonts-serif、$font-family-sans-serif、$headings-font-family、$font-family-code各自喂给对应的字体角色。Docsy 的 Google Fonts 开关$td-enable-google-fonts、$td-google-font-name与$td-web-font-path主题已不再读取,留在文件里不影响构建,也不产生任何效果:OINK 自带 Inter、Chakra Petch 与 IBM Plex Mono,任何预设都不向 Google Fonts 发请求。想换字体走 token 层,见品牌外观。 -
换 shortcode。Docsy 的
alert、pageinfo、tabpane、card系列都有当前对应 形态,用上面的迁移工具批量转,--only一类一类来。 -
一次删一组,每组构建一次。在临时副本里演练,记下主题 commit、Hugo 版本、删了哪些文件、产出多少个 HTML;确认等价之后再在生产分支上重做一遍。
第二步里「验证后删」的那一类,通常是这些文件:
layouts/baseof.html与公共的 docs / blogbaseof*.html;- navbar、footer、sidebar、TOC、search、head CSS 的 partial 及其对应 hook;
- 旧的品牌文档外壳 partial;
asciinema、echarts、infographic、doc-carousel、details、tab/tabpane、card 与param的 shortcode 副本;- 只服务于上述实现的 JavaScript、Lunr 副本、轮播代码与 SCSS;
- 不再被任何站点资源需要的 PostCSS 与 Autoprefixer 步骤。
删完之后有两类问题会浮出来。
站点自己的脚本报 $ is not defined:主题不带 jQuery,它以前由 Docsy 在每个页面的 <head> 里加载。主题的功能都不需要它,仍然需要的站点自己引入:
用 Docsy blocks/* 搭的首页在 OINK 构建中报
template for shortcode "blocks/cover" not found:主题没有这一组 shortcode。改用
data/home/<语言>.yaml 的首页分区,或给页面写 layout: landing,见
首页与落地页。
从 0.4 升级的要点
0.4 改了几个默认行为。升级后发现页面多了或少了东西,先看这几条:
-
顺序翻页默认开启。
docs、book、blog页尾都有上一页 / 下一页;文档沿侧栏树走,博客沿时间走。刻意不属于任何序列的页面用pager: false退出。 -
顶栏在所有布局上都显示。紧凑状态只有一行图标导航,没有第二套移动端手风琴菜单,依赖旧移动菜单的本地脚本与测试要删掉。整个分区不要顶栏时用 cascade 里的
navbar_enabled: false。 -
页脚默认
fat且全站生效。只接受fat/slim/none;页脚数据必须放在data/footer/<语言>.yaml(单语言站点用data/footer.yaml),data/home里残留的footer键会告警并提示新位置,严格发布构建拒绝这条警告。 -
单键导航默认开启:
/打开完整搜索,\只进命令模式。培训材料里描述旧行为的地方要改。页面操作也挪到了面包屑旁边的拆分按钮上。 -
代码块的 DOM 变了。
.td-code外壳套在原来的.highlight外面(.highlight与.chroma都保留),站点 CSS 里.td-content > .highlight这类直接子选择器要改成后代选择器.td-content .highlight。 -
两个 ICP 页脚参数被移除:
footer_icp与footer_icp_url换成一个支持行内 Markdown 的字符串。hugo.yml -
数学公式要站点自己开 passthrough。Hugo 不会合并主题的
markup配置,用\(…\)、\[…\]、$$…$$的站点必须在自己的hugo.yml里启用 goldmark passthrough 扩展,见公式。
验证
升级不是「构建通过」就算完,按表面分别看:
文档 / Book- 侧栏顺序、翻页、标题、页面操作、编号与交叉引用
博客- 时间顺序翻页、RSS 归属、顶栏与页脚
首页 / Landing- 无 JS 时的内容、紧凑菜单、打印
发布页- 推导出的下载 URL、校验和、发布状态
组件- 站点用得最多的那几个组件各找一页看渲染结果
无障碍- 纯键盘走一遍、焦点顺序、两种配色、强制颜色模式
部署- 站内链接与资源都保留了 base path 前缀
本站的完整门禁是:
其它站点跑等价的构建、链接、输出与浏览器检查即可,细节见排错与检查。
源码可构建、标签已签名并能通过 Go proxy 解析、站点已固定该标签、线上已部署,这是四件事,要分别记录。别用一次绿色的本地构建代替它们。
最后一步在真实环境上做:先部署一份预览,在真实 URL 上验证页面与浏览器的网络请求,评审通过再合并,合并后在生产上做一次冒烟测试。
回滚
回滚的是版本固定,不是工作树:
三条原则:
- 保留升级前的模块固定、站点 commit 与已知可用的部署产物,回滚时三者一起恢复。
- 不要只回滚一部分。给新主题塞回几个旧布局副本,会得到一个比任何完整版本都更难诊断的混合状态。
- 升级分支与验收证据都留着。回滚是为了先恢复线上,不是丢掉已经做完的工作。
线上产物本身的回滚(重新发布上一个部署)见发布上线。
相关
- 发布上线 — 部署产物的回滚
- 排错与检查 — 升级后构建报错怎么读
- 本地预览 — 清缓存与
go.work工作区 - 从零建站与其它安装方式 — 四种安装方式的取舍
- 组件总览 — 每个组件的当前写法
6.6 - 排错与检查
出问题时先做一次干净的生产构建,从第一条错误开始看,后面的多半是级联结果:
日志里出现 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤,说明配置里混进了上游 Docsy 的流程。OINK 消费端的构建只有一条 Hugo 命令。
下面四张表按「症状 → 原因 → 修法」组织,找到症状那一行即可,不必从头读。
构建
| 症状 | 原因 | 修法 |
|---|---|---|
| 构建报要求更高的 Hugo 版本 | 装的是标准版而不是 Extended,或版本低于 0.160.1 | hugo version 输出里必须有 extended。多个 Hugo 共存时先查 PATH 与版本固定配置,而不是再装一份 |
module "github.com/pgsty/oink" not found |
主题没解析出来 | Hugo Module:看 hugo mod graph、go.mod、go.sum,以及有没有多余的 workspace / replace。submodule:CI 有没有在 Hugo 之前跑 git submodule update --init。归档 / 克隆:theme: 的值要与 themes/ 下的目录名一致 |
| 模块下载卡住或超时 | Go 的模块代理不通 | Hugo 通过 Go 拉模块,所以走 GOPROXY。国内网络可以 export GOPROXY=https://goproxy.cn,direct;隔离环境改用离线归档或提交 themes/oink/ |
页面上出现 {.cards}、{.steps}、{caption=…} 这类原样文字 |
站点没开 goldmark 的块级属性 | 站点的 hugo.yml 里必须有下面那三项,主题的 markup 配置不会被 Hugo 合并进来 |
图片带属性行时被包进了 <p>,图注没生效 |
缺 wrapStandAloneImageWithinParagraph: false |
同上,三项一起加 |
| 行内 HTML 被转义成文字 | 缺 renderer.unsafe: true |
同上 |
\(…\) $$…$$ 原样显示 |
站点没启用 goldmark passthrough | 见公式;math: true 不是启用开关 |
shortcode "tabs" must be closed or self-closed |
有 {{< tabs >}} 没写对应的 {{< /tabs >}} |
报错里带 文件:行:列,去那一行补上闭合标记 |
template for shortcode "tabs" not found |
正文里写了一个不存在的 shortcode,或引用 shortcode 语法时没有转义 | 文档里讲解 shortcode 语法时必须转义:在开标记与闭标记的内侧各加一对 /* 与 */,Hugo 才会把它当文字而不是调用。名字打错就改回正确的名字 |
... attributes: unknown attribute "witdh" at ... |
属性行里的键拼错或不被允许 | 警告列出允许键并忽略坏属性;style 与 on* 同样丢弃。--panicOnWarning 在发布时把它变成失败 |
shortcode "field": unsupported parameter "colour" at ... |
shortcode 参数名不对 | 警告指出 shortcode、参数、文件与行号,再忽略不支持的参数或组件。普通预览保持可用,严格发布失败 |
invalid params.ui.page_width "widee" (allowed: normal | wide | full) -- using "normal" |
配置或 front matter 的取值,不在允许集合里 | 配置类的错误降级而不中断,一个笔误不会让 hugo server 下每个 URL 都返回 500。消息里带键名、收到的值和实际用的回退值。构建加 --panicOnWarning,它就上不了线 |
| 某个页面设置不生效,也没有任何提示 | 键写在了 front matter 的 ui: 段里 |
页面键写在 front matter 顶层,键名是站点键去掉 ui.。写进 ui: 段的键没有人读,也没有人报错,见页面参数 |
| 构建通过但线上少东西 | 有 WARNING 没人看 | 构建命令加 --panicOnWarning。非法配置取值、giscus 必填键缺失、不支持的 comments.type、Hugo 的弃用提示都只是告警 |
那三项 goldmark 配置:
两个最常见的 shortcode 报错长这样,注意结尾的 文件:行:列:
语言
| 症状 | 原因 | 修法 |
|---|---|---|
| 译文页面不出现 | 四种可能,按顺序查 | ① hugo.yml 里有 languages.zh 且设了 weight;② 文件名是 page.zh.md,zh 必须小写;③ 译文 front matter 没有 draft: true,date 不在未来;④ 影响路由的元数据与源文件一致 |
| 语言切换跳到了首页 | Hugo 没找到对应译文 | 这是设计行为:找不到译文就回退到目标语言首页。要跳到对应页面,需要那个译文文件确实存在 |
| 锚点链接打开了页面却不定位 | 译文标题文字不同,自动生成的 ID 也不同 | 在译文标题上显式写英文 ID:## 安装 {#installation}。标题里含 shortcode 或行内 HTML 时不要凭文本猜 ID,去看英文页渲染出来的 HTML |
| 菜单 / 首页分区没翻译 | 这些不在页面里,在配置和数据文件里 | 菜单在 languages.<lang>.menus,首页分区在 data/home/<lang>.yaml,界面字符串在 i18n/<lang>.yaml,见多语言 |
中文页 hreflang 指向英文首页 |
该页没有英文对等文件 | 补上英文页,或接受这个回退:它同时是「Hugo 有没有认出译文关系」的探针 |
搜索
| 症状 | 原因 | 修法 |
|---|---|---|
| 搜索框有但一直没结果 | 索引没生成 | params.offline_search: true 之后,产物根目录下应该有 offline-search-index.<语言>.json,每种语言一份。没有就是没开 |
| 索引文件请求 404 | baseURL 不对 |
子路径部署下 baseURL 配错是索引 404 最常见的原因。先在浏览器网络面板看它去哪里取索引,见发布上线 |
hugo server 下搜不了,构建出来就正常 |
站点把预览期的索引关掉了 | params.offline_search_on_serve 默认为 true,预览与线上行为一致;配置里显式写成 false 时预览不生成索引,删掉或改回 true |
| 中文搜不到 | 多数不是分词问题 | 中文查询走主题的 CJK 子串回退。先确认那个中文页面的内容进了中文索引(打开 offline-search-index.zh.json 查一下),再看分词 |
| 新页面搜不到,旧页面正常 | 索引是构建产物 | 重新构建。hugo server 下改了页面要等它重建完 |
params.search.algolia requires explicit appId, apiKey, and indexName values |
Algolia 三个键没配全 | 三个键必须显式给全,主题不会替你用别的项目的 DocSearch 凭据。不用 Algolia 就把这段配置删掉 |
| 命令面板搜不到内容 | 它与全文检索是两件事 | 索引不可用时命令面板仍然能打开,只是提示索引不可用,页面操作与命令照常,见命令面板 |
平台
| 症状 | 原因 | 修法 |
|---|---|---|
| GitHub Pages 上页面 404 或样式全丢 | 项目站点的 URL 带仓库路径,baseURL 没带 |
用工作流里的 --baseURL "${{ steps.pages.outputs.base_url }}/",别手写。完整工作流见发布上线 |
| GitHub Pages 上「最后修改时间」「贡献者」全空 | checkout 是浅克隆 | actions/checkout 加 fetch-depth: 0:enableGitInfo 要读完整历史 |
| Cloudflare Pages 构建报 Hugo 版本太低 | 构建镜像的默认 Hugo 低于主题要求 | 在 Production 和 Preview 两个环境都设 HUGO_VERSION,并设 SKIP_DEPENDENCY_INSTALL=1 |
| 托管商构建时拉不到主题 | 构建环境没有 Go | Hugo Module 需要 Go。平台不提供就改用 submodule 或把 themes/oink/ 提交进仓库 |
| CI 上构建结果和本地不一样 | go.work 参与了 CI 构建 |
CI 里设 GOWORK: off 与 HUGO_MODULE_WORKSPACE: off,让它只认 go.mod 里固定的版本 |
| 预览部署被搜索引擎收录了 | 预览也用了 production 环境构建 | 预览构建不要带 --environment production,非 production 自带 noindex 与 Disallow: /,见分析与 SEO |
| macOS 报打开文件过多 | 实时预览监视的文件超过了 shell 限制 | 先把生成目录与无关目录排除出监视范围,这通常才是根因;再考虑 ulimit -n |
| WSL 下很慢或漏掉改动 | 跨 Windows 挂载点工作 | 让 Hugo 处理 Linux 文件系统里的路径,跨文件系统的变更通知和权限行为会让实时重载失效 |
| 缺 Bootstrap / Font Awesome / Lunr / Mermaid 之类资源 | 发行物不完整 | 不要用 CDN URL 掩盖。确认 assets/third_party/、assets/js/third_party/、static/webfonts/、VENDOR.json 都在;确实缺就重新获取同一个固定版本 |
站点自带检查
除了构建本身,站点还可以自己跑这几项。前两条任何 OINK 站点都能用,后面几条是本仓库的 npm 脚本,其它站点跑等价的检查即可。
零告警构建,- 重复输出路径、参数非法、外部集成配置不全
输出信任检查,- 四种输出里的每个
href/src都是站内相对或http(s)/mailto/tel;没有javascript:URL、没有行内on*事件处理器;跨站的<iframe><script><img>等要显式加--third-party才放行 翻译对等,- 每个英文页有没有中文对等页,以及渲染后的标题 ID 是否逐一对齐;锚点链接错位在这里暴露
完整门禁,- 下面六项串起来跑
npm test 里的六项各管一段:
test:base— 先构建一次,再跑 Markdown 风格、翻译对等、渲染后的 Markdown 与链接检查。test:hugo-build— 构建断言:博客元数据、RSS、内容组件、构建过程零弃用提示。test:md-output— Markdown 与llms.txt输出的 golden 比对,字节级。改了组件的 Markdown 形态就会在这里挂。test:alt-site— 用tests/fixtures/*.yml里的替代配置各构建一次,确认不同配置组合都能起来。test:favicons— head 输出的 golden 比对。test:release-pin-contract— 站点公告的版本与go.mod固定的版本是否一致。
浏览器行为另开一套:npm run test:browser 依次跑 Playwright 的无障碍(axe WCAG AA)、响应式外壳、键盘导航、内容组件、代码块与场景组件六个套件。
check-output-security.py 在主题仓库里它在主题仓库的 bin/ 下,是产品级的信任检查,任何 OINK 站点都可以跑,不依赖站点的测试框架。克隆主题仓库后指向自己的 public/ 即可,参数与用法见断网构建验证。
诊断习惯
上面的表覆盖不到的问题,按这几条挖:
- 用固定的 Hugo Extended 版本复现,不在版本浮动的环境里判断。
- 清掉
public/与resources/_gen再重建,排除陈旧缓存。 - 对比开发与生产两套配置层,很多只在线上出现的问题是环境差异。
- 看第一条错误,不是最后那条。
- 用一个最小页面区分「主题行为」和「站点覆盖」:把可疑内容单独放一页,站点覆盖分批重新启用,定位到具体那一项。
- 看故障页面的浏览器控制台与网络面板,尤其是 404 的资源路径。
求助渠道
开 issue 时带上这几样,能省掉一轮来回:Hugo 版本(hugo version 完整输出)、主题版本(hugo mod graph | grep oink)、第一条完整错误、能复现的最小页面或最小站点。
- 主题与文档的问题:https://github.com/pgsty/oink/issues
- 本站内容的问题:https://github.com/pgsty/oink.pgsty.com/issues
- 上游 Docsy 的兼容性讨论:https://github.com/google/docsy/discussions
相关
7 - 设计与开发
本专栏公开随 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 管理内置
依赖的版本、许可证、文件与校验和。
公共行为发生变化时,必须在同一次交付中更新实现、对应检查器以及本目录下相关 契约的中英文版本。测试应验证行为和输出,不应只固定某段文字。
7.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 |
各输出格式按既定顺序执行;可变格式状态并不存在跨格式竞态。但在 Print 内,Hugo 可能并行渲染同一 Book 页面与相互重叠的聚合。因此每页由一个缓存 coordinator 按 固定顺序生成普通与整书两种变体,各调用方只选择自己需要的形态。普通 Print 保留 页面局部标题与带路由的 xref URL;Book 聚合保留带命名空间的标题与文档内 xref。
站点自行选择是否启用自定义输出;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
密度。
信任边界、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 构建只能证明本地验证通过。
7.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>,绝不执行嵌入脚本。
Swagger 与 Redoc 接受 HTTP(S) 规范 URL 或以 static/ 为根的路径,都不解析页面
资源。Redoc 将开头有无斜杠视为等价,并把本地路径与 baseURL 拼接。只有 HTML
输出可交互;Print、Markdown 与 RSS 输出静态规范链接。
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;浏览器测试覆盖交互界面。迁移行为见 迁移边界。
7.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。无效策略遵循共享的警告与
回退契约。达到 sidebar_cache_limit 后,两种 walker 只有在语言、导航根与实际
影响输出的有效设置均相同时才复用中性标记。没有 JavaScript 时这份标记仍然可见;
普通外壳运行时只补上 active 路径。会输出 sidebar_headings 的 Book 页面保持页面
专属,并绕过共享树缓存。
沉浸式博客展示
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.scroll_spy 与页面键
scroll_spy 在整个 1.x 期间都是静默兼容 no-op,不加载独立运行时;只有未来的
破坏性版本才会删除它们。
分享
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 把整个分区显示为日期、标题、标签行,不分页。卡片使用共享
首图、本地化日期/作者/分区元数据、标签与三行摘要。
分类法页(/tags/、/authors/)与其术语页共用一个页头
shell/taxonomy-head.html。分类法页以整体分类法图标的着色方块、本地化名称与
术语数开头;术语页以术语标题与取自 ui_taxonomy_pages、按当前 locale 的 CLDR
复数类别选择的页面数开头,没有渲染面包屑时标题上方再加一行
kicker,写明分类法并链回列表页——面包屑开启时它在上一行已经做了这两件事:代表
生成的分类法页的那一级面包屑借用页头同一个本地化标签,而不是 Hugo 的复数名标题。
页头之下,分类法页把术语排成单行卡片网格 shell/taxonomy-cards.html:使用次数
多者在前、同数按字母序(与右栏词云同序),以 auto-fill 填满等宽列,因此术语
很少时两张卡片也不会被拉宽到整页。一张卡片就是术语图标、术语名与页面数,整张
卡片即链接;只有作者以署名同款小头像开头,走同一个头像 partial。卡片不带描述、
不带最新一页:术语没有标题与计数之外值得一说的内容,多出的那一行只会让网格发糊。
不再有筛选芯片行与「全部」芯片:分区根已经在侧栏与顶栏里。术语页保持行列表,
作者资料页保留自己的页头。
分类法页与术语页的右栏以 shell/taxonomy-switcher.html 开头:声明的每种分类法
一行——整体图标、本地化名称、术语数——链向其列表页,当前分类法置于选中底色上。
这是从一种分类法的页面去另一种的路:词云芯片跳到术语页,词云头只负责折叠;只有
一种分类法的站点不渲染切换器。这一组与词云共用 toc_taxonomies 开关。分类法页
的词云按全站统计(taxonomy-root.html 对该 kind 不返回根),并省略自己那一组,
它的术语就是旁边的卡片;术语页保持分区作用域与完整的一组。
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 与消费站点浏览器套件覆盖导航、语言与子路径链接、博客变体、页尾顺序、
键盘行为、无障碍与响应式布局。
7.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 继续有效。
7.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 仅作为 1.x 静默兼容 no-op 保留 |
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 路由,再分别记录固定 版本、部署与线上一致状态。
7.6 - 设计决策
决策记录解释 OINK 为什么在多个兼容方案中选择了当前设计。上方五份契约仍是 现行行为的规范描述;实现与归属检查器仍是可执行事实。
OINK 过去把评审、PRD 与执行记录放在本地 plan/ 目录中。这样既不便发现有价值的
推理,也容易让已经放弃的设计看起来仍有权威。已经接受的理由现在统一进入这座双语、
版本化的文档站,与它所支撑的契约放在一起。
决策地图
| 决策 | 解决的问题 |
|---|---|
| 警告与安全回退 | 为什么普通预览能容忍错误输入,而发布仍保持严格 |
| 配置模型 | 配置放在哪里、页面如何覆盖,以及 OINK 为什么不另造配置命名空间 |
| Markdown 优先创作 | 为什么优先使用原生 Markdown,以及 Docs、Blog、Book、Landing 如何延长共享系统 |
| 生成式配置 Schema | 为什么编辑器 Schema 是生成的投影,以及漂移门禁如何阻止第三个配置权威出现 |
记录格式
一份已接受决策应记录背景、选择、后果,以及证明该选择仍然成立的证据。它不重复参数 参考或教程。每份决策都要链接到归属契约与验证面,中英文页面必须同步修改。
决策发生变化时,应在同一次交付中更新实现、检查器、受影响契约与决策记录。旧答案留在 Git 历史和版本变更记录中,不在导航树里并列保留两套“现行”答案。
相关
7.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,以及主题夹具与本站的严格构建。
7.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 的不变量。公开参考及其
中文对页由集成站的双语和渲染链接检查覆盖。
7.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 事实记录在 块属性研究中。
7.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.7 - 设计研究
研究记录测量了什么、使用了哪些输入与工具版本。它可以解释决策,但不能覆盖当前契约或实现。
只有其他维护者能够检查方法、理解边界并复现相关检查时,研究才适合进入公开 Design 内容树。 原始 Agent 对话、临时构建日志和本机绝对路径不符合这一标准。
研究地图
| 记录 | 证据 |
|---|---|
| Goldmark 块属性 | 支持的 Hugo 下限版本上,渲染钩子能看到什么,以及 CommonMark 容器的边界 |
| 消费站与迁移证据 | 带日期的语料盘点与确定性 Book 迁移结果 |
| 2026-08-26 全面审查 | 实现、配置、输出、安全、测试、性能与文档审查 |
发布规则
研究记录必须说明日期、输入、相关版本、方法、结果与已知边界。容易变化的数字明确标为快照。 涉及外部框架的比较,公开前要依据一手资料重新核验,并提炼成与 OINK 有关的结论,不能直接 复制成竞品目录。
7.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.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 边界见 创作书籍和 迁移契约。
出版采纳快照
2026-08-24 的隔离验证让两个消费站运行了已发布的通用 Book 出版链路:
| 消费站 | 通用出版证据 | 下游状态 |
|---|---|---|
| DDIA | 23 个有序页面、131 个带类型目标与 292 条已解析交叉引用;EPUBCheck、内部检查与 PDF 检查均通过 | 该快照中仍保留语义预处理器,等待消费站独立接受新的门禁 |
| TPME | 18 个有序页面、41 个带类型目标与 1,062 条已解析交叉引用;同一套通用检查通过 | 第二个消费站证明了可移植性,没有形成上游迁移门禁 |
这是下游采纳证据,不是尚未解决的上游设计边界。
边界
这些数字不能直接用于产品宣传,也不能当作当前站点清单。重做研究时,需要重新确定仓库清单并生成 新的带日期报告。本公开记录刻意排除了本机路径、未提交内容、私有仓库名称、原始 Agent 对话与生成 构建产物。
7.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/契约,本次没有重新发布或部署任何站点。
7.8 - 设计提案与 PRD
提案描述的行为可能尚不存在。当前行为由契约、已接受决策、实现与归属检查器定义。不能把提案 当作配置参考。
本栏目是 OINK 产品需求文档、RFC 风格设计与未决维护者提案的唯一正本位置。不要在主题仓库或
文档仓库中另建本地 plan/、plans/、proposal/ 或其它并行设计树。
当前提案
| 提案 | 当前边界 |
|---|---|
| 反向链接与知识图谱 | G1(静态反向链接)已接受,已在主题 main 分支实现,随 OINK 0.8.0 发布;局部与全站图谱(G2/G3)保持草案 |
| 媒体收敛 | 部分已实现;media-result 契约与 Landing 资源元数据已交付,M3 决议为原生图片处理,退役(M4)保持开放 |
Agent 批量索引提案已在输出交付后退役。稳定行为现在归属
架构,用户步骤归属
Agent 就绪输出。Book 出版提案也在 BookManifest 与 EPUB/PDF
工具交付后退役。稳定行为归属架构与
创作书籍,带日期的下游采纳证据归属
消费站证据。剩余的消费站采纳工作
不会让上游设计提案继续保持活动状态。两份提案草案均由 Git 历史保存。
生成式配置 Schema 提案已按生命周期退役:行为的规范位置是配置总览, 长期理由进入生成式配置 Schema 决策,草案原文由 Git 历史保存。
新 PRD 放在哪里
创建一份英文主页面及其简体中文对页:
两份文件都使用显式、稳定的英文标题 ID。中文页面中的代码、键、路径、版本与 API 名称保持原样。 提案开头要有可见的草案状态,并包含:
- 状态、负责人、日期和受影响契约面;
- 背景与证据;
- 目标与明确非目标;
- 提议行为,以及输出、无障碍、安全边界;
- 兼容与迁移影响;
- 实现与归属检查器计划;
- 验收标准与待决问题;
- 记录提案自身变化的决策日志。
大型实验可以在 ../research/ 下增加带日期的页面;临时日志与生成
产物不进入 Hugo 内容,也不进入 Git。
生命周期
提案被接受后不会自动成为第二份契约。稳定行为进入归属契约,稳定理由进入 Decisions,用户步骤进入 相关指南,然后把提案退出活动导航。本地构建、提交、tag、公开模块、消费站 pin 与部署仍是相互独立 的完成状态。
评审门禁
实施前,评审者确认提案没有重复已有外壳、resolver、组件族或数据权威。实施期间,如果设计改变, 先更新这份双语提案,不能让代码悄悄漂移。验收至少覆盖主题的最窄归属检查、真实文档站、渲染后的 中英文、相关输出、无障碍与响应式检查。
7.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。
7.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处理能力是否仍有真实消费需求? - 哪些输出兼容名称仍被真实消费站使用?