这是本节的多页打印视图。 .
创作内容
本栏覆盖 OINK 支持的几种内容类型:文档页、博客文章、书籍、发布下载页、OpenAPI 参考。它们共用同一套 Markdown 与 front matter,各自另有约定。
一页文档的构成
一页文档是一个 Markdown 文件。文件开头两行 --- 之间是 front matter,即页面元数据:标题、侧栏短名、描述、排序。其余部分是正文,内容为普通 Markdown 加 OINK 的原生组件。下面是一个完整页面:
存为 content/docs/install.zh.md,运行 hugo server 后页面出现在 /zh/docs/install/,侧栏出现「安装」一行。
内容类型与对应页面
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:
- 克隆文档站并预览:
提示
加
-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做这项审计)。
相关
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 - 页面参数
本页是页面级参数的全表,只列 OINK 主题会读取的键。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 构建,那条警告在真正要紧的地方仍然是硬失败。
少数几个键确实会中断构建,表里会写明。它们是那种「继续构建就会发布出错误内容」而不只是「发布出朴素内容」的情形:残缺的上游署名(半条声明读起来和完整的一模一样)、translation_notice、release 事实、落地页的 sections,以及任何解析不到目标的引用。
基本
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改为链接父栏目。非布尔构建失败 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, ,- 顶栏在指针设备上自动隐藏
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, ,- 目录的滚动跟随;写
true打开 keyboard_nav, ,- 单键键盘导航,见键盘导航。非布尔告警并回退
lastmod_commit, ,- 「最后修改」后面怎么显示提交。非法值告警并回退
sidebar_expand_levels、sidebar_menu_compact、sidebar_menu_foldable、sidebar_item_overflow, ,- 侧栏行为也可以逐页覆盖;取值见配置总览
搜索
指南在全文检索。
search_keywords, ,- 附加检索词,包含中英文与同义词
search_boost, ,- 排序乘数,最终得分为文本匹配分乘以该值。非数字、非有限、零或负值告警并回退
1.0 search_exclude, ,- 不进本地索引
输出形态
指南在 Agent 支持(.md 与 llms.txt)与打印支持。
outputs, ,- 这一页生成哪些输出格式;写
[HTML]时不再生成.md no_print, ,- 不进入整章 / 整书的聚合打印输出
页尾:评论、反馈与出处
顺序固定为反馈 → 出处 → 翻页器 → 评论,见编写页面。
comments, ,- 本页是否显示 giscus 评论区,见启用评论
feedback, ,- 映射形态支持
enable与reasons。其它写法告警并回退 annotation, ,- 页尾的「最后修改 / 出处」区块。只接受布尔,其它写法告警并回退
translation_notice, ,- 权威版本的语言代码,译文据此显示一条指回原文的说明;本页即以本语言原创时写
false
上游出处
页面改写自别处的材料时,用 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: []表示不要封面 featured_image, ,- 本文正文里怎么渲染自己的题图。非法值告警并回退
blog_index, ,- 写在博客根目录上,决定该栏目列表页的形态。非法值告警并回退
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, ,- 发布事实。字符串形态是
https://github.com/<owner>/<repo>/releases/tag/<tag>;映射形态的键是productversionrepotagdateprevchecksums,version与repo必填,未知键或类型不符构建失败 release_products, ,- 发布列表只保留这些产品。非法过滤条件构建失败
release_group_by_product, ,- 按产品分组;开启后每一篇被选中的文章都必须写
release.product
相关
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 能点进对应的标签页。
相关
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>,即引用目标,不是代码块本身。例的题注必填:只写 num 或只写 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 这类锚点时冲突,生成的标题链接同步改写。产物是面向打印的 HTML,PDF 与 EPUB 由站点自行处理。
具体开关与整章打印见打印支持。
迁移既有书稿
已有的中文书稿通常用站点自己的 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 形态。
- 主题只到打印 HTML 为止:分页、字体嵌入、索引编制、PDF / EPUB 打包都在契约之外。
相关
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;每种语言、子路径部署各测一次。
相关
7 - API 文档
一页接口文档由一份 OpenAPI 规范加一个 shortcode 构成。Swagger UI 与 Redoc 两个运行时随主题分发(版本分别是 5.32.13 与 2.5.3,见仓库 VENDOR.json),页面用到才加载,构建与浏览都不访问外部服务。
三个步骤:把规范文件放进 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/
不要把规范文件放在页面旁边。redoc 会在内容目录里查找同名文件并据此拼出 URL,但内容目录里的 .yaml 是页面资源,Hugo 只在它被引用或处理时才发布。redoc 只拼 URL、不引用资源,浏览器因此得到 404。
远程规范(https://… 开头)两个 shortcode 都接受,但那是一项网络依赖,还会把读者的元数据暴露给那台主机。内网部署与有 CSP 的站点应当使用同源规范。
下面的例子用真实存在的 /openapi/docs-demo.yaml,一份演示用的集群管理 API,没有可访问的服务端。
Swagger UI
swagger 只有一个具名参数 src,值是从站点根开始的 URL。它经过主题的 URL 校验,子路径部署同样正确:
它渲染一个 class="td-swagger-ui" 的容器并就地初始化。容器 ID 由页面地址与 shortcode 序号推导(td-swagger-<hash>-<n>),因此同一页可以放多个。
本页只给源码,不真渲染 Swagger UI:它自己生成的标记有三处 axe WCAG AA 违规(服务器下拉框没有可访问名称、版本号区域是不能聚焦的可滚动区),本站的无障碍门禁要求每个页面零违规。下面的 Redoc 是真渲染的。
Redoc
redoc 只接受一个位置参数,即规范路径。多写一个参数构建失败。
路径解析按顺序有三条分支:http 开头视为远程 URL;能在内容目录里找到同名文件时用 baseURL + 页面目录 + 文件名;否则用 baseURL + 原样路径。redoc 的路径因此不要以斜杠开头,/openapi/… 会拼出 https://example.com//openapi/… 这样的双斜杠。与 swagger 不同,它生成基于 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 |
| 打印 | 只有空容器:两个界面都由 JavaScript 在浏览器里生成,打印输出里没有内容 |
| Markdown | 原样输出容器 <div> / <redoc> 与初始化脚本,不会退化成接口清单 |
| RSS | 同 Markdown |
接口文档只在 HTML 里有内容。要让打印或 Agent 输出里也有接口信息,在同一页用正文写关键端点的说明;shortcode 之外的正文在四种输出里都完整保留。
限制与常见问题
- 两个组件的容器 ID 都按「页面地址 + shortcode 序号」推导,同一页放多个互不冲突。
- 两者可以同页共存,但页面会很长,也会同时加载两套运行时。正式站点选一个。
- Swagger UI 的标记有 axe WCAG AA 违规(
select-name、scrollable-region-focusable),它来自上游产物,主题不改写。站点若有零违规的无障碍门禁,把这类页面排除,或改用 Redoc。 redoc不接受额外属性参数:写第二个位置参数构建失败。redoc路径不要以/开头,否则拼出双斜杠。- 规范文件必须能被浏览器取到:放
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 或跨域报错。
- 断网后再刷新一次:运行时是本地的,规范同源时界面应照常出现。