跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

创作内容

写文档页、博客、书籍、发布页与 API 文档:一页文档长什么样,内容怎么组织。

本栏覆盖 OINK 支持的几种内容类型:文档页、博客文章、书籍、发布下载页、OpenAPI 参考。它们共用同一套 Markdown 与 front matter,各自另有约定。

一页文档的构成

一页文档是一个 Markdown 文件。文件开头两行 --- 之间是 front matter,即页面元数据:标题、侧栏短名、描述、排序。其余部分是正文,内容为普通 Markdown 加 OINK 的原生组件。下面是一个完整页面:

content/docs/install.zh.md
---
title: 安装 Pigsty
linkTitle: 安装
description: 在一台干净的 EL 9 机器上装出可用的 PostgreSQL 集群。
weight: 20
---

## 前提条件 {#prerequisites}

一台能 SSH 登录的 Linux 机器,`sudo` 免密,Python 3.11 或更高版本。

> [!IMPORTANT]
> 安装脚本会改写 `/etc/yum.repos.d/`,先备份。

存为 content/docs/install.zh.md,运行 hugo server 后页面出现在 /zh/docs/install/,侧栏出现「安装」一行。

内容类型与对应页面

你要写的 去哪页
一页文档:front matter、标题锚点、链接、图片、草稿 编写页面
目录树与侧栏:_index.mdweight、图标、折叠、多根侧栏 组织内容
查某个 front matter 键是什么意思 页面参数
一篇博客、发布公告、RSS 博客与文章
一本书:章节编号、图表式例、交叉引用、整本打印 书籍出版
一个发布下载页:版本卡片、资产表、校验和 发布与下载页
一份 OpenAPI 参考页 API 文档
中英双语写作:对等文件、锚点对齐、缺译回退 多语言
某个组件的语法与参数 组件总览

1 - 编写页面

新建一页文档:文件放在哪、front matter 写什么、标题锚点为什么要手写、链接与图片怎么写、页尾会自动出现什么。

本页覆盖一页文档的完整写法:文件位置、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页面资源,两种语言共用

hugo new content docs/install.md 用 archetype 生成一个带 front matter 的空文件,见 Hugo 文档;手写文件同样可行。

重要

中文页没有英文对等页时,Hugo 不会把无语言后缀的资源分给它。这种情况下资源文件名要带 .zh.shell.zh.webp),正文里仍然写 shell.webp

必要的 front matter

文件开头两行 --- 之间是 YAML front matter。四个键每页都应写上:

content/docs/install.zh.md
---
title: 安装 Pigsty          # 页面大标题、浏览器标题、搜索结果标题
linkTitle: 安装             # 侧栏与面包屑里的短名,省略时用 title
description: 在一台干净的 EL 9 机器上装出可用的 PostgreSQL 集群。
weight: 20                  # 同级页面的排序,用 10 的倍数留出插入空间
---

description 用一句话说清这页让读者做成什么。它出现在栏目首页的卡片、搜索结果与社交卡片中。weight 决定侧栏顺序,weight 相同时才退回字母序。

其余的键可选:图标、草稿、搜索权重、评论开关、页面外壳等,全表见页面参数

标题层级与稳定锚点

正文用 ## 开始分节,# 留给 title。主题已渲染页面大标题,正文里再写一个 # 会出现两个一级标题。右栏的页面目录从 ## 开始收,收到第几级由 Hugo 的 markup.tableOfContents 决定,本站是 ####

每个 ##### 都要手写英文锚点 {#id}

源码
## 前提条件 {#prerequisites}

### 磁盘与内存 {#disk-and-memory}

理由有两条:

  • 中英对齐。Hugo 从标题文字生成 ID,中文标题生成中文 ID:/docs/install/#prerequisites/zh/docs/install/#前提条件 指向同一个语义位置,却是两个锚点,翻译审计无法比对。译文标题写上英文页的 ID,两边即同一个片段。
  • 链接稳定。标题文字会随措辞调整而改变,公开链接不应随之失效。显式 ID 一旦发布即视为公开路由;需要改名时保留旧 ID 的空锚点:
源码:给旧锚点留一个空目标
## 快速开始 <a id="get-started"></a> {#quickstart}

ID 用短横线小写英文,全页唯一。本站的翻译审计脚本会比对英文页与中文页渲染出的标题 ID,不一致就报错。

三种写法,用途不同:

写法 例子 什么时候用
站内绝对路径 [配置总览](/zh/docs/customize/config/) 默认写法。指向已发布的路由,便于审计与全站替换,不受源码文件移动影响
相对路径 [另一页](../organize/)![图](shell.webp) 同一页面包内的资源,或有意跟着源码目录走的相邻页面
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 的页面不会进入构建产物:

front matter
---
title: 尚未定稿的迁移指南
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 围栏(还有 plantumlmarkmapecharts Mermaid

剩下的少数组件(徽章、按键、引用文件、终端录像、Book 的图表式例)用 shortcode,语法与参数见组件总览

组合例子:步骤里放代码围栏与提示块。

源码
1. 安装 Hugo Extended,最低 0.160.1:
   ```bash
   brew install hugo
   ```
1. 克隆文档站并预览:
   ```bash
   git clone https://github.com/pgsty/oink.pgsty.com my-docs
   cd my-docs && hugo server
   ```
   > [!TIP]
   > 加 `-D` 连草稿一起预览。
{.steps}
  1. 安装 Hugo Extended,最低 0.160.1:
    brew install hugo
  2. 克隆文档站并预览:
    git clone https://github.com/pgsty/oink.pgsty.com my-docs
    cd my-docs && hugo server
    提示

    -D 连草稿一起预览。

页尾的自动内容

页面末尾的四块内容由主题按固定顺序生成,不必在正文里写:

位置 是什么 默认 怎么改
1 反馈:「这页有帮助吗」两个按钮 仓库与页面信息
2 最后修改:时间加最近一次提交的标题,链到 GitHub 有 Git 信息时开 仓库与页面信息
3 翻页器:上一页 / 下一页,顺序与侧栏树一致 docs / book / blog 开 导航与菜单
4 评论:giscus 配置完整且开启时 启用评论

标题旁边的操作菜单(复制 Markdown、编辑本页、查看历史、提 issue、打印)也是自动的,同样在仓库与页面信息里配置。

单页关闭其中某一块用 front matter:feedback: falseannotation: falsepager: falsecomments: false。键的含义见页面参数

验证

写完一页,运行一次严格构建:

hugo --printPathWarnings --panicOnWarning
  • 输出必须以 Total in … 结束,没有 ERROR、没有 WARN。属性行写了不允许的键、组件参数非法、ref 目标不存在,都在这一步失败并指出文件与行号;主题不做静默降级。
  • --printPathWarnings 报出两个页面指向同一输出路径的情况,多语言站或改过 permalinks 时较常出现。

在浏览器里确认三项:

  1. 侧栏里出现了这一页,位置符合 weight
  2. 右栏目录列出了你写的 ##,点击后 URL 里的锚点是英文;
  3. 中英两个版本的同名标题锚点一致(本站有 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

每个目录都要有 _index.md

栏目首页是目录里的 _index.md(中文为 _index.zh.md)。缺少它时 Hugo 仍会生成栏目,但没有标题、描述、图标与 weight:侧栏那一行显示目录名,排序不受控制。

content/docs/deploy/_index.zh.md
---
title: 部署上线
linkTitle: 部署
description: 把站点发布到 GitHub Pages、Cloudflare Pages 或自己的 Nginx。
weight: 50
icon: fa-solid fa-cloud-arrow-up
---

栏目 _index.md 另有一项专属能力:用 cascade 把共享设置一次下推给整棵子树,不必每页重复。

content/docs/reference/_index.zh.md
---
title: 参考
weight: 90
cascade:
  pager: false        # 这个子树里的页面都不显示上一页 / 下一页
  search_boost: 0.8   # 参考页在搜索里排后一点
---

排序:weight 用 10 的倍数

同一栏目里的页面按 weight 升序排列,weight 相同时才退回日期与 linkTitle 字母序。一律用 10 的倍数(10、20、30),此后往中间插页不必改动其它页。栏目自身的 weight 决定它在父级里的位置。

没写 weight 的页面视为 0,Hugo 把它们排在所有写了 weight 的页面之后,彼此按日期与标题排列。这个顺序会随内容改动漂移,因此每页都写上 weight

单文件还是页面包

没有自身资源的页面用单文件 slug.md;带图片、cast、示例文件的页面改成目录加 index.md,资源与它同放。两种形态在侧栏里没有区别,URL 也相同。详见编写页面

栏目首页显示子页列表还是卡片

_index.md 的正文之后,主题自动接上子页索引,两种样式:

hugo.yml:全站默认
params:
  ui:
    section_index: cards # list | cards

list 是主题默认,每个子页一行标题加描述;cards 是链接卡片网格,读取子页的 iconlinkTitledescription。本站用 cards,本栏目首页即是例子。单个栏目需要另一种样式时在它的 front matter 里覆盖:

content/docs/reference/_index.zh.md
section_index: list
cascade:
  section_index: list   # 连同后代栏目一起

两个页面级开关不受样式影响:simple_list: true 渲染紧凑的项目符号列表,no_list: true 不生成索引,用于正文自行手写导航的场合。

提示

卡片样式下 description 即卡片正文。描述控制在一句话、单行可显示。

侧栏图标

在页面或栏目的 front matter 里写一对 Font Awesome class:

content/docs/deploy/_index.zh.md
icon: fa-solid fa-cloud-arrow-up

图标密度是站点级策略,用于避免叶子页全部带图标:

hugo.yml
params:
  ui:
    sidebar_icon_policy: groups # all | groups | none
取值 效果
all 每个写了 icon 的条目都显示(未设置时的兼容默认值)
groups 只有根节点和有子页的节点显示图标,普通叶子页不显示
none 侧栏不显示任何条目图标

新站点建议显式写 groups:保留分组的语义标识,去掉叶子层的图标。本站使用这个设置,左侧只有六个栏目带图标。

展开与折叠

有子页的栏目在侧栏里带一个折叠箭头,读者的展开状态保存在本地。默认行为:当前页所在的那条路径展开,其余收起;博客类栏目默认展开。

content/docs/reference/_index.zh.md
sidebar_expanded: true   # 这个栏目始终默认展开

站点级的折叠、紧凑模式、初始展开层数、宽度与截断在布局与页面类型里配;键的完整定义见配置总览

从侧栏里藏起来

front matter 效果
toc_hide: true 页面不出现在侧栏树里(页面本身照常发布,链接照常可用)
hide_summary: true 页面不出现在栏目首页的子页索引里
sidebar_divider: true 这一项不再是链接,而是侧栏里的一条分组标题
manual_link: https://… 侧栏这一行指向别处;配 manual_link_titlemanual_link_target: _blank

toc_hidehide_summary 控制两个不同的入口,两处都不该出现时才同时设置。

外壳由 type 决定,不是路径

文档外壳(侧栏、目录、面包屑、翻页器)不取决于目录名,只取决于页面的 type 是否在 params.ui.shell_types 里:

hugo.yml:主题默认
params:
  ui:
    shell_types: [docs, book, blog, swagger]

文档因此可以放在任意路径,用 cascade 指定 type 即可。例如把一套手册放在 content/handbook/,栏目根的写法如下:

content/handbook/_index.zh.md
---
title: 运维手册
type: docs
sidebar_root_for: self      # 侧栏树的根即本栏目,不回退到 /docs
cascade:
  type: docs                # 整棵子树都用文档外壳
---
重要

文档目录不叫 docs 时,type: docs 之外还要写 sidebar_root_for: self。否则侧栏会按 params.ui.docs_section(默认 docs)去找根,读者在 /handbook/ 下却看到 /docs/ 的树。

多根侧栏

侧栏树默认以读者所在的顶层栏目为根,树上方一行标出当前的根。规模较大的子树可以自己成为一个根,例如带版本的 API 参考或一本独立的手册:

content/docs/api/v2/_index.zh.md
---
title: API 参考 v2
sidebar_root_for: self   # self | children
---
取值 语义
self 这个栏目的首页及其全部后代都以它为侧栏根
children 首页仍留在父级树里,只有后代以它为根

根节点上方的切换器是全站的:它列出所有顶层栏目,加上站内所有 sidebar_root_for: self 的栏目。只有一个入口时它退化成一个普通链接,两个及以上才是下拉菜单。顶层栏目不出现在切换器里时,在它的 _index.mdsidebar_root_menu: false

切换器下方,栏目首页仍是树里的第一个链接:切换器选择一棵树,根链接指向一篇文档。sidebar_root_link_self: false 让根那一行改为指向父级栏目。

验证

hugo --printPathWarnings --panicOnWarning

必须 Total in …,没有 ERROR / WARN。--printPathWarnings 报出两个页面指向同一输出路径的情况,改目录结构时较常出现。

在浏览器里逐项确认:

  1. 侧栏里的顺序与写下的 weight 一致,新栏目出现在预期位置;
  2. 栏目首页的子页索引齐全(缺项来自 hide_summary 或缺少 _index.zh.md);
  3. 面包屑与翻页器的顺序与侧栏一致,翻页器读的是同一棵树;
  4. 换语言之后树的形状相同(每个 _index.md 都要有 .zh.md 对等文件)。

侧栏条目超过 params.ui.sidebar_menu_truncate 时构建给出警告,并指出应调到多少。这个警告不可忽略:被截断的条目不会出现在侧栏里。

3 - 页面参数

front matter 全表:主题真正读取的每一个页面键,按侧栏、外壳、搜索、输出、页尾、Book、Landing、发布页分组。

本页是页面级参数的全表,只列 OINK 主题会读取的键。Hugo 自身的 front matter 字段(slugurlbuildsitemapexpiryDate 等)照常可用,语义见 Hugo 文档。站点级参数(hugo.yml 里的 params.*)见配置总览

表格说明

优先级从高到低:

  1. 页面自己的 front matter;
  2. 最近一层 cascade(多层 cascade 都设了同一个键时,离页面最近的那一层生效);
  3. hugo.yml 里的站点参数。

「默认」列标「站点值」的键,未写时回落到同名的站点参数。

页面键一律写在 front matter 顶层,键名是站点键去掉 ui. 前缀:站点的 params.ui.section_index 对应页面的 section_index。front matter 里不写 ui: 段,键一律在顶层。写在 ui: 段里的键不会被读取,也不会有任何提示——某个设置看着没生效时,先对照本页核一遍键名。

content/docs/wide-reference.zh.md
---
title: 兼容性矩阵
weight: 40
page_width: wide
footer_style: slim
image_zoom: true
section_index: list
---

放进 cascade 时键名不变,多包一层:

content/docs/reference/_index.zh.md
cascade:
  pager: false
  section_index: list

非法值不会中断构建。主题会发一条警告,指出键名、收到的值以及实际用了哪个回退值,然后按表里的默认值把这一页渲染出来——一个笔误只降级一个设置,而不是让 hugo server 下每个 URL 都返回 HTTP 500。它也不会因此混进线上:所有发布关卡都带 --panicOnWarning 构建,那条警告在真正要紧的地方仍然是硬失败。

少数几个键确实会中断构建,表里会写明。它们是那种「继续构建就会发布出错误内容」而不只是「发布出朴素内容」的情形:残缺的上游署名(半条声明读起来和完整的一模一样)、translation_noticerelease 事实、落地页的 sections,以及任何解析不到目标的引用。

基本

title , 字符串 , default
页面大标题、浏览器标题、搜索结果标题。每页必写
linkTitle , 字符串 , defaulttitle
侧栏、面包屑、翻页器、卡片里的短名
description , 字符串 , default
一句话摘要:栏目卡片、搜索摘要、meta description;博客页里渲染成正文上方的导语
weight , 整数 , default0
同级排序,用 10 的倍数;0(不写)排在所有写了 weight 的页面之后,见组织内容
draft , 布尔 , defaultfalse
草稿不进构建产物,hugo server -D 可预览,见编写页面
date , 日期 , default
博客日期、发布页排序依据;未来日期默认不构建
lastmod , 日期 , defaultGit 提交时间
页尾「最后修改」;站点启用 enableGitInfo 时不必手写
aliases , 字符串数组 , default
旧路径重定向到本页;用于页面迁移,不用于日常导航
type , 字符串 , default顶层目录名
决定模板与外壳:docs book blog swagger,见组织内容
layout , 字符串 , default
为单个页面指定布局:landingreleases
cascade , 映射 , default
把下面这些键下推给整棵子树

侧栏与导航

指南在组织内容

icon , Font Awesome class 对 , default
侧栏、栏目卡片与搜索结果的图标,例如 fa-solid fa-rocket
toc_hide , 布尔 , defaultfalse
不出现在侧栏树里,也不进翻页序列
hide_summary , 布尔 , defaultfalse
不出现在栏目首页的子页索引里
sidebar_divider , 布尔 , defaultfalse
这一行渲染成侧栏分组标题:不是链接,也不进翻页序列
sidebar_expanded , 布尔 , defaultblog 栏目 true,其余 false
这个栏目在侧栏里默认展开
sidebar_root_for , self / children , default
让这个栏目成为侧栏树的根;self 连同栏目首页,children 只管后代。其它取值告警并忽略
sidebar_root_link_self , 布尔 , defaulttrue
根那一行链接自身;false 改为链接父栏目。非布尔构建失败
sidebar_root_menu , 布尔 , defaulttrue
顶层栏目是否出现在根切换器里
toc_root , 布尔 , defaultfalse
侧栏根是站点首页时,把这个顶层栏目整个排除在树与翻页序列之外
manual_link , URL , default
侧栏与栏目索引里这一行指向别处
manual_link_relref , 内容引用 , default
同上,但用 relref 解析;目标不存在时构建失败
manual_link_title , 字符串 , defaulttitle
手动链接的悬停标题
manual_link_target , 字符串 , default
例如 _blank,主题自动补 noopener
no_list , 布尔 , defaultfalse
栏目首页不生成子页索引
simple_list , 布尔 , defaultfalse
子页索引渲染成紧凑的项目符号列表
section_index , list / cards , default站点值(list
子页索引的样式。非法值告警并回退
section_index_columns , 整数 , default2
卡片样式的列数
notoc , 布尔 , defaultfalse
不显示右栏页面目录
pager , 布尔 , defaultparams.ui.pager_types 决定
false 关闭本页的上一页 / 下一页。非布尔告警并忽略该覆盖
navbar_enabled , 布尔 , default站点值(true
这一页是否渲染顶栏
navbar_autohide , 布尔 , default站点值(false
顶栏在指针设备上自动隐藏
page_context_menu , 布尔 , default站点值(true
标题行的页面操作菜单(复制 Markdown、编辑本页、打印……)
page_context_menu.assistant_links , 布尔 , default站点值(false
ChatGPT / Claude 交接项,写成 page_context_menu: { assistant_links: false }。页面只能收窄站点策略,不能单独开启

页面外壳

站点级的默认值与效果说明在布局与页面类型

page_width , normal / wide / full , defaultnormal
正文栏宽度。非法值告警并回退
reading_width , slim / normal / wide , defaultnormal
Book 页的阅读行宽,只对 type: book 生效
footer_style , fat / slim / none , default站点值(fat
页脚形态。非法值告警并回退
body_class , 字符串 , default
追加到 <body> 上的 class,供站点自己的 CSS 使用
reading_time , 布尔 , default站点值
本页是否显示阅读时长;写 false 关掉
sidebar_enabled , 布尔 , defaulttrue
这一页是否显示左侧栏;写 false 关掉
scroll_spy , 布尔 , default站点值
目录的滚动跟随;写 true 打开
keyboard_nav , 布尔 , default站点值(true
单键键盘导航,见键盘导航。非布尔告警并回退
lastmod_commit , subject / hash / none , defaultsubject
「最后修改」后面怎么显示提交。非法值告警并回退
sidebar_expand_levelssidebar_menu_compactsidebar_menu_foldablesidebar_item_overflow , 同站点参数 , default站点值
侧栏行为也可以逐页覆盖;取值见配置总览

指南在全文检索

search_keywords , 字符串或字符串数组 , default
附加检索词,包含中英文与同义词
search_boost , 正数 , default1.0
排序乘数,最终得分为文本匹配分乘以该值。非数字、非有限、零或负值告警并回退 1.0
search_exclude , 布尔 , defaultfalse
不进本地索引

输出形态

指南在 Agent 支持.mdllms.txt)与打印支持

outputs , 字符串数组 , default站点 outputs
这一页生成哪些输出格式;写 [HTML] 时不再生成 .md
no_print , 布尔 , defaultfalse
不进入整章 / 整书的聚合打印输出

页尾:评论、反馈与出处

顺序固定为反馈 → 出处 → 翻页器 → 评论,见编写页面

comments , 布尔 , default站点 params.comments.enablefalse
本页是否显示 giscus 评论区,见启用评论
feedback , 布尔或映射 , default站点 params.ui.feedback(关)
映射形态支持 enablereasons。其它写法告警并回退
annotation , 布尔 , default站点 params.ui.annotation(开)
页尾的「最后修改 / 出处」区块。只接受布尔,其它写法告警并回退
translation_notice , 语言代码或 false , default站点 params.ui.translation_notice(关)
权威版本的语言代码,译文据此显示一条指回原文的说明;本页即以本语言原创时写 false

上游出处

页面改写自别处的材料时,用 upstream_link 声明来源,页尾出处行会给出作品、版权人、许可证与完整声明的链接。这一族键的解析顺序是站点参数 → data/upstreams 中由 upstream_source 指名的条目 → 本页 front matter,最具体的声明胜出。

upstream_link 只从 front matter 读取(cascade 有效,站点参数无效)——站点级的值会让每一页都声称同一个来源。没有 upstream_link 却写了任何一个同族键,构建失败。

upstream_link , URL , default
本页据以改写的材料地址。写空串退出 cascade 继承来的值
upstream_name , 字符串 , default
上游作品名,按上游自己的写法。设了 upstream_link 即必填
upstream_copyright , 字符串 , default
版权声明,保留上游原文。必填
upstream_license , SPDX 标识 , default
必须能在 data/licenses 中查到,否则构建失败。必填
upstream_notice , 站内路径或 URL , default
承载完整声明(许可证全文、免责声明、上游 NOTICE、快照版本)的页面。必填
upstream_ref , 字符串 , default
快照对应的 tag 或 commit,显示在作品名后的括号里
upstream_source , 字符串 , default站点参数
data/upstreams 中的条目名,用于集中声明多页共用的上游事实;条目不存在构建失败
upstream_modified , 布尔 , defaultfalse
页尾追加一条「本地已修改」;站点配了仓库信息时带「查看历史」链接。非布尔构建失败

四个必填键(upstream_nameupstream_copyrightupstream_licenseupstream_notice)缺一即构建失败:残缺的署名比明显的缺失更糟。主题自带一份 SPDX 表 data/licenses.yaml,站点用同名文件补充或覆盖条目。

图片缩放

image_zoom , 布尔 , default站点值(false
本页的图片是否可点击放大,见图片。非布尔告警并回退

博客与文章

指南在博客与文章

author , 字符串 , default
文章署名,支持行内 Markdown。页面写了 authors 时忽略它
authors , 字符串数组 , default
authors taxonomy 的 term,顺序即署名顺序,见作者与署名。需要在 taxonomies: 下声明 author: authors
series , 字符串数组 , default
series taxonomy 的 term。正文上方的横幅取第一个,见系列
series_weight , 整数 , default
在系列中的位置。带权重的成员按升序排在前,其余按日期升序跟在后
tags , 字符串数组 , default
标签,见分类体系
categories , 字符串数组 , default
分类,同上
images , 字符串数组 , default
第一项作为文章封面与分享卡片;写进栏目 _index.mdcascade 即为栏目级默认,images: [] 表示不要封面
featured_image , none / banner / wash , default站点值(none
本文正文里怎么渲染自己的题图。非法值告警并回退
blog_index , list / cards , default站点值(list
写在博客根目录上,决定该栏目列表页的形态。非法值告警并回退
share , 字符串数组或 false , default站点 params.ui.share(空)
页尾分享目标,整体替换继承来的列表;false 让本页退出,见分享。未知目标告警并丢弃
summary , 字符串 , default
标签 / 分类页上文章行的摘要回退来源,description 优先

Book

指南在书籍出版。整本书通过栏目 cascadetype: book

book_number , 字符串 , default
章节编号,显示在页面标题与侧栏条目前面
book_status , draft , default
标记草稿章节:侧栏与目录里带草稿标记,索引里默认不列
sidebar_headings , false / true / 2–4 的整数 , default站点值(false
在侧栏当前条目下展开 h2–h4 分支。超出范围告警并回退
book_draft_banner , 布尔 , default站点值(false
草稿章节正文开头加一条横幅。非布尔告警并回退

Landing

指南在首页与落地页。任意页面写 layout: landing 就用落地页外壳。

landing , 字符串 , default
数据取自 data/landing/<key>/<语言>.yaml
sections , 数组 , default
在 front matter 里内联分区定义,优先于 landing。不是数组时构建失败

发布页

指南在发布与下载页。栏目写 layout: releases 后忽略 weight,按发布日期与 SemVer 倒序排列。

release , 字符串或映射 , default
发布事实。字符串形态是 https://github.com/<owner>/<repo>/releases/tag/<tag>;映射形态的键是 product version repo tag date prev checksumsversionrepo 必填,未知键或类型不符构建失败
release_products , 字符串或字符串数组 , default
发布列表只保留这些产品。非法过滤条件构建失败
release_group_by_product , 布尔 , defaultfalse
按产品分组;开启后每一篇被选中的文章都必须写 release.product

4 - 博客与文章

开一个博客栏目:目录约定、文章的 front matter、封面图、按年份分组的列表页与 RSS。

博客文章与文档页的正文写法相同,区别在外壳:文章带日期、作者、标签与封面图,列表按年份倒序排列,栏目带 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

栏目根把类型下推给整棵子树,并设定该栏目共用的行为:

content/blog/_index.zh.md
---
title: 博客
description: OINK 工程实践与发布注记
type: blog
icon: fa-solid fa-blog
sidebar_root_for: self      # 博客有自己的侧栏树
cascade:
  type: blog
  feedback: false           # 文章不问「这页有帮助吗」
  comments: true            # 但开评论
---

params.ui.blog_section(默认 blog)指明博客根的位置。目录另起名字时改这个参数,或按上面的写法用 sidebar_root_for: self

侧栏里博客栏目默认展开,条目按日期倒序;给某篇文章写上 weight 会把它固定在最前。

一篇文章的 front matter

content/blog/release/0.4.0.zh.md
---
title: Oink 0.4.0 — 面向完整发布流程的场景组件体系
linkTitle: Oink v0.4.0        # 侧栏与翻页器里的短名
date: 2026-08-14              # 发布日期,决定排序与分组
lastmod: 2026-08-14
description: >-
  Oink 0.4.0 交付连续阅读与发布界面、可复用 Landing 页面、
  带稳定引用的 Book 出版能力,以及键盘优先的站点外壳。
author: OINK 维护者
categories: [发布]
tags: [Oink, Release]
---

与文档页不同的几点:

  • date 必填。它决定文章在列表里的位置、年份分组与 RSS 时间。写在未来的日期默认不构建,hugo server -F 可以预览。
  • description 渲染成正文上方的导语,不只是搜索摘要,因此写成给读者阅读的一句话。
  • author 支持行内 Markdown,可以写成 [Vonng](https://vonng.com)。需要多位作者、头像或作者主页时,改用下面的 authors taxonomy;两者互不干扰,没写 authors 的文章照旧渲染 author
  • 日期显示格式由 params.time_format_blog 决定,可以按语言分别设置(本站英文是 Monday, January 02, 2006,中文是 2006年1月2日)。

双语文章成对存放,两种语言的 dateauthorweightaliases 保持一致;标题、描述、标签要翻译,提交 ID、版本号、命令和 URL 不翻译。

列表页与标签页的每一行左侧有一张缩略图,按以下顺序解析,第一个命中的生效:

  1. 文章 front matter 的 images,取第一项;
  2. 页面包里文件名含 featured 的图片资源(会被裁切成缩略图,图片资源自己的 byline 会作为图注);
  3. 从祖先栏目 cascade 继承来的 images,就近生效。

栏目级默认封面用 Hugo 原生的 cascade 覆盖整棵子树,本站两个子栏目各设一张:

content/blog/release/_index.zh.md
cascade:
  images: [/images/releasenote.webp]

某一篇不要封面时,在它的 front matter 写 images: [];整个子栏目都不要,就把 images: [] 写进那一层的 cascade。站点级的 params.images 不受影响 —— 它只做分享卡片,不会渲染成列表缩略图。

渲染到文章正文里

默认情况下,解析出来的这张图只出现在列表行与社交卡片里,文章本身什么都不显示——手写一个题图,迟早会和卡片对不上。params.ui.featured_image 让主题用同一个解析结果把它渲染出来:

模式 文章里显示什么
none 什么都不显示。主题默认值,所以今天不渲染题图的站点,升级后渲染出的字节完全一样
banner 标题上方一张固定 16:9 的图,连着读一串文章时节奏统一
wash 图铺在文章头部背后,只留十分之一的不透明度,在正文开始之前渐隐为无——文章从自己的主题里取到一点颜色,却不消耗任何对比度
hugo.yml
params:
  ui:
    featured_image: banner

页面键是 featured_image,所以某个子栏目的 cascade 可以只为那棵树打开它,单篇文章也可以退出。没有题图的文章在两种模式下都不渲染任何东西——正因如此,一个题图有一搭没一搭的栏目也可以整体打开这个开关。两种模式都不引入脚本,也不增加打包成员。

content/blog/release/_index.md
cascade:
  featured_image: wash

列表页与分页

栏目 _index.md 的正文之后,主题自动接上文章列表:按年份分组(「撰写于 2026」),年份倒序,每条显示标题、日期、所属子栏目、标签、缩略图与正文前 250 字的摘要。

分页用 Hugo 原生的分页器,默认每页 10 篇,在 hugo.yml 里调整:

hugo.yml
pagination:
  pagerSize: 20

取值与其余分页选项见 Hugo 文档

卡片形态

params.ui.blog_index: cards 把同一份列表渲染成内容卡片网格而不是行列表:文章题图的 16:9 裁切在上,标题、日期与子栏目行居中,下面三行摘要。

hugo.yml
params:
  ui:
    blog_index: cards
    blog_index_columns: 3

这个选择纯粹是呈现层面的——按年分组、分页与 manual_link 的行为完全一致,行列表那一路的输出一个字节都没变。列数只在 xl 断点以上生效;md 到 xl 之间恒为两列,md 以下一列。博客根目录的 front matter blog_index 或它的 cascade 可以按栏目设置。Term 页与 taxonomy 页保持行列表,读者侧没有在两种形态之间切换的开关。

卡片题图只要资源可处理就走 Hugo 的 .Fill,一屏卡片不会为此下载一堆原图。

RSS

哪些页面产出 Feed 由 outputs 决定。给 section 加上 RSS,每个栏目就有自己的 Feed:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]

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 更彻底:

hugo.yml
disableKinds: [RSS]

组件在 Feed 里退化成静态形态:折叠块展开、交互控件去掉。四态输出的规则对博客与文档一致。

分类与标签

tagscategories 是 Hugo 的分类体系,主题把它们渲染成文章头部的 chip、右栏的标签云和顶栏的筛选菜单。启用、双语标签与按内容类型开关见分类体系

发布注记

带版本号的发布公告写成普通文章,惯例放在 blog/release/ 下,linkTitle 带版本号(Oink v0.4.0)。需要发布卡片、资产表与校验和的下载页见发布与下载页

文章里用组件

提示块、标签页、代码块、图片、表格的用法与文档页相同,语法见组件总览。文章正文的标题同样写显式英文 {#id}

文章末尾的反馈 / 最后修改 / 翻页器 / 评论四块与文档页一致,见编写页面。博客通常关闭反馈、保留评论。

作者与署名

声明这个 taxonomy 就是全部开关,主题不为此增加任何参数:

hugo.yml
taxonomies:
  category: categories
  tag: tags
  author: authors

文章按顺序写出作者:

authors: [vonng, ada-example]

文章头部就按这个顺序渲染头像与带链接的名字——front matter 里的序列既是集合也是顺序——列表行渲染名字,博客 feed 为每篇文章的每位作者发一条 <dc:creator>,与站点级的 managingEditor 并存。名字之间用 CSS 的 gap 分隔而不是连接词,因为「和」是个逐语言的决定,而这里有 32 种语言。

作者主页就是 term 页本身,所以不存在另一份 data/authors 和它打架:

content/authors/vonng/_index.md
---
title: Vonng
description: OINK 与 Pigsty 的维护者。
images: [portrait.webp]
---

正文是长介绍,渲染在主页上名字下方。

显示名取的是 term 页的链接标题——写了 linkTitle 就用它,否则用 title——所以主页可以挂全名、署名处用短昵称。description 是一句话介绍,正文是长介绍,头像则是题图解析器为这一页选中的那张——images: 与页面包里的肖像文件,走的是文章题图那套同样的规则。双语主页就是旁边一个 _index.zh.md。文章写了、但没人给它建主页的名字照样出署名:链接标题、一个首字母,以及指向归档页的链接。

0.4 的 author: 字符串在没有 authors 的地方原样保留,两种写法互不告警。

系列

系列是一条穿过若干篇各自独立成文的文章的阅读路径。编号、交叉引用与聚合输出属于书籍,这里是更轻的那个东西。声明 taxonomy 同样就是全部开关:

hugo.yml
taxonomies:
  series: series

文章写出系列名,也可以给自己定个位置:

series: [shell-internals]
series_weight: 20

它的正文上方就会出现一条横幅,写明系列名、自己是第几篇、下一篇是哪篇,以及折在 <details> 里的完整列表——不用 JavaScript,也不增加打包成员。term 页 content/series/<name>/_index.md 是系列的引言,旁边放一个 _index.zh.md 就成双语。

阅读顺序由主题自己算,因为 term 页给不出这个顺序:Hugo 的 taxonomy weight 既到不了 Page.Weight,也进不了 GroupByParam。带权重的成员按 series_weight 升序排在前,其余按日期升序跟在后面,同序时用 Path 决胜。横幅与 term 页读同一个解析结果,所以它们不可能对「第二篇是哪篇」有分歧——这也意味着系列 term 页是由旧到新排列的,和其它所有 term 页相反。这正是这个功能本身。

一篇文章属于多个系列时只显示一条横幅,取它写在最前面的那个系列。只有一篇的系列不显示横幅。

authorsseries 都不出现在文章的通用 taxonomy 标签行里,因为它们各自有专门的呈现面。想把某一个放回去,就在 params.taxonomy.page_header 里写上它的名字。

分享

params.ui.share 在页尾最前面放一条分享栏。它默认为空,所以在站点写出目标之前什么都不渲染;写出来的顺序就是渲染顺序:

hugo.yml
params:
  ui:
    share: [x, bluesky, mastodon, reddit, hackernews, email, copy]

可选的目标有十六个:xblueskymastodonfacebooklinkedinreddithackernewstelegramwhatsapplinepinterestweibochatgptclaudeemailcopy。未知的名字告警并丢弃。Discord 是故意没有的:它根本没有公开的 share-intent URL,与其让主题去猜一个私有 scheme,不如用 copy 顶上。

页面键是 share,所以 cascade 可以把这条栏限定在一棵树里,页面自己的列表会整体替换继承来的那份,share: false 则让单页退出:

content/blog/_index.md
cascade:
  share: [x, bluesky, email, copy]

只有普通页面渲染分享栏——列表页、term 页与首页没有「唯一被分享的那个东西」——打印、Markdown 与 RSS 一概不带。

它不做什么,才是它能出现在这个主题里的原因。没有分享计数、没有平台 SDK、没有 iframe、没有第三方脚本或样式表——而那三样正是这类组件通常的形态:每一页都向一家读者从未选择过的公司发一次请求。每个目标都是一个纯粹的 <a href> intent 链接,只带这一页自己的 permalink 与标题,不挂任何投放参数,另加一个本地复制按钮。站点构建时不取任何东西,页面加载时也不取;一次分享唯一可能引发的请求,就是读者点下去之后自己发起的那次跳转。把十六个目标全开的构建,不加 --third-party 也能通过 bin/check-output-security.py

chatgptclaude 是把同一个构建期 permalink 交给助手,附一句「请读这一页」。它们不是页面操作菜单里的「在 ChatGPT 中打开」/「在 Claude 中打开」——那两条由运行时在激活时改写成浏览器里的实时 URL,因此留在 page_context_menu.assistant_links 后面。

复制按钮就是内置的 copy_link 动作,也就是说不管有没有配分享栏,命令面板在每个站点的每一页上都带着它。

验证

hugo --printPathWarnings --panicOnWarning

必须 Total in …,没有 ERROR / WARN。随后确认:

  1. 文章出现在 /zh/blog/ 的正确年份分组里,日期显示为中文格式;
  2. public/zh/blog/index.xml 存在,里面有这篇文章,链接是完整的绝对地址;
  3. 缩略图出现在列表里(缺失说明三条封面来源都没命中);
  4. 标签 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-15.3A-2 均合法),不是渲染时计算的序号。重排目录因此不会让已经印出去的编号漂移。

书首页与章首页

书根声明类型、级联给后代,并显式请求 print 输出。这项聚合输出构建代价高,主题不替消费站开启:

content/handbook/_index.md
---
title: PostgreSQL 运维手册
type: book
book_number: B
cascade:
  type: book
outputs: [HTML, print, markdown]
---

分区书对应 Hugo 的 section 输出类型,书位于站点根时才用 home

hugo.yml
outputs:
  section: [HTML, print, markdown]
params:
  ui:
    sidebar_headings: 3     # 当前章节行下投射 h2–h3 标题树
    book_draft_banner: true # 草稿章节页首多一条本地化提示

章首页只需要编号与顺序:

content/handbook/ch02/_index.md
---
title: 复制与故障切换
book_number: 2
book_status: draft
weight: 20
---

book_number 显示在页面标题、侧栏与生成目录里。book_status: draft 是可见的编辑状态标签,不改变 Hugo 的发布状态:草稿章节照常构建、照常发布。

sidebar_headings 接受 falsetrue(只到 h2)或 2–4 的最大层级。要被引用的标题一律写显式 ID,如 ## 同步复制 {#sync-replication}:自动生成的 slug 适合导航,不适合作为长期引用目标。

配置键的完整定义在配置总览,页面参数在页面参数

编号:原生形态

四种编号对象各有一种原生形态:一个 Markdown 块,紧跟其后一行属性行。属性行里 num= 是编号,#id 是锚点,caption= 是纯文本题注。

图片块后面跟属性行。#id 省略时默认是 fig-<num>

源码
![OINK 发布注记页面](/images/releasenote.webp)
{#book-release-note num="2-1" caption="发布注记页面同时是发布事实的唯一来源。" width=600 height=300}
OINK 发布注记页面
图 2-1 发布注记页面同时是发布事实的唯一来源。

原生图形态要求站点设置 markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false,否则属性行会挂到段落上被忽略。替代文字取自 Markdown 图片本身,不会被题注替代。

管道表后面跟属性行,默认 ID 是 tbl-<num>

源码
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
| --- | --- | --- | --- |
| Read Committed | 不可能 | 可能 | 可能 |
| Repeatable Read | 不可能 | 不可能 | 可能 |
| Serializable | 不可能 | 不可能 | 不可能 |
{#tbl-2-1 num="2-1" caption="PostgreSQL 各隔离级别下的异常现象。"}
隔离级别 脏读 不可重复读 幻读
Read Committed 不可能 可能 可能
Repeatable Read 不可能 不可能 可能
Serializable 不可能 不可能 不可能
表 2-1 PostgreSQL 各隔离级别下的异常现象。

$$ 块后面跟属性行,默认 ID 是 eq-<num>。编号与题注排在公式右侧的同一行里,不换行;题注写长了会挤压公式那一列,公式随之变成需要横向滚动的区域。公式的题注要短。

源码
$$
A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}}
$$
{#eq-2-1 num="2-1" caption="可用性与平均故障间隔、平均恢复时间的关系。"}
A=MTBFMTBF+MTTR A = \frac{\mathrm{MTBF}}{\mathrm{MTBF} + \mathrm{MTTR}}
公式 2-1 可用性与平均故障间隔、平均恢复时间的关系。

原生形态依赖站点开启 Goldmark passthrough。未开启时用下面的 eq shortcode,它走本地服务端 KaTeX。

代码围栏加 num=caption= 即编号例,默认 ID 是 eg-<num>。围栏里写的 #id 命名外层 <figure>,即引用目标,不是代码块本身。例的题注必填:只写 num 或只写 caption 都会让构建失败。编号例渲染成一个整体:题注是框的表头,正文在框内;正文恰好是一个代码块时贴着框排,不再另画一圈边框。

源码
```sql {num="2-1" caption="按天统计主库写入量。" #eg-2-1}
SELECT date_trunc('day', ts) AS day, count(*)
FROM pg_stat_statements_history
GROUP BY 1 ORDER BY 1 DESC LIMIT 7;
```
示例 2-1 按天统计主库写入量。
SELECT date_trunc('day', ts) AS day, count(*)
FROM pg_stat_statements_history
GROUP BY 1 ORDER BY 1 DESC LIMIT 7;

编号:shortcode 形态

四个 shortcode fig tbl eq eg 渲染出与原生形态一致的 <figure>,注册到同一个目标表,按源码位置排序。仅在原生形态做不到时使用:图片要外链跳转、表格要在一个编号下放多张表、站点未开 passthrough、例子体是多个围栏加说明文字。

figsrc=(也接受内部 Markdown 内容,二者互斥),并额外支持 link alt width height class 与迁移用的 title 别名:

源码
{{< fig num="2-2" src="/images/docsy.webp" alt="Docsy 主题的默认外壳"
    caption="OINK 的上游:Docsy 的内容模型仍在下面。" width="600" height="300" />}}
Docsy 主题的默认外壳
图 2-2 OINK 的上游:Docsy 的内容模型仍在下面。

tbl 把标签、表格、题注与锚点包进一个语义 figure:

源码
{{< tbl num="2-2" caption="四种输出下编号组件的形态。" >}}
| 输出 | 标签 | 锚点 |
| --- | --- | --- |
| HTML | 可见 | 稳定 |
| 打印 | 可见 | 稳定 |
{{< /tbl >}}
输出 标签 锚点
HTML 可见 稳定
打印 可见 稳定
表 2-2 四种输出下编号组件的形态。

eq 的内容交给本地服务端 KaTeX,因此不依赖 passthrough:

源码
{{< eq num="2-2" caption="连接池饱和度。" >}}U = \frac{\lambda}{\mu \cdot c}{{< /eq >}}
U=λμcU = \frac{\lambda}{\mu \cdot c}
公式 2-2 连接池饱和度。

不带参数的 {{< eq >}} 是无编号的块级公式兜底:不注册目标,不能被 xref 引用,也不出现在公式索引里。

eg 是包装型 shortcode,正文按页面的 Markdown 策略渲染,通常装一个或多个围栏:

源码
{{< eg num="2-2" caption="用 pg_basebackup 拉起一个新从库。" >}}
```bash
pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R
```
{{< /eg >}}
示例 2-2 用 pg_basebackup 拉起一个新从库。
pg_basebackup -h primary -U replicator -D /pg/data -Fp -Xs -P -R

同一页里 ID 必须唯一,同一类里一个编号也只能对应一个 ID。重复时构建失败,报错指出先占用它的那一处在哪行。

shortcode 正文里不能写脚注

Hugo 把 shortcode 的正文当作独立的 Goldmark 文档渲染,脚注是页面级的。tblegfigcardtabfieldinclude 的正文里出现 [^label] 一律构建失败,报错给出文件、行号与标签。定义写在页面上时该引用会原样印出 [^label],定义写在正文里则生成第二份脚注列表、fn:N 与页面自身的 ID 冲突——两种结果都不该发布。

需要脚注的表格或代码块改用原生形态:表格、图片、围栏加 {num=… caption=…},内容留在页面文档里,脚注照常编号、跳转与回链。渲染出来的图表与 shortcode 形态一致,所以这通常是一行改动。代码里形似脚注的文本(列表里的 [^0-9] 字符类、行内代码)不受影响。

交叉引用

引用同页目标可以用普通 Markdown 链接:表 2-1 指向上面那张隔离级别表。代价是标签与编号手写,改编号时需要自己检索。

xref 把标签、编号与锚点合成一处,并支持跨页与跨语言:

源码
参见 {{< xref fig="2-2" />}} 与 {{< xref eg="2-1" />}};
显式锚点:{{< xref fig="2-1" anchor="book-release-note" />}}。

参见 图 2-2示例 2-1; 显式锚点:图 2-1

规则:

  • 最多一个类型键(fig tbl eq eg)。类型提供本地化标签(图 / 表 / 公式 / 示例)并推导出默认锚点 <kind>-<num>
  • anchor= 覆盖推导出的锚点,用于目标写了显式 #id 的情况。
  • page= 跨页引用,走 Hugo 当前语言的页面查找,源码里不必硬编码 /zh/ 前缀。
  • 不给类型时必须同时给 anchor= 和内部链接文字:{{< xref page="../ch01/install" anchor="sync-replication" >}}同步复制{{< /xref >}}
  • 引用可以出现在目标之前,渲染时不读注册表,因此前向引用合法。

跨页的普通 Markdown 链接在整本打印里仍然是站点 URL。需要在聚合文档里也能跳转的引用写成 xref

索引:目录与图表清单

五个索引 shortcode 遍历同一棵书树,触发后代内容并聚合注册结果。它们通常放在书首页(_index.md)或专门的「插图目录」页上。

content/handbook/_index.md
{{< book-toc depth=3 >}}

## 插图目录 {#lof}
{{< book-figures >}}

## 表格目录 {#lot}
{{< book-tables >}}

## 公式索引 {#loe}
{{< book-equations >}}

## 示例索引 {#lox}
{{< book-examples >}}

这五个 shortcode 在本页只给源码。它们从当前页所在的导航根向下遍历,放在一棵普通文档树里会把整棵 docs 树当作书列出。真实效果见《使用 OINK 创作优美的内容》,源码位于 content/book/_index.md

  • book-tocdepth 取 1–3:1 列章,2 加入嵌套分区,3 再投射每页的标题树;drafts=false 只把 book_status: draft 的行从这份生成列表里滤掉,不影响页面发布。
  • book-figures / book-tables / book-equations / book-examples 不接受任何参数,各列一类,条目形如「图 2-1 — 题注」并链到稳定 ID。
  • 整本打印时,这些链接全部变成文档内片段。

顺序阅读与草稿

翻页器默认对 docsbookblog 三种类型开启,顺序是侧栏那棵树的前序遍历:分区首页在前,子页按 weight。关闭整类改 params.ui.pager_types,关闭单页写 pager: false

hugo.yml
params:
  ui:
    pager_types: [docs, book]

toc_hidemanual_link 纯链接占位、sidebar_divider 分隔行都不会成为翻页目的地。

草稿章节除了侧栏上的「草稿」标签,还可以开启页首横幅:

hugo.yml
params:
  ui:
    book_draft_banner: true

横幅只在 type: bookbook_status: draft 的页面出现,文案来自本地化键 book_draft_notice

打印整本

书根有了 print 输出后,按可见的阅读顺序生成封面、本地目录、根页面正文与每个后代章节,全部装在一个 HTML 文档里。no_print: true 的页面、纯链接节点、分隔行与隐藏占位不会成为章节。

聚合文档里,编号组件的 ID 逐字节保留。页面内的 Markdown 标题 ID 会加上来源页面前缀,避免多章共有 summary 这类锚点时冲突,生成的标题链接同步改写。产物是面向打印的 HTML,PDF 与 EPUB 由站点自行处理。

具体开关与整章打印见打印支持

迁移既有书稿

已有的中文书稿通常用站点自己的 figure shortcode、加粗的假题注、指向 #fig_* 的裸链接来表示图表编号。主题仓库带一个迁移脚本,把这些旧形态改写成 figtblxref,并保留原有的公开锚点。站点先固定到一个包含 Book 组件的已发布 OINK 版本,再迁移内容。

干跑:只看 diff 与报告,不改文件
python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme \
  --root /path/to/your-book \
  --report /tmp/book-migrate.json > /tmp/book-migrate.diff

四个配方对应三份真实书稿的旧约定(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_scannedfiles_changedcountsskippedidempotent 五项。脚本只改写能唯一确定的目标:无法确定编号、题注不唯一、标记形态不认识的地方原样保留,逐条记进 skipped 供人工处理。旧题注里的粗体、行内代码与公式会降级为纯文本,因为 Book 的题注契约是纯文本。

审阅 diff 之后在专用分支上应用,再运行第二遍确认幂等:

应用并验证幂等
python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme --root /path/to/your-book --write \
  --report /tmp/book-migrate-written.json

python3 ~/pgsty/oink/bin/migrations/book_figures.py \
  --profile tpme --root /path/to/your-book --no-diff \
  --report /tmp/book-migrate-second.json

第二份报告应当是 files_changed: 0counts 为空、idempotent: true;脚本以退出码 0 表示幂等。

配方只识别这三份书稿里实际观测到的旧形态;书稿的旧约定不在这四个配方之内时,脚本不适用,需要按编号:原生形态手工改写。主题仓库的 bin/check-book-migrations.py 用干跑与幂等两项检查覆盖这四个配方。

验证

  1. 构建零告警:hugo --printPathWarnings --panicOnWarning。编号写错、ID 重复、题注缺失都在这一步失败。
  2. 页面上应看到「图 2-1」这样的本地化标签、可点的 xref 链接,以及点击后正确跳转的锚点。
  3. 对比侧栏、翻页器、book-toc 与整本打印四处的章节顺序是否一致。
  4. 检查 Markdown 输出:curl -s http://localhost:1313/zh/handbook/ch02/index.md。shortcode 形态应退化成 **图 2-2.** 题注 加原始正文,原生形态原样保留源码块与属性行。
  5. 从主题仓库对构建产物跑一遍锚点检查:
python3 ~/pgsty/oink/bin/check-book.py --site-public public

它校验每个引用的目标锚点存在、类型与编号匹配、页内 ID 唯一,以及编号图片有与题注相称的替代文字。

Book shortcode 参数

num , 字符串 , default
必填(eq 无参形态除外)。匹配 [0-9A-Za-z.-]+,要加引号
id , 字符串 , defaultfig-<num> / tbl-<num> / eq-<num> / eg-<num>
匹配 [A-Za-z][A-Za-z0-9_.:-]*,逐字节保留
caption , 纯文本 , default
eg 必填;fig tbl eq 可选。不是 Markdown
class , class token , default
追加到 <figure>;需要 num
src , 图片路径 , default
fig。与内部内容互斥,走共享图片解析顺序
link alt width height , , default
fig。宽高是正整数
title , 纯文本 , default
figcaption 的迁移别名,二者互斥

xref

fig tbl eq eg , 编号字符串 , default
至多一个。提供本地化标签并推导锚点
anchor , ID , default由类型与编号推导
无类型时必填,且必须有内部链接文字
page , 页面引用 , default当前页
走当前语言的页面查找,找不到则构建失败

book-toc

depth , 整数 1–3 , default2
1 章 / 2 含嵌套分区 / 3 含标题树
drafts , 布尔 , defaulttrue
false 时从生成列表里滤掉草稿章节

book-figuresbook-tablesbook-equationsbook-examples 不接受任何参数。

限制与常见问题

  • 没有自动编号。章节号、图号、表号都手写;改编号是一次有意的编辑,不是构建的副作用。
  • 属性行必须紧贴块,中间不能有空行。被 Prettier 之类工具移动过的属性行静默失效,图退化成普通图片。
  • book_kindbook_part 是契约认可的元数据键,当前主题模板不渲染它们;有视觉效果的是 book_numberbook_status
  • 索引 shortcode 会触发后代内容渲染,在超大树上明显拉长构建时间。整本 print 需要显式开启也是同一原因。
  • shortcode 的正文里不能出现脚注引用,构建失败并指出改用原生形态;见上文编号:shortcode 形态
  • 主题只到打印 HTML 为止:分页、字体嵌入、索引编制、PDF / EPUB 打包都在契约之外。
  • 组织内容 — 目录树怎么变成侧栏与阅读顺序
  • 图片 — 图注、尺寸、缩放与图片处理
  • 表格 — 表格属性行与全宽表
  • 公式 — KaTeX 与 passthrough 配置
  • 打印支持 — 整章与整本打印

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:

content/blog/release/0.4.0.zh.md
release_url: https://github.com/pgsty/oink/releases/tag/v0.4.0

owner、项目名与标签从 URL 里解析出来,日期用页面自己的 date。不是精确 标签形式的 GitHub 发布 URL 会警告并跳过发布区块——--panicOnWarning 构建 随之失败。0.5 的 release 映射(product / version / repo / tag / date / prev / checksums)及其字符串简写已移除;仍携带它的页面会收到指名 release_url 的警告。

在需要摘要的位置放一个不带参数的 shortcode,调用里不接受任何事实:

源码
{{< release-card >}}

卡片带着仅凭 URL 就能推导的四个链接——发布页、两种源码归档、仓库——全部本地推导。校验和文件放在正文下方的资产表里,版本对比在 GitHub 上看。

发布索引页

一个分区可以改用发布索引布局。它列出小节里的每一个常规页面,从新到旧 ——按页面日期排序,同一天内以标签里的版本号决胜(SemVer 优先级,非 SemVer 标签用确定的字典序兜底):

content/blog/release/_index.zh.md
---
title: 版本发布
layout: releases
---

release_url 可解析的条目读作「项目名 + 标签」——如 oink v0.4.0——下一行 是页面描述;没有它的页面保留自己的标题,版本之间夹一篇普通短文是合法条目, 不是警告。0.5 的 release_products 过滤与 release_group_by_product 分组 已移除;写了会警告。

本站的版本发布目前用普通博客列表。需要严格时间序时改用 layout: releases

校验和资产

checksums 围栏是校验和表的原生形态,围栏里写 sha*sum 命令的原样输出:

源码
```checksums
1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4  oink-0.4.0-linux-amd64.tar.gz
7b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250 *oink-0.4.0-darwin-arm64.tar.gz
```
下载资产
文件校验和
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 front matter 的页面
```checksums {base="https://repo.pigsty.io/oink/v0.4.0/" algo="sha256"}
1e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4  oink-0.4.0-linux-amd64.tar.gz
```

release-assets 是同一个解析器与渲染器的 shortcode 形态。它多一个围栏没有的 src=,可以把校验和文件本身提交为页面资源或全局资产(src 与围栏内容互斥);group="auto" 按平台与架构分组:

源码
{{< release-assets group="auto" >}}
5a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6  oink-0.4.0-1.el9.x86_64.rpm
c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8  oink-0.4.0-1.el9.aarch64.rpm
{{< /release-assets >}}

.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

data/download/prd5.yaml
version: 0.4.0
repo: pgsty/oink
published: true
channels:
  - id: script
    kind: rolling
    title: Install script
    title_zh: 安装脚本
    icon: fa-solid fa-bolt
    note: The rolling channel deliberately contains no version interpolation.
    note_zh: 滚动渠道刻意不插入版本号。
    steps:
      - title: Install
        title_zh: 安装
        code: curl -fsSL https://repo.example.org/oink/install | bash
        lang: bash
  - id: source
    kind: pinned
    title: Source archive
    title_zh: 源码归档
    icon: fa-solid fa-code-branch
    url: https://github.com/pgsty/oink/archive/refs/tags/${tag}.tar.gz
    steps:
      - title: Clone the tag
        title_zh: 克隆标签
        code: git clone --branch ${tag} https://github.com/pgsty/oink.git
        lang: bash
  - id: assets
    kind: pinned
    title: Release assets
    title_zh: 发布资产
    icon: fa-solid fa-box-open
    checksums: |
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  oink-0.4.0.tar.gz

记录级字段只有 version repo tag published channels 五个,多写一个键即构建失败。version 也可以不写在这里,改由站点的 params.version 提供。

version , 字符串 , default站点 params.version
两处都没有则构建失败
repo , owner/name , default
固定版本渠道有链接或资产时必填
tag , 字符串 , defaultv{version}
只允许 URL 安全字符
published , 布尔 , defaulttrue
false 表示不可变发布还不存在
channels , 数组 , default
非空

每个渠道:

id , ^[a-z][a-z0-9-]*$ , default
记录内唯一,用作锚点
kind , rolling | pinned , default
决定能不能插值版本事实
title , 本地化字符串 , default
必须能解析出非空值
note , 本地化字符串 , default
渠道下方的一行说明
icon , Font Awesome class 对 , default
例如 fa-solid fa-bolt
url , http(s) 或站内路径 , default
pinned 可插值
steps[] , title / code / lang , defaultlang: text
代码步骤走 OINK 的增强代码渲染器
checksums , sha*sum 文本 , default
pinned;与 checksums_src 互斥
checksums_src , 资产路径 , default
把校验和文件当作 Hugo 资产读入

两条规则:

  • 本地化按后缀解析:<字段>_<精确语言><字段>_<主语言><字段>。中文站解析 title_zh_cntitle_zhtitle。不接受 camelCase 别名。
  • 只有固定版本渠道的 urlsteps[].code 能插值 ${version}${tag}。滚动渠道拒绝插值,避免稳定版安装命令被绑定到某个版本。标题与说明不插值。

渲染下载区块

download 接受恰好一个位置参数,即数据键:

源码
{{< download "prd5" >}}

安装脚本

滚动渠道刻意不插入版本号。

安装
curl -fsSL https://repo.example.org/oink/install | bash

源码归档

源码归档
克隆标签
git clone --branch v0.4.0 https://github.com/pgsty/oink.git

发布资产

下载资产
文件校验和
oink-0.4.0.tar.gz SHA-256 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

HTML 渲染一排锚点 chip 加各渠道分区,代码步骤复用增强代码块与按需加载的复制运行时,校验和渠道复用上面那张资产表。打印静态展开同样的内容,Markdown 输出标题、源码围栏与完整哈希,RSS 不输出这个组件。

标签未打、资产未上传时,把记录标为未发布:

data/download/<key>.yaml
published: false

滚动渠道照常可用。固定版本渠道变成不可点击的「待发布」状态,省略固定版本命令,禁用资产链接与复制控件。标签与资产可解析之后再翻转这个开关,不要先在正文里写入推测出来的链接。

同一份记录也能被 Landing 页面的 download 分区消费,不需要第二套版本模型,见首页与落地页

与博客发布注记的关系

两者分工:

  • 博客里的发布注记(本站在 content/blog/release/)是叙事:这一版改了什么、怎么升级、有什么破坏性变更。它的 front matter 里带 release_url,页首可以放一张 release-card。写法见博客与文章
  • 下载数据是操作:选哪个渠道、运行哪条命令、校验哪个哈希。它与版本号解耦,升级时只改一处。

一次发布的顺序:更新 data/download/<key>.yamlversion → 新写一篇 content/blog/release/<version>.md 并填 release_url → 标签与资产就绪后把 published 翻成 true

验证

  1. 构建零告警:hugo --printPathWarnings --panicOnWarning。哈希行格式、算法混用、缺 base、渠道字段拼错都在这一步失败。
  2. 页面上:卡片显示的标签与日期与仓库一致;资产表每行都能点开真实的下载 URL。
  3. 逐条核对哈希与实际产物:组件只负责排版,不验证内容。
  4. 检查非 HTML 输出里哈希是完整的:
curl -s http://localhost:1313/zh/docs/write/releases/index.md | grep -c '^| '
  1. 发布前先用 published: false 走一遍,标签与资产确实存在后再改成 true;每种语言、子路径部署各测一次。

7 - API 文档

把 OpenAPI 规范放进站点,用随主题分发的 Swagger UI 或 Redoc 渲染成可浏览的接口文档,不连 CDN。

一页接口文档由一份 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
  • content/
    • docs/
      • write/
        • openapi.zh.md这一页

不要把规范文件放在页面旁边。redoc 会在内容目录里查找同名文件并据此拼出 URL,但内容目录里的 .yaml 是页面资源,Hugo 只在它被引用或处理时才发布。redoc 只拼 URL、不引用资源,浏览器因此得到 404。

远程规范(https://… 开头)两个 shortcode 都接受,但那是一项网络依赖,还会把读者的元数据暴露给那台主机。内网部署与有 CSP 的站点应当使用同源规范。

下面的例子用真实存在的 /openapi/docs-demo.yaml,一份演示用的集群管理 API,没有可访问的服务端。

Swagger UI

swagger 只有一个具名参数 src,值是从站点根开始的 URL。它经过主题的 URL 校验,子路径部署同样正确:

源码
{{< swagger src="/openapi/docs-demo.yaml" >}}

它渲染一个 class="td-swagger-ui" 的容器并就地初始化。容器 ID 由页面地址与 shortcode 序号推导(td-swagger-<hash>-<n>),因此同一页可以放多个。

本页只给源码,不真渲染 Swagger UI:它自己生成的标记有三处 axe WCAG AA 违规(服务器下拉框没有可访问名称、版本号区域是不能聚焦的可滚动区),本站的无障碍门禁要求每个页面零违规。下面的 Redoc 是真渲染的。

Redoc

redoc 只接受一个位置参数,即规范路径。多写一个参数构建失败。

源码
{{< redoc "openapi/docs-demo.yaml" >}}

路径解析按顺序有三条分支: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 页面类型:

content/api/_index.md
---
title: 集群管理 API
type: swagger
page_width: wide
cascade:
  type: 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-namescrollable-region-focusable),它来自上游产物,主题不改写。站点若有零违规的无障碍门禁,把这类页面排除,或改用 Redoc。
  • redoc 不接受额外属性参数:写第二个位置参数构建失败。
  • redoc 路径不要以 / 开头,否则拼出双斜杠。
  • 规范文件必须能被浏览器取到:放 static/,构建后确认 public/ 下存在该文件。
  • 没有服务端 mock:Swagger UI 的 “Try it out” 会向 servers 里写的地址发起真实请求,示例规范里的地址不可访问。

验证

  1. 构建零告警:hugo --printPathWarnings --panicOnWarning
  2. 规范确实发布了:ls public/openapi/docs-demo.yaml,或访问 http://localhost:1313/openapi/docs-demo.yaml
  3. 页面上能展开端点、看到 schema;浏览器控制台没有 404 或跨域报错。
  4. 断网后再刷新一次:运行时是本地的,规范同源时界面应照常出现。