跳转到主要内容

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

返回本页常规视图.

OINK 文档

OINK 是一款只需 Hugo Extended 的技术文档主题:组件写在 Markdown 里,资源随主题分发,双语开箱可用,一份内容产出四种输出。

OINK 是一款技术文档 Hugo 主题。组件是 Markdown 语法的一部分,不是另一套模板语言;浏览器需要的字体、图标、搜索与图表运行时随主题分发;构建依赖只有一个 Hugo Extended 二进制,不需要 Node.js,不请求 CDN。当前发布版本 v0.6.0。

五条入口

  • 十分钟上手 — 安装 Hugo、克隆本站、替换站点信息、部署。
  • 组件总览 — 每个组件一页,先给源码再给渲染效果。
  • 使用 OINK 创作优美的内容 — 从第一次预览到持续维护发布物的实战教程。
  • 案例 — 把生产站点拆解成可复用的设计与迁移模式。
  • 设计与开发 — 面向 OINK 维护者的契约、已接受决策、研究证据与候选提案。

按任务导航

你要做的事去哪
判断是否适用OINK 是什么
安装并预览十分钟上手
写一页文档编写页面
把目录树变成侧栏组织内容
查组件写法组件总览
改站名、Logo、配色与字体品牌外观
查某个配置键的默认值配置总览
做双语或多语言站多语言
从头到尾掌握 OINK使用 OINK 创作优美的内容
研究生产环境实现案例
部署到线上发布上线
升级版本或从 Docsy 迁移版本升级
维护主题、审查契约或编写 PRD设计与开发

Docs 的七个栏目按阅读顺序排列:了解、上手、写内容、查组件、改站点、管发布,最后理解并维护其背后的契约与设计记录。

1 - OINK 是什么

一款只需 Hugo Extended 的技术文档主题,从 Docsy 演化而来,组件写在 Markdown 里,资源随主题分发,十四个生产站点在用。

OINK 是一款独立的 Hugo 主题,用于搭建中大型技术文档站。它从 Docsy 演化而来:保留 Docsy 的内容模型与多语言行为,替换外壳、导航、搜索与内容组件。

消费站点的构建依赖只有一个 Hugo Extended 二进制,不需要 Node.js、npm 或 PostCSS,也不请求 CDN。Bootstrap、Font Awesome、字体、本地搜索、图表与 API 文档运行时都提交在主题仓库里,只在页面用到时下发。

组件不是另一套模板语言:> [!NOTE] 是提示块,表格加一行 {.fields} 是参数表,图片下面加 {caption=} 就有图注。当前有十四个生产站点在用它,本站是其中之一。

OINK 把 Markdown 内容、配置与本地资源汇成一个静态文档站
一次 Hugo 构建,产出可直接托管的静态站点

主题的职责

  • 文档与博客外壳:导航、侧栏树、目录、面包屑、翻页、深色模式、打印视图与无障碍交互。
  • 多语言框架:译文路由、缺译回退、语言权重、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 工具链需要主题内置内容管理后台或所见即所得编辑器

与其它文档方案的差别

下表只列结构性差别,且只写能从各项目自身文档与仓库确认的部分。各项目的版本会变动,选型前以其当前文档为准。

维度OINKDocsyHextraDocusaurus
构建工具Hugo Extended,单个二进制Hugo Extended + Node/npmHugoNode.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 多语言(OINK 的语言包由此继承)Hugo 多语言 + 21 个界面语言包内置 i18n 框架
书籍编号与交叉引用 / 发布下载页 / 数据驱动落地页主题内置需自建或找插件

两点补充。每页 Markdown 输出与 llms.txt 不是 OINK 独有的能力,Docsy 与 Hextra 也有,三者都要站点在 outputs 里显式打开。表格最后一行的三项只有 OINK 内置,它们来自 PGSTY 自己的生产站点,不是通用文档站的必需品。主题的交互功能默认关闭,搜索、缩放、评论与反馈都要站点显式打开。

OINK 不是叠在 Docsy 上的皮肤,而是 fork 之后独立演化的主题。Docsy 的源码历史、Apache-2.0 义务与署名完整保留,细节见开源许可与致谢

入口

  • 十分钟上手 — 安装 Hugo、克隆本站、替换站点信息、发布到 GitHub Pages。
  • 组件总览 — 一个组件一页,先源码后效果。
  • 示例站点 — 十四个生产站点,各自用了 OINK 的哪部分。

亮点特性按能力逐条列出主题提供的东西,每条链接到讲它的指南页。

1.1 - 亮点特性

逐条列出 OINK 与普通 Hugo 主题的差别,每条链接到讲它的指南页。

本页逐条列出 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 逐项记录 28 个依赖的版本、来源、许可证文件与 SHA-256 校验值,更新某个运行时要同时更新产物、许可证与校验值。

对可能引起网络请求的功能,主题让构建失败而不是静默连出去:PlantUML 缺 params.plantuml.svg_image_url、Diagrams.net 缺 params.drawio.drawio_server、Algolia 缺 appId / apiKey / indexName,构建都会报错。

本地优先不覆盖作者自己添加的内容。以下都是显式的网络选择:外部链接、远程图片与视频、iframe、远程 API 规范;Algolia、Google 自定义搜索这类托管搜索;分析、评论与其它 SaaS 集成;作者主动配置远程渲染器的 PlantUML 与 Diagrams.net。用到它们的页面仍然是有效页面,但站点不应再宣称这些页面可以完全离线使用。

开源许可与致谢 · 配置总览

一份内容,四种输出

每个组件在四种输出下都有确定的形态:交互式 HTML;去掉缩放与复制控件、折叠块完全展开的打印页;纯 Markdown;RSS。打印视图按栏目整份生成(本栏目是 /zh/_print/docs/about/),Markdown 版本是同一页面地址加 index.md

站点在 outputs 里显式选择需要哪几种,主题不替站点决定。

打印支持 · Agent 支持

双语与 32 个界面语言

多语言走 Hugo 原生机制:译文路由、按权重排序的语言选择器、缺译回退、RTL、以及 canonical 与 alternate 元数据。界面文案有 32 个语言包,共用同一套 key;英语、简体中文(zh-cn 与通用 zh)与繁体中文(zh-tw)经过人工审校,其余语言保留 Docsy 继承下来的翻译,OINK 新增的键先用英文兜底。

多语言

打开 params.offline_search 后,Hugo 为每种语言生成一份索引,浏览器用本地 Lunr 检索拉丁文字、用子串回退检索中日韩文本,查询内容不发给任何第三方。页面可以用 search_boost 调权重、用 search_keywords 补同义词。

全文检索

命令面板

Cmd/Ctrl + K 打开命令面板;裸按 / 进入搜索态,裸按 \ 进入纯命令态。面板里同时有页面、命令与页面动作(切换语言、切换主题、复制 Markdown 等),搜索与操作共用一个入口。

命令面板

键盘导航

默认开启,可按站点或按栏目关闭。w s 在侧栏树上下移动,a d 折叠展开,q e 上一篇下一篇,j k 沿页面目录跳转,t 切换深浅色,l 切换语言,h 隐藏阅读外壳。输入框、文本域获得焦点或输入法处于组字状态时,单键快捷键全部让行。页脚最底层栏的问号按钮打开速查卡。

键盘导航

文档之外的四种内容

主题还内置四类需要额外结构的页面:

  • 书籍:章节编号,图 / 表 / 式 / 例用 {#id num=} 编号、用 xref 交叉引用,book-tocbook-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)。

「在 ChatGPT / Claude 中打开」默认关闭:读者点击时会把当前 URL 交给第三方,需要站点显式打开 params.ui.page_context_menu.assistant_links

Agent 支持

多版本

配置 params.versions 后顶栏出现版本菜单,旧版本站点顶部显示归档横幅,提示读者查看最新版本;菜单是否逐页跳转由站点决定。多个版本是分别构建、分别部署的静态站点,不需要运行时支持。

多版本

自己验证

本站启用了上面多数特性,三条自查:

  1. 在任意页面按 Cmd/Ctrl + K,输入 postgres 查看本地搜索结果;按 \ 进入纯命令态。
  2. 在当前页面地址后加 index.md,得到这一页的 Markdown 版本。
  3. 打开 https://oink.pgsty.com/zh/llms.txt,那是给 AI 助手的站点清单。

1.2 - Case 导览

按文档、书籍、落地页与交互工具的形态,找到最接近自己需求的 OINK 生产案例。

正式的 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 平台。

如何选择起点

主题仓库的 tests/site/ 是内部 CI 夹具,而不是起步模板;其中页面的职责是 触发渲染行为。上面的生产案例更适合作为架构与设计参考。

浏览全部案例 · 十分钟上手 · 仓库导览

1.3 - 开源许可与致谢

查清哪一层适用哪份许可证:主题 Apache-2.0、文档 CC BY 4.0、随主题分发的第三方运行时各自保留原许可。

OINK 由三层材料组成:主题源码、文档内容、随主题分发的第三方资源。三者各自的许可证不会被重新授权成一份统一作品。下面每张表都指向仓库里的权威文件,摘要与许可证原文不一致时以文件为准

许可证对应关系

范围许可证权威文件
OINK 主题源码(布局、partial、 shortcode、SCSS、JS、i18n)Apache License 2.0主题 LICENSENOTICE
本站的站点代码、构建脚本与源自 Docsy 的材料Apache License 2.0站点 LICENSENOTICE
本站的原创文档内容(另有声明的除外)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/…)。

项目版本许可证在主题里做什么
bootstrap5.3.8MIT栅格、组件与 RTL 样式基础
@popperjs/core2.11.8MITBootstrap 的浮层定位
@fortawesome/fontawesome-free7.3.1CC-BY-4.0 AND OFL-1.1 AND MIT全站图标
@fontsource-variable/inter5.3.0OFL-1.1界面与正文字体
@fontsource/chakra-petch5.3.0OFL-1.1品牌展示字体
@fontsource/ibm-plex-mono5.3.0OFL-1.1代码字体
lunr2.3.9MIT本地全文检索
@docsearch/js5.0.1MIT可选的 Algolia DocSearch 前端
@docsearch/css5.0.1MIT同上的样式
mermaid11.16.1MITMermaid 图表
katex0.18.4MIT数学公式
markmap-autoloader0.18.12MIT思维导图
markmap-lib0.18.12MIT思维导图
markmap-view0.18.12MIT思维导图
markmap-toolbar0.18.12MIT思维导图工具条
d37.9.0ISCMarkmap 依赖
@highlightjs/cdn-assets11.12.0BSD-3-ClauseMarkmap 依赖
webfontloader1.6.28Apache-2.0Markmap 依赖
swagger-ui-dist5.32.13Apache-2.0OpenAPI 文档页
redoc2.5.3MITOpenAPI 文档页
asciinema-player3.17.0Apache-2.0终端录像回放
echarts6.1.0Apache-2.0图表
@antv/infographic0.2.19MIT信息图
pako3.0.1MIT AND Zlib解压(图表数据)
external-svg-loader1.7.1MIT内联外部 SVG
idb-keyval6.2.0Apache-2.0浏览器端缓存

许可证原文与各资源放在一起:例如 assets/third_party/bootstrap/LICENSEassets/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精炼的文档外壳、代码块的文件名与复制交互、按页布局开关
HextraHugo 原生的实现取向、文件树、徽章、标签页
Mintlify结构化导航分层、同步的代码分组、API 参考的阅读体验

Hugo 是构建平台,Go 在 Hugo Module 安装方式下负责解析模块。两者都是前提条件,主题不重新分发它们的可执行文件。

引用这些名字用于说明传承、依赖或灵感来源,不表示相关项目为 OINK 背书;各项目与产品名称归其权利人所有。

复用这份文档

CC BY 4.0 允许任何目的的分享与演绎,条件是给出署名、提供许可证链接、说明是否做过修改,并且不得暗示 OINK、PGSTY 或上游项目为改编内容背书。一段合格的署名可以是:

本文改编自 PGSTY 贡献者编写的 OINK 文档,采用 CC BY 4.0 许可,并做了修改。

页面里单独署名的图片或引文,要保留它们各自的署名与许可;删掉页脚不会免除署名义务。

复用这个主题

Apache-2.0 允许按条款使用、修改与分发主题源码及编译产物,条件是保留许可证、版权与归属声明,保留 NOTICE 内容,并在分发修改后的源码时标明改过哪些文件。主题发行包应当包含 LICENSENOTICEVENDOR.json,以及清单引用的全部第三方许可证文件。

Apache-2.0 不授予商标使用权,也不会把第三方资源变成 Apache 许可的作品。

2 - 十分钟上手

克隆 OINK 文档站,本地预览,替换站点信息,部署到 GitHub Pages。

这条路径不从空目录开始,而是克隆你正在读的这个站点,删掉不需要的部分,再替换成你自己的信息。本站是 OINK 的回归站,包含每个组件与每种页面类型,并与主题保持同版本;从它开始删减,比从空目录逐项补配置与示例少写很多。

前提:一台能安装 Hugo Extended 与 Go 的机器、一个 GitHub 账号、十分钟。不需要 Node.js,也不需要其它前端工具链。

结果

完成后得到一个双语文档站:左侧栏是你的目录树,右侧是本页目录,顶栏有全文搜索与命令面板,深浅色跟随系统;一份 Markdown 同时产出网页、打印页、纯 Markdown 与 RSS;托管在 GitHub Pages 上。

内容、配置与主题在构建期汇成一个静态站点的示意图
一份内容,四种输出:HTML、打印、Markdown、RSS

步骤

  1. 安装 Hugo Extended 与 Go

    除 Git 之外需要两样。Hugo Extended 必须是 0.160.1 或更高版本:标准版 Hugo 没有内置 Sass 编译器,编译不了主题样式,构建失败。Go 用于解析模块:OINK 以 Hugo Module 发布,Hugo 通过 Go 的模块机制下载并校验 github.com/pgsty/oink

    macOS
    brew install hugo go git
    Linux
    # 发行版仓库里的 Hugo 往往过旧,改用官方 deb 包(本站 CI 也是如此)
    curl -LO https://github.com/gohugoio/hugo/releases/download/v0.164.0/hugo_extended_0.164.0_linux-amd64.deb
    sudo dpkg -i hugo_extended_0.164.0_linux-amd64.deb
    sudo apt install -y golang-go git
    Windows
    winget install Hugo.Hugo.Extended
    winget install GoLang.Go
    winget install Git.Git

    安装完成后核对一次,输出里必须出现 extended

    $ hugo version
    hugo v0.164.0+extended+withdeploy darwin/arm64 BuildDate=2026-07-06T16:39:30Z
    $ go version
    go version go1.26.6 darwin/arm64
    

    其它平台按 Hugo 安装指南go.dev/dl 安装,注意选 extended 版本。

  2. 克隆文档站并预览

    git clone https://github.com/pgsty/oink.pgsty.com my-docs
    cd my-docs
    hugo server

    打开 http://localhost:1313/,中文站在 http://localhost:1313/zh/。第一次启动会下载主题模块(几秒到一分钟,取决于网络),之后修改文件是毫秒级热重载。

    已提交的 go.mod 固定了主题版本,克隆之后即可构建,不需要额外的安装脚本。

    说明

    仓库里的 Makefile 只是几条命令的别名。make devmake check 通过 HUGO_MODULE_REPLACEMENTS 使用同级的 ../oink 主题 checkout;make buildmake serve 始终使用 go.mod 固定的公开版本。新站点用 hugo server 即可。

  3. 替换站点信息

    站点身份:全部在 hugo.yml 里。baseURL 用的是 YAML 锚点,实际地址写在 params.productionURL 上,只改这一处:

    hugo.yml
    title: Product Docs # 顶栏站名与 <title>
    
    params:
      productionURL: &productionURL https://docs.example.com/
      github_repo: https://github.com/example/product-docs # 「编辑当前页面」指向哪
      copyright:
        authors: '[Example Inc.](https://example.com/)'
        from_year: 2026
      footer_center_info: ''
    
    baseURL: *productionURL

    languages.en.titlelanguages.zh.title 会覆盖顶层 title,两处一起改。参数逐项的含义与默认值见配置总览

    删掉本站专用的配置:保留它们会让你的站点指向 OINK 的仓库与账号。

    hugo.yml 里的键怎么处理
    services.googleAnalytics.idOINK 的统计 ID,删掉;需要统计时换成你自己的
    params.commentsgiscus 指向 pgsty/oink.pgsty.com 的讨论区,整段删掉或换成你的仓库
    params.tdVersion params.version params.version_menu params.versionsOINK 的版本菜单,删掉
    params.github_project_repo主题仓库链接,删掉
    languages.<lang>.menus.main顶栏菜单指向 /docs/tutorial 这类本站栏目,按你的目录重写

    换 Logo 与图标:替换这三个文件,文件名保持不变,主题按文件名挂载:

    static/
    static/favicon.svg           # 浏览器标签页图标
    static/favicon.ico
    static/apple-touch-icon.png  # iOS 添加到主屏

    static/logo.svg 是本站自己的品牌组合标,没有参数指向它:删掉,或者换成你的横向字标再设 params.wordmark

    替换内容content/docs/ 是 OINK 自己的主题文档,整棵删除,写你自己的第一页:

    rm -rf content/docs && mkdir -p content/docs
    content/docs/_index.md
    ---
    title: Docs
    linkTitle: Docs
    description: Product documentation.
    weight: 20
    ---
    
    Everything about running Product in production.

    content/blog/ 可以留一篇当模板,也可以整个目录删除(删除后把 menus.main 里的 blog 项一并删掉)。哪些目录必须保留、哪些是文档站自用,见仓库导览

    只做英文站:删除 languages.zh 整段与所有 .zh.md 文件,languages 缩成一段:

    hugo.yml
    defaultContentLanguage: en
    languages:
      en:
        label: English
        locale: en-US
        weight: 1
        title: Product Docs
        menus:
          main:
            - { name: Docs, pageRef: /docs, weight: 20 }
    find content -name '*.zh.md' -delete

    保留双语或换成其它语言对,见多语言

  4. 部署

    在 GitHub 上新建一个空仓库,把本地历史换成你自己的:

    rm -rf .git && git init -b main
    git add . && git commit -m "Initial documentation site"
    git remote add origin git@github.com:example/product-docs.git
    git push -u origin main

    仓库自带 .github/workflows/pages.yml:推到 main 分支即构建并发布,也可以在 Actions 页面手动触发(workflow_dispatch)。它固定 Hugo Extended 与 Go 的版本,用 --printPathWarnings --panicOnWarning 构建,baseURL 由 GitHub Pages 提供,因此发布到 example.github.io/product-docs/ 这类子路径也不必改配置。

    在仓库的 Settings → Pages → Build and deployment → Source 选 GitHub Actions。默认值是 Deploy from a branch,不改这一项 workflow 会在部署步骤失败。

    删除 scripts/ 后要改 workflow

    pages.yml 中的 Verify advertised and pinned release match 一步运行 node scripts/check-release-pin.mjs,校验站点公告的版本与 go.mod 固定的版本一致。删掉 scripts/ 之后,把这一步与 Set up Node.js 一并从 pages.yml 移除。

    Cloudflare Pages、Netlify、Nginx 与离线打包见发布上线:构建命令都是 hugo --gc --minify,区别只在 baseURL 与环境变量。

验证

本地运行一次生产构建。它比开发服务器严格,路径告警会让构建失败:

hugo --gc --minify --printPathWarnings --panicOnWarning

输出 Total in … 且没有 WARN / ERROR 即通过。再对照预览核对:

  • 顶栏是你的站名与 Logo,浏览器标签页是你的 favicon
  • 侧栏是你自己的目录树,每页都能打开
  • CtrlK(macOS 上是 K)打开命令面板,能搜到刚写的页面
  • 页面标题右侧的菜单里,「编辑当前页面」指向你自己的仓库,不是 pgsty/oink.pgsty.com
  • 部署后 GitHub 仓库 Actions 页面里的 Deploy Oink site to GitHub Pages 是绿的

构建报错见排错与检查

下一步

  • 仓库导览 — 克隆下来的每个目录是什么,哪些可以删。
  • 编写页面 — 一页文档的组成:front matter、标题锚点、链接与图片。
  • 组件总览 — 提示块、标签页、参数表、文件树等,每个组件一页。
  • 品牌外观 — 主色、字体预设、页宽与自定义样式。
  • 发布上线 — GitHub Pages 之外的托管方式与验收清单。

给编码助手的指令

上面四步可以交给编码助手(Claude Code、Codex 等)执行。复制下面这段指令,把方括号里的三处替换为你自己的信息:

可整段复制的指令
请帮我用 OINK 主题建一个文档站,按下面的流程做,遇到不确定的地方按「只在缺信息时问人」处理。

1. 检查环境:运行 `hugo version`,要求输出包含 `extended` 且版本 >= 0.160.1;运行 `go version`,
   要求能拿到版本号。任一不满足就先按官方文档安装,macOS 用 `brew install hugo go`,
   Debian/Ubuntu 装 GitHub Releases 上的 hugo_extended deb 包。
2. 克隆站点模板:`git clone https://github.com/pgsty/oink.pgsty.com [目标目录]` 并进入该目录。
3. 改 hugo.yml 三处:顶层 `title` 与 `languages.<lang>.title` 改成 [站点名称];
   `params.productionURL` 改成 [站点域名](baseURL 是指向它的 YAML 锚点,不要单独改 baseURL);
   `params.github_repo` 改成本站将来的仓库地址。
   同时删掉这些本站专用配置:`services.googleAnalytics`、`params.comments`、
   `params.tdVersion`、`params.version`、`params.version_menu`、`params.versions`、
   `params.github_project_repo`,并把 `menus.main` 改成只指向 /docs 与 /blog。
4. 清空示例内容:删除 `content/docs/` 整棵目录,新建 `content/docs/_index.md`
   (front matter 至少有 title / description / weight);`content/blog/` 下只保留一篇文章当模板。
   删除文档站自用的脚手架:`tests/`、`scripts/`、`playwright.config.mjs`、`package.json`、
   `package-lock.json`、`AGENTS.md`、`TRANSLATION.md`、`CONTRIBUTING.md`、`agent-docs.config.yml`,
   并把 `.github/workflows/` 下除 `pages.yml` 外的 workflow 删掉,
   同时删掉 `pages.yml` 里的 `Set up Node.js` 与 `Verify advertised and pinned release match` 两步。
5. 后台启动 `hugo server`,确认 http://localhost:1313/ 返回 200 且页面标题是新站点名。
6. 校验:运行 `hugo --gc --minify --printPathWarnings --panicOnWarning`,
   要求以 `Total in ...` 结束且没有 WARN/ERROR;有报错就修到通过,不要用忽略告警的方式绕过。
7. 只在缺少 [站点名称]、[站点域名]、仓库地址这三项信息时才问我,其余按上面的默认做法执行。

建好的站点便于助手读取:每页都有 .md 纯文本输出,站点根有 llms.txt,页面标题右侧的菜单里有「复制 Markdown 文本」与「在 Claude 中打开」。见 Agent 支持

不从这个仓库起步、要从空目录搭建,见从零建站与其它安装方式

2.1 - 仓库导览

克隆下来的每个目录是什么:哪些必须保留、哪些替换为你的信息、哪些是文档站自用可以整个删除。

本页逐项说明 pgsty/oink.pgsty.com 克隆下来的每个文件与目录:哪些必须保留、哪些替换为你自己的信息、哪些是文档站自用可以整棵删除,并给出一个安全的删除顺序。

主题代码不在这个仓库里:它是 go.mod 固定的一个 Hugo Module,存放在 Go 的模块缓存中。这个仓库只有内容、配置与站点自己的少量覆盖。

顶层结构

克隆下来的 my-docs/

  • my-docs/
    • hugo.yml站点唯一配置:身份、语言、菜单、参数、模块导入
    • go.mod固定主题版本
    • go.sum主题模块的校验和
    • content/全部内容,目录结构就是侧栏结构
      • _index.md首页;_index.zh.md 是它的中文对等页
      • search.mdGoogle 自定义搜索的结果页,用不到可删
      • docs/文档树:OINK 自己的主题文档
      • blog/博客:工程记录与版本发布
    • assets/要经 Hugo 处理的资源
      • scss/站点样式覆盖,三个 partial
      • images/需要缩放裁切的图片
      • parts/include shortcode 引入的 Markdown 与 YAML 片段
    • static/原样复制到站点根,不做处理
      • logo.svg品牌组合标,没有参数指向它
      • favicon.svg浏览器标签页图标
      • favicon.ico
      • apple-touch-icon.pngiOS 添加到主屏
      • images/截图与示意图
    • layouts/站点模板覆盖:只覆盖最窄的那一个
      • _shortcodes/站点自己的 shortcode
    • data/数据驱动的页面
      • home/首页分区:en.yaml / zh.yaml
      • landing/Landing 页数据
      • download/发布与下载页数据
    • .github/
      • workflows/pages.yml 部署;另外两个是本站回归测试
    • tests/文档站自用:Playwright、goldens、构建断言
      • browser/Playwright 规格
      • hugo-build/构建断言
      • md-output/Markdown 输出 goldens
      • alt-site/备用配置构建
      • favicons/
      • release-pin/
      • fixtures/
    • scripts/文档站自用:翻译对等与链接检查
      • check-doc-translations.mjs
      • check-markdown-style.mjs
      • check-rendered-links.mjs
      • check-rendered-markdown.mjs
      • check-release-pin.mjs
    • Makefilebuild / serve 直接调 Hugo;dev / check 指向同级 ../oink
    • package.json测试工具链,站点构建用不到
    • package-lock.json
    • playwright.config.mjs
    • agent-docs.config.ymlAgent 文档评分工具的配置
    • AGENTS.md给编码 Agent 的仓库说明
    • TRANSLATION.md双语翻译流程
    • CONTRIBUTING.md
    • README.md
    • LICENSEApache-2.0,站点代码
    • LICENSE-CC-BY-4.0内容许可
    • NOTICE

上面没有列出的还有 .gitignore.gitattributes.nvmrc.npmrc,以及被 .gitignore 排除的生成物:public/(构建产物)、resources/(Hugo 资源缓存)、node_modules/。后一组不进版本库。

仓库里没有 i18n/:界面文字(「上一页」「本页目录」这类)由主题的 32 份语言文件提供。要改其中某一句,在站点根目录建 i18n/zh.yaml,只写需要覆盖的键。

各项的处理方式

路径是什么fork 后怎么处理
hugo.yml站点的唯一配置文件,没有 config/ 目录也没有分环境覆盖替换为你的信息:身份、语言、菜单、品牌
go.mod go.sum固定主题版本并记录校验和必须保留,一起提交
content/全部内容;目录结构决定侧栏结构必须保留;里面的 docs/blog/ 换成你自己的
content/search.mdlayout: search 的整页搜索结果,只在配了 Google 自定义搜索(params.gcs_engine_id)时才有内容用主题自带的本地搜索时可以删
assets/scss/站点样式覆盖(_variables_project.scss 等)要改配色字体就保留,不改可以清空
assets/images/需要 Hugo 处理(缩放、裁切)的图片换成你自己的
assets/parts/include shortcode 引入的片段随引用它的页面一起替换或删除
static/原样复制到站点根替换为你的:logo、favicon、截图
layouts/_shortcodes/本站自己的四个 shortcode,当前内容里已无引用可删
data/home/首页分区数据(Hero、能力面板)改成你的;删除后首页退回普通页面
data/landing/ data/download/Landing 页与发布下载页的数据用不到就删
.github/workflows/pages.yml推到 main 就构建并发布到 GitHub Pages保留,按你的仓库改
.github/workflows/site-checks.yml browser-quality.yml本站的回归测试流水线文档站自用,可删
tests/ scripts/ playwright.config.mjs package.json package-lock.json本站的回归测试与检查工具链文档站自用,可删
Makefile主题与站点共同开发的快捷方式(要求同级有 ../oink文档站自用,可删
AGENTS.md TRANSLATION.md CONTRIBUTING.md agent-docs.config.yml本站的协作约定换成你自己的,或删
README.md LICENSE LICENSE-CC-BY-4.0 NOTICE说明与许可换成你自己的
.nvmrc .npmrcNode 版本与 npm 配置package.json 一起删
用 OINK 建站不需要 Node.js

这个仓库里的 package.jsontests/scripts/ 用于维护文档站本身。你的站点构建只有一条命令:hugo --gc --minify

删除顺序

顺序是先删外围、再删内容、最后清数据。每删一步构建一次,出问题时能定位到具体步骤。

  1. 删脚手架

    这一批与站点渲染无关,删除后不影响任何页面。

    rm -rf tests scripts node_modules
    rm -f package.json package-lock.json playwright.config.mjs .nvmrc .npmrc
    rm -f AGENTS.md TRANSLATION.md CONTRIBUTING.md agent-docs.config.yml Makefile
    rm -f .github/workflows/site-checks.yml .github/workflows/browser-quality.yml

    删除 scripts/ 之后必须改 .github/workflows/pages.yml:把 Set up Node.jsVerify advertised and pinned release match 两步删掉,否则部署会在该步骤失败。

  2. 删示例内容

    content/docs/ 是 OINK 自己的主题文档,content/blog/ 是它的工程博客,与你的产品无关。

    rm -rf content/docs
    mkdir -p content/docs
    rm -rf content/blog        # 不要博客的话;要的话只留一篇当模板

    同时改 hugo.yml 里每种语言下的 menus.main:那些菜单项指向 /docs/tutorial/blog/release 这些已不存在的路径。content/_index.md 是首页,保留它,把正文换成你的。

  3. 清数据

    data/ 下三组数据分别供首页、Landing 页与发布页使用。首页数据保留后修改,另外两组用不到就删除。

    rm -rf data/landing data/download

    data/home/en.yamldata/home/zh.yaml 决定首页有哪些分区,逐项含义见首页与落地页。删除整个 data/home/ 也能构建,首页退回为普通内容页。

  4. 换身份

    最后把 hugo.yml 里的站名、params.productionURLparams.github_repo 与品牌参数换成你的,替换 static/ 下的 logo 与 favicon,删掉 services.googleAnalyticsparams.commentsparams.version* 这些 OINK 专用配置。逐条清单见十分钟上手第 3 步。

主题的位置

主题以 Hugo Module 的形式引用,两处配置指向它:

hugo.yml
module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: '0.160.1'
go.mod
require github.com/pgsty/oink v0.6.0

hugo.yml 声明使用哪个主题,go.mod 固定用它的哪一版,go.sum 记录该版本的校验和。三个文件都要提交。主题源码不进你的仓库:Hugo 把它下载到 Go 的模块缓存,hugo mod graph 显示实际解析结果。

升级到最新版:

hugo mod get -u github.com/pgsty/oink

固定到某一个版本:

hugo mod get github.com/pgsty/oink@v0.6.0

两条命令都会改写 go.modgo.sum。生产站点固定到发布标签,不要跟随 main。升级前后检查什么、如何回滚,见版本升级

站点覆盖

layouts/ 下的文件按 Hugo 的模板查找顺序盖过主题里的同名文件。本站只放了一类:

  • layouts/_shortcodes/*.html:站点自己的 shortcode。产品文档需要带业务语义的 shortcode 时也放这里。

标题自链锚点由主题的 _markup/render-heading.html 提供,站点不需要自己建这个钩子。

要改外壳(侧栏、页脚、页尾)时,覆盖最窄的那个 partial,不要整份复制 baseof.html:复制之后每次主题升级都要手工合并。

验证

每删一步运行一次构建,报错能定位到刚删除的内容:

hugo --gc --minify --printPathWarnings --panicOnWarning

删除完成后,这几条应当成立:

  • 构建以 Total in … 结束,没有 WARN / ERROR
  • 顶栏菜单没有指向已删目录的死链
  • 标题右侧仍然有自链锚点(主题自带的标题渲染钩子,站点不需要覆盖)
  • git status 里没有 public/resources/

2.2 - 从零建站与其它安装方式

从空目录搭一个最小 OINK 站点,以及 Module / submodule / 离线归档 / 克隆四种安装方式的取舍。

本页从空目录搭建一个最小 OINK 站点:十几行 hugo.yml 加一条 hugo mod get,得到一个可预览的单语站点。代价是首页、示例内容与可参照的组件用法都要自己写。

已有 Hugo 站点时不需要脚手架:装上主题模块,再补三项 goldmark 前置配置(见hugo.yml),正文不用重写。已有 Docsy 站点见版本升级

后半部分是四种安装方式的取舍:Hugo Module、Git submodule、离线归档、固定版本克隆。

从空目录到第一页

  1. 建骨架并获取主题

    hugo new site --format yaml my-docs
    cd my-docs
    hugo mod init github.com/example/my-docs
    hugo mod get github.com/pgsty/oink@v0.6.0

    hugo mod init 后面跟的是你自己站点的模块路径,通常就是仓库地址。hugo mod get 会写出 go.modgo.sum,两个都要提交。

    最新版本号在 GitHub Releases;本页出现的 v0.6.0 是本站当前固定的版本。生产站点固定到发布标签,不要跟随 main@latest 是一次性解析动作,不是版本策略。

  2. hugo.yml

    hugo new site 生成的 hugo.yaml 改名为 hugo.yml(两个后缀 Hugo 都接受,本文统一用后者),内容替换为下面这份,可直接构建:

    hugo.yml
    title: Product Docs
    baseURL: https://docs.example.com/
    defaultContentLanguage: en
    # enableGitInfo: true        # 页面「最后修改」时间来自 git,先 git init 再打开
    
    languages:
      en:
        label: English
        locale: en-US
        weight: 1
        title: Product Docs
        params:
          description: Everything about running Product in production
        menus:
          main:
            - { name: Docs, pageRef: /docs, weight: 20 }
            - { name: Blog, pageRef: /blog, weight: 50 }
    
    # 三项 Goldmark 前置:OINK 的原生 Markdown 组件全靠它们
    markup:
      goldmark:
        renderer:
          unsafe: true # 允许内容里的行内 HTML
        parser:
          attribute:
            block: true # {.steps} {.cards} {caption=} 这类属性行
          wrapStandAloneImageWithinParagraph: false # 块级图片才能带属性行
      highlight:
        noClasses: false # 代码配色跟随深浅色模式
    
    params:
      offline_search: true
      github_repo: https://github.com/example/product-docs
      copyright:
        authors: '[Example Inc.](https://example.com/)'
        from_year: 2026
      ui:
        dark_mode: true
        sidebar_menu_foldable: true
        section_index: cards
    
    outputs:
      home: [HTML, markdown, LLMS]
      page: [HTML, markdown]
      section: [HTML, RSS, print, markdown]
    
    module:
      imports:
        - path: github.com/pgsty/oink
      hugoVersion:
        extended: true
        min: '0.160.1'

    五段分别管什么:

    管什么少了会怎样
    顶层 + languages站名、域名、语言与顶栏菜单baseURL 不对,线上所有绝对链接指错
    markup.goldmark三项组件前置属性行变成正文里的一行 {.steps}
    params搜索、仓库链接、外壳开关交互功能默认关闭,主题不替站点决定
    outputs每页的 .mdllms.txt、打印页页面菜单里没有「复制 Markdown」,也没有打印视图
    module引用主题、声明 Hugo 下限构建时找不到主题

    写公式还需要 Goldmark 的 passthrough 扩展,见公式。每个键的完整含义与默认值见配置总览

  3. 写第一页

    content/ 下的每个一级目录是一个分区,目录结构就是侧栏结构。文档分区至少要有一个 _index.md

    content/docs/_index.md
    ---
    title: Docs
    linkTitle: Docs
    description: Everything about running Product in production.
    weight: 20
    ---
    
    从[安装](/docs/install/)开始。
    content/docs/install.md
    ---
    title: Install
    description: Install Product on a fresh machine.
    weight: 10
    ---
    
    ## Prerequisites {#prerequisites}
    
    > [!IMPORTANT]
    > Product 需要 PostgreSQL 18 或更高版本。
    
    ## Install {#install}
    
    ```bash
    curl -fsSL https://get.example.com | bash
    ```

    标题写显式 {#id}:后续加译文时两种语言的锚点才能对应。页面写法见编写页面

  4. 预览

    hugo server

    打开 http://localhost:1313/,侧栏里有 Docs → Install。修改文件是毫秒级热重载。

其它安装方式

上面用的是 Hugo Module。另外三种方式面向特定约束:网络隔离、平台要求构建输入包含完整主题树、组织内部需要评审主题副本。除 hugo mod vendor 之外,它们都不建立 Go 模块,站点用 theme: oink 而不是 module.imports 引用主题;共同的代价是版本解析与完整性校验由你自己负责。

Hugo Module(推荐)

hugo mod init github.com/example/product-docs
hugo mod get github.com/pgsty/oink@v0.6.0
hugo.yml
module:
  imports:
    - path: github.com/pgsty/oink

唯一能让 Hugo 自己解析版本、校验 checksum、并在 go.sum 里留下审计记录的方式。hugo mod graph 看实际解析结果,hugo mod get -u 升级。需要本机有 Go。

Git submodule

在站点仓库里记录准确的主题 commit:

git submodule add https://github.com/pgsty/oink.git themes/oink
git -C themes/oink fetch --tags
git -C themes/oink checkout v0.6.0
git add .gitmodules themes/oink
hugo.yml
theme: oink

CI 必须在运行 Hugo 之前初始化 submodule,否则 themes/oink 是空目录:

git submodule update --init --recursive

离线归档

网络隔离环境使用。两条路径,都先在联网机器上准备,再整体搬入。

hugo mod vendor:把已解析的主题源码固化进站点目录,之后构建既不联网也不需要 Go。

hugo mod vendor          # 生成 _vendor/,里面是主题的完整源码树
tar czf my-docs.tgz .    # 连 _vendor/ 一起搬进隔离环境

_vendor/ 存在时 Hugo 优先使用它(hugo mod graph 输出 +vendor),hugo.yml 里的 module.imports 保持不变。这一步需要 Go,之后的构建不需要。升级主题要回到联网环境重新执行 hugo mod gethugo mod vendor

_vendor/ 只收主题挂载出来的目录(assets data i18n layouts static)以及 hugo.yamltheme.toml,不含 LICENSENOTICEVENDOR.json。要对外分发这份归档,把这三个文件从主题仓库一并取来。

用 tag 源码归档:不建 Go 模块,直接把某个版本的主题解压到 themes/oink/

curl -L -o oink.tar.gz \
  https://github.com/pgsty/oink/archive/refs/tags/v0.6.0.tar.gz
mkdir -p themes/oink
tar xzf oink.tar.gz -C themes/oink --strip-components=1
hugo.yml
theme: oink

主题仓库的根目录就是模块根目录,解压出来直接是 layouts/assets/i18n/static/ 这一层,不需要再进入下一级。重新分发时必须保留 LICENSENOTICEVENDOR.json。最后一个记录了每个第三方运行时的版本、来源、许可证路径与 SHA-256,是离线审计的依据。

跨机器传输时,在联网侧从不可变标签生成归档与校验值:

git clone --branch v0.6.0 --depth 1 \
  https://github.com/pgsty/oink.git oink
git -C oink archive --format=tar.gz --prefix=oink/ \
  --output=../oink-v0.6.0.tar.gz v0.6.0
shasum -a 256 oink-v0.6.0.tar.gz \
  > oink-v0.6.0.tar.gz.sha256

把归档与 .sha256 一起传入隔离环境,先校验再解压:

shasum -a 256 -c oink-v0.6.0.tar.gz.sha256
mkdir -p themes
tar -xzf oink-v0.6.0.tar.gz -C themes

这样得到的归档是自建产物,不是项目发行物。某个标签的发行页面是否附带归档与校验文件按发布而定,使用公开附件时独立验证其校验值。

断网构建之前确认归档内容完整,这十一项都要在:

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 许可证表

固定版本克隆

托管平台要求构建输入包含完整主题树时用:

git clone https://github.com/pgsty/oink.git themes/oink
git -C themes/oink checkout v0.6.0

与 submodule 的区别是主题文件直接进入你的仓库历史,没有 .gitmodules 这层间接。记录最终解析出的 commit 与恢复流程。

四种方式对比

方式需要 Go版本可审计主题源码进你的仓库适用
Hugo Modulego.sum 自动校验默认推荐
Git submodule仓库记录 commit以引用形式需要主题源码在库内
离线归档手工核对 checksum网络隔离
固定版本克隆需自行记录平台要求完整树
消费站点不需要前端工具链

Bootstrap、Font Awesome、字体、搜索与图表运行时全部随主题分发。站点不需要 node_modules、PostCSS、RTLCSS,也不需要 CDN。为 Docsy 站点安装 npm 依赖的教程属于上游 Docsy 的流程,不适用于 OINK。

用本地主题 checkout 开发

同时修改主题与站点时才需要这一节。把两个仓库克隆为同级目录:

同级目录布局
~/pgsty/
├── oink/            # 主题
└── product-docs/    # 你的站点

用环境变量 HUGO_MODULE_REPLACEMENTS 把模块临时替换为本地 checkout,go.mod 不变:

cd ~/pgsty/product-docs
HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> ../oink' hugo server

文档站仓库的 Makefile 就是这几条命令的别名,make devmake check 要求主题 checkout 在同级目录 ../oink

Makefile:文档站里的写法
build:
	hugo --cleanDestinationDir --minify

check:
	HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' npm test

dev:
	HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> $(abspath ../oink)' hugo server --renderToMemory

Go workspace(go work init + HUGO_MODULE_WORKSPACE=go.work)是等价的另一种做法。两种做法都只作用于本机:CI 与生产构建用的是 go.mod 里的版本,go.work 不要提交。

验证

hugo mod graph                                       # 主题实际解析到哪一版
hugo --gc --minify --printPathWarnings --panicOnWarning

构建以 Total in … 结束、没有 WARN / ERROR 即通过。再确认:

  • /docs/ 打得开,侧栏里有你写的页面
  • 顶栏有搜索框,搜得到刚写的标题
  • 深浅色切换按钮在,切换后代码块配色跟着变(说明 markup.highlight.noClasses: false 生效)
  • git status 里有 go.modgo.sum,没有 public/resources/

3 - 创作内容

写文档页、博客、书籍、发布页与 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 文档
中英双语写作:对等文件、锚点对齐、缺译回退多语言
某个组件的语法与参数组件总览

3.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 围栏(还有 plantumlmarkmapechartsMermaid

剩下的少数组件(徽章、按键、引用文件、终端录像、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 做这项审计)。

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

每个目录都要有 _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.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

3.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 能点进对应的标签页。

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-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 配置
  • 打印支持 — 整章与整本打印

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
多渠道下载区块downloaddata/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-2561e2f4c8a9d05b7361f8ac25d0e7b4913a6c8df215047eb9c3a1d6b8250f9e7c4
oink-0.4.0-darwin-arm64.tar.gz macOSarm64SHA-2567b3d9e0c145a8f26d0b7e93c48156aa2f0d9c7b31e846a5029df1b6c7a3e8250

只接受两种行:<十六进制><两个空格><文件名><十六进制><空格>*<文件名>。空行与以 # 开头的行忽略。哈希长度决定算法(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-2565a0c7d1e93b4826f0ad35c9e17b6402d8f1c95ae63d70b28c4e19a5f38207db6
oink-0.4.0-1.el9.aarch64.rpm Linuxarm64SHA-256c93f16a8d052b7e41ac68d3907b25fe0a41d8c7362b95e0187ac4d63f9520ea8

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-256aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

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;每种语言、子路径部署各测一次。

3.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. 断网后再刷新一次:运行时是本地的,规范同源时界面应照常出现。

4 - 组件总览

写文档时可用的全部组件,一个组件一页,例子由浅入深,参数表在页尾。

这一栏回答一个问题:某个组件在 Markdown 里怎么写。每页的顺序相同:最简例子、逐步深入的例子、输出形态、参数表、限制。查语法见下面的速查表。

两种形态

组件的第一形态是 Markdown 语法本身:块引用、列表、表格、图片、围栏,加上紧跟其后的一行 {…} 属性。原生形态在 GitHub 与任意 Markdown 编辑器中仍然可读,Markdown 输出保留的也是源码。

原生形态表达不了的场景使用 shortcode:正文标签页、带块级描述的参数表、带图标与徽章的卡片、终端录像。规则有五条:

  • 所有 shortcode 都写 {{</* 名字 */>}},只有 {{%/* steps */%}}% 分隔符,因为它的正文是页面级 Markdown。
  • 嵌套名字(tabcardfield)只在各自的父 shortcode 里有效。
  • 参数写错不会静悄悄降级,构建失败,报错带文件名与行号。
  • 公开字符串参数(图注、标签、标题)一律是纯文本,不解析 Markdown。只有正文是 Markdown:tabcardfield 的正文,include 引入的文件,以及 Book 的 figtbleg 正文。
  • 页面没用到的组件不下发运行时。脚本按这一页实际用到的组件拼成一个包,打印、Markdown 与 RSS 输出不加载任何脚本。

站点前置配置

组件依赖三项 Goldmark 设置。克隆本站起步时它们已经配好,从零建站照抄以下片段:

hugo.yml
markup:
  goldmark:
    renderer:
      unsafe: true # 内容里的 HTML 不被剥掉
    parser:
      attribute:
        block: true # 启用 {…} 属性行
      wrapStandAloneImageWithinParagraph: false # 独立图片不再包进 <p>
  • 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]原生
图片图注、尺寸、缩放、编号与构建期图片处理![说明](oink.webp)原生需站点开关
代码块高亮、标题、复制、折叠、行链接```sh围栏按页加载
标签页同一件事的多个平台或语言版本属性行 {tab="Linux"}原生 + shortcode按页加载
表格普通表格,加满宽、矩阵、标题与编号{.full-width}原生
参数表参数清单,带类型 / 必填 / 默认值芯片{.fields meta="type default"}原生 + shortcode
步骤有先后的流程{.steps}原生 + shortcode
卡片一组并列的去处{.cards}原生 + shortcode
文件树目录结构与对齐的注释列```filetree围栏按页加载
公式KaTeX 行内与块级公式$$ … $$原生按页加载
Mermaid流程图、时序图、甘特图```mermaid围栏按页加载
PlantUMLUML 图;需要自建渲染服务```plantuml围栏需站点开关
思维导图Markdown 列表变成思维导图```markmap围栏需站点开关
Draw.io可回编辑的图;需要自建服务![说明](arch.drawio.svg)原生需站点开关
ECharts声明式数据图表```echarts围栏按页加载
InfographicAntV 信息图```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] 起头,正文跟在后面。用于把提示、警告、前提条件从正文中分离出来;正文一句话能说清的内容不必使用提示块。

最简例子

源码
> [!NOTE]
> Hugo Module 需要本机安装 Go;只用离线归档时不需要。
说明

Hugo Module 需要本机安装 Go;只用离线归档时不需要。

不写标题时使用本地化的类型名(中文站显示「注意」,英文站显示 “Note”)。源码在 GitHub 上按 GitHub 的提示块渲染,在普通 Markdown 阅读器中显示为块引用,内容都不会丢失。

十种类型

前五种与 GitHub 一致,后五种是 OINK 追加的语义类型。每种类型有默认图标与强调色。

源码
> [!TIP]
> 用 `hugo server -D` 可以预览草稿。

> [!IMPORTANT]
> 主题下限是 Hugo Extended 0.160.1,低于它构建直接失败。

> [!WARNING]
> `hugo --cleanDestinationDir` 会清空 `public/`。

> [!CAUTION]
> 删除 `resources/_gen` 后第一次构建会慢很多。

> [!SUCCESS]
> 构建通过、零告警——可以推上线了。

> [!DANGER]
> 不要把 `go.work` 提交进仓库。

> [!QUESTION]
> 站点要不要开评论?看[启用评论](/zh/docs/admin/comments/)。

> [!EXAMPLE]
> `pgsty.com` 就是一个只用了提示块与表格的纯文档站。

> [!QUOTE]
> Documentation is a love letter that you write to your future self.
提示

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(代码、粗体、链接)。

源码
> [!WARNING] 会改写 `public/`
> 生产构建前先确认 `baseURL` 指向正式域名,否则所有绝对链接都会指错。
会改写 public/

生产构建前先确认 baseURL 指向正式域名,否则所有绝对链接都会指错。

正文内容

正文是页面级 Markdown:列表、代码围栏、表格、图片、嵌套的提示块。每一行都以 > 开头,围栏也不例外。

源码
> [!TIP] 三条命令启动预览
>
> 1. 克隆:`git clone https://github.com/pgsty/oink.pgsty.com my-docs`
> 2. 进入目录并预览:
>    ```bash
>    cd my-docs && hugo server
>    ```
> 3. 打开 <http://localhost:1313/>
>
> | 端口 | 用途 |
> | --- | --- |
> | 1313 | Hugo 开发服务器 |
三条命令启动预览
  1. 克隆:git clone https://github.com/pgsty/oink.pgsty.com my-docs
  2. 进入目录并预览:
    cd my-docs && hugo server
  3. 打开 http://localhost:1313/
端口用途
1313Hugo 开发服务器

折叠

类型后加 - 默认收起,加 + 默认展开;两者都渲染为原生 <details>,不加载 JavaScript。适用于完整输出、备选方案、背景说明这类不必默认展示的内容。

源码
> [!NOTE]- 为什么需要 Go?
> Hugo 通过 Go 的模块系统下载主题(`hugo mod get`)。用 submodule 或离线归档时可以不装 Go。

> [!TIP]+ 默认展开,但读者可以收起
> 收起状态不会被记住,刷新后回到默认。
为什么需要 Go?

Hugo 通过 Go 的模块系统下载主题(hugo mod get)。用 submodule 或离线归档时可以不装 Go。

默认展开,但读者可以收起

收起状态不会被记住,刷新后回到默认。

中性折叠块 DETAILS

[!DETAILS] 是没有语义颜色的折叠块:不加符号默认收起,[!DETAILS]+ 默认展开。用于冗长输出、完整配置文件等需要折叠的内容。

源码
> [!DETAILS] 完整的 `hugo version` 输出
> ```text
> hugo v0.164.0+extended+withdeploy darwin/arm64 BuildDate=2026-07-06T16:39:30Z VendorInfo=Homebrew
> ```
完整的 hugo version 输出
hugo v0.164.0+extended+withdeploy darwin/arm64 BuildDate=2026-07-06T16:39:30Z VendorInfo=Homebrew

自定义图标

块引用结束后的下一行写属性 {icon="fa-solid fa-xxx"}(一对 Font Awesome class),替换该类型的默认图标。属性行紧接块引用,中间不能有空行。

源码
> [!TIP] PostgreSQL 18 已支持
> 从 Pigsty v4 起默认安装 PostgreSQL 18。
{icon="fa-solid fa-database"}
PostgreSQL 18 已支持

从 Pigsty v4 起默认安装 PostgreSQL 18。

嵌套

提示块可以嵌套(每层多一个 >),也可以放在列表项或步骤中。建议最多嵌套一层。

源码
> [!WARNING] 升级前先备份
> 升级主题版本可能改变渲染结果。
>
> > [!TIP]- 怎么备份
> > `git tag pre-upgrade` 就够了——回滚只是 `git checkout pre-upgrade`
升级前先备份

升级主题版本可能改变渲染结果。

怎么备份

git tag pre-upgrade 就够了——回滚只是 git checkout pre-upgrade

未知类型与易错写法

未知的类型名不会导致构建失败,也不会丢失内容:该块渲染为普通块引用,[!TYPE] 标记原样可见。

源码
> [!NOTICE] 这不是合法类型
> 标记会保留在页面上提醒你。

[!NOTICE] 这不是合法类型

标记会保留在页面上提醒你。

其它常见问题:

  • 正文与标题合并:经过 Prettier 等格式化工具的文件,在标题行下保留一个空的 > 行,否则工具会把标题并入正文。
  • 属性行被格式化工具移动:把 {icon=…} 这类标记行放在 <!-- prettier-ignore-start --> / <!-- prettier-ignore-end --> 之间。
  • styleonclick 等属性导致构建失败:属性行只接受 iconclass(见下表)。

输出形态

输出呈现
HTML静态类型是 <div class="td-callout" role="note">;折叠类型是原生 <details> + <summary>
打印全部静态展开,折叠块带 data-td-callout-collapsible 标记
Markdown保留源码块引用(含 [!TYPE] 标记与标题)
RSS与打印相同,静态展开

提示块不加载脚本。

参数参考

标记行 > [!TYPE]± 标题

TYPE , 枚举 , default
NOTE TIP IMPORTANT WARNING CAUTION SUCCESS DANGER QUESTION EXAMPLE QUOTE DETAILS;大小写不敏感;未知值渲染为普通块引用
± , - / + / 无 , default
- 折叠默认收起,+ 折叠默认展开;DETAILS 不加符号即收起
标题 , 行内 Markdown , default类型的本地化名称
与标记同一行

属性行 {…}(块引用之后紧接的一行):

icon , Font Awesome class 对 , default类型默认图标
例如 fa-solid fa-databaseDETAILS 默认无图标
class , 空格分隔的 class , default
原样透传给站点 CSS

styleon* 与其它任何键都会让构建失败。

限制与常见问题

  • 不能自定义颜色:颜色由类型决定,需要新语义时选最接近的类型并自定义标题。
  • 折叠状态不持久化。
  • 提示块可以放在 {.steps} 列表项与 {{%/* steps */%}} 步骤中(见步骤),块引用的每一行都以 > 开头,缩进与列表项对齐。

4.2 - 图片

用普通 Markdown 图片语法写图,加一行属性就得到图注、尺寸、缩放、链接、编号与 Hugo 图片处理。

图片只有一种写法:Markdown 的 ![替代文字](来源 "标题")。独立成段的图片可以在下一行跟一行 {…} 属性,成为带图注的 figure、缩放候选、编号图或经 Hugo 处理的派生图。主题没有图片 shortcode。

最简例子

源码
![OINK 文档外壳:侧栏、正文与目录三栏](oink-shell.webp)
OINK 文档外壳:侧栏、正文与目录三栏

这张图与本页放在同一目录(页面包)中,主题读取它的固有尺寸并写入 width/height,页面加载时不发生跳版;所有图片懒加载。替代文字供屏幕阅读器与搜索引擎使用,应当始终填写;空 alt 表示装饰性图片,缩放会跳过它。

图片来源

来源按以下顺序解析,写法相同:

放法源码里怎么写适合
与页面同目录(页面包 index.md + 图片)![…](oink-shell.webp)只有这一页用的截图;随页面一起移动、翻译共用
全局资源 assets/images/…![…](images/logo/oink.webp)多页共用、还要做处理(缩放 / 裁切)的图
静态目录 static/images/…![…](/images/hero-light.webp)不需要处理的大图、下载物;主题拿不到尺寸时可以用 width/height
远程 URL![…](https://example.com/a.png)少用:构建期不会下载,也不能处理

相对路径先按页面资源、再按全局资源查找,都找不到时按静态路径原样输出;主题不检查静态路径与远程 URL 是否存在。只有要求处理(command=)的图找不到资源时才构建失败。

行内与块级

位于文字中间的是行内图片,渲染为一个 <img>,不能带属性;独立成段的是块级图片,可以带属性行。

源码
这一枚小图 ![文档外壳缩略图](oink-mini.webp) 夹在句子里,是行内图片。

![文档外壳缩略图](oink-mini.webp)
{width="100" height="64"}

这一枚小图 文档外壳缩略图 夹在句子里,是行内图片。

文档外壳缩略图

行内图片按自身尺寸显示(这里是 50×32)。没有固有尺寸的 SVG 行内插入时会被拉伸到容器宽度,SVG 应作为块级图片使用并给出 width/height

说明

块级图片依赖站点设置 markup.goldmark.parser.wrapStandAloneImageWithinParagraph: false(本站已配置;见配置总览)。缺少它时 Goldmark 会把独立图片包进 <p>,属性行也会被当作正文。

图注

属性行加 caption="…",图片渲染为 <figure> + <figcaption>。图注是纯文本,不解析 Markdown。

源码
![发布卡片:版本号、发布日期与资产按钮](release-note.webp)
{caption="发布卡片由 data/download 与页面的 release 记录生成"}
发布卡片:版本号、发布日期与资产按钮
发布卡片由 data/download 与页面的 release 记录生成

Markdown 里的 "标题" 保持原义(悬停提示),不会成为图注。

尺寸

width/height 是正整数,覆盖资源自身的尺寸:为静态或远程图片提供占位框以避免跳版,或把大图缩小显示(浏览器缩放,不改文件)。

源码
![OINK 首页插画(浅色)](/images/hero-light.webp)
{width="450" height="300" caption="static/images/ 里的 900×600 插画按一半显示"}
OINK 首页插画(浅色)
static/images/ 里的 900×600 插画按一半显示

处理型图片

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

源码
![文档外壳缩略图](oink-shell.webp)
{command="Fit" options="300x150" caption="Fit 300x150:按比例装进 300×150 的框"}

![文档外壳左半边](oink-shell.webp)
{command="Fill" options="300x150 Left" caption="Fill 300x150 Left:填满框,从左侧裁"}
文档外壳缩略图
Fit 300x150:按比例装进 300×150 的框
文档外壳左半边
Fill 300x150 Left:填满框,从左侧裁

静态路径、远程 URL 与 SVG 不能处理,对它们写 command 会构建失败。选项语法(锚点、质量、格式转换,如 300x150 webp q80)见 Hugo 图片处理

两种写法,用途不同:

  • 没有图注、图片本身是链接:用 Markdown 的链接包图 [![alt](src)](href)
  • 有图注的 figure 整体可点:属性行加 link="…"(必须同时有 captionnum)。
源码
[![点击进入亮点特性页](oink-shell.webp)](/zh/docs/about/features/)

![发布卡片](release-note.webp)
{caption="点击图片查看发布与下载页的说明" link="/zh/docs/write/releases/"}

点击进入亮点特性页

发布卡片
点击图片查看发布与下载页的说明

带链接的图不参与缩放。没有图注只写 link= 会构建失败,报错中提示改用 [![…](…)](…)

编号图

编号图用于书籍与长篇手册:属性行加 num,可选 #id。编号是作者书写的字符串(2-13.4),主题不自动计数;图注前加本地化的「图 2-1」前缀,#id 缺省为 fig-<num>。正文用普通链接 [图 2-1](#fig-2-1)xref shortcode 引用;全书图目录见书籍出版

源码
![发布卡片](release-note.webp)
{#fig-release num="2-1" caption="发布卡片:版本、日期与资产"}

见[图 2-1](#fig-release)。
发布卡片
图 2-1 发布卡片:版本、日期与资产

图 2-1

编号图可以同时是处理型图片(num + command),也可以带 link

缩放

图片缩放默认关闭。站点开启后,块级图片、figure、画廊中带 alt 的图成为可点击的按钮,在原生 <dialog> 中查看大图(Esc 关闭,焦点回到原处)。本页在 front matter 中开启了它,上面的图都可以点击。

hugo.yml
params:
  ui:
    image_zoom: true
某一页的 front matter:只关这一页
image_zoom: false

不缩放的图:行内图、alt 为空的装饰图、带链接的图、data-no-zoom 标记的图。运行时只在页面确有候选图时加载;打印 / Markdown / RSS 中没有对话框。

源码:装饰图不缩放
![](oink-shell.webp)
{width="150" height="75"}

深浅色图片

主题没有按深浅色切换图片的参数。需要两张图时,各写一个 class,在站点 CSS 中按 [data-bs-theme="dark"] 显示其一:

源码
![侧栏(浅色)](oink-shell.webp)
{class="only-light"}

![侧栏(深色)](oink-shell.webp)
{class="only-dark"}
assets/scss/_styles_project.scss
[data-bs-theme="dark"] .only-light,
:not([data-bs-theme="dark"]) .only-dark { display: none; }

class 由主题原样透传,供站点 CSS 使用。

输出形态

输出呈现
HTML行内 <img>;块级 <img class="td-image">;有图注 / 编号时 <figure class="td-figure"> + <figcaption>;缩放候选带 data-td-image-zoom
打印同 HTML,去掉缩放控件
Markdown原样输出 ![alt](src) 与属性行
RSS图片 src 改为绝对地址;无缩放

参数参考

属性行 {…}(块级图片之后紧接的一行):

caption , 纯文本 , default
有它就渲染成 figure;不解析 Markdown
#id , 标识符 , defaultnumfig-<num>
[A-Za-z][A-Za-z0-9_.:-]*;作为锚点与 Book 目标 ID
num , 字符串 , default
[0-9A-Za-z.-]+;注册为 Book 图目标,图注加「图 N.」前缀
width / height , 正整数 , default资源固有尺寸
覆盖尺寸;静态 / 远程图靠它避免跳版
command , 枚举 , default
Fit Resize Fill Crop;必须与 options 同给;仅页面 / 全局资源
options , 字符串 , default
Hugo 图片处理选项,如 600x300300x150 Left800x webp q80
link , URL , default
把 figure 包进链接;需要 captionnum;带链接的图不缩放
class , class 列表 , default
透传给站点 CSS
data-* / aria-* , 字符串 , default
透传

styleon*alttitlesrc 与其它任何键出现在属性行都会构建失败(alt、title、src 属于 Markdown 图片本身)。

限制与常见问题

  • 图注不含 Markdown:所有公开字符串参数都是纯文本;富文本说明写在图片下方的段落中。
  • title 不是图注:![a](b "c")c 是悬停提示。
  • 处理型图片只对资源生效:static/ 中的图需要处理时移到页面包或 assets/
  • 构建期不下载远程图片。
  • 缩放不支持拖拽、平移、上一张 / 下一张;一组相关图片使用画廊

4.3 - 代码块

普通 Markdown 围栏加一行属性,就得到文件名标题、精确复制、行号、高亮、换行、折叠与可链接的行。

代码块是普通的 Markdown 围栏,高亮由 Hugo 内置的 Chroma 在构建期完成,浏览器里没有高亮器。用于命令、配置片段与源码:围栏信息行上的 {…} 属性决定标题栏、复制行为、行号与行锚点。图示类围栏(mermaidechartsfiletree 等)不走这条路径,它们各有渲染钩子。

最简例子

源码
```sql
SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC;
```
SELECT datname, numbackends FROM pg_stat_database ORDER BY numbackends DESC;

没有属性的围栏同样有完整外壳与复制按钮。无标题栏时不渲染空白横条,复制按钮浮在右上角,鼠标悬停或焦点进入块内时出现,触屏设备上始终可见。外壳不显示语言名,lexer 名字只写入 data-language,供样式表与测试使用。

语言标记就是 Chroma 的 lexer 名。diff 围栏用 Chroma 的增删行样式呈现补丁,不需要额外组件:

源码
```diff {title="hugo.yml 的改动"}
 params:
   ui:
-    sidebar_menu_compact: true
+    sidebar_menu_compact: false
     sidebar_menu_foldable: true
```
hugo.yml 的改动
 params:
   ui:
-    sidebar_menu_compact: true
+    sidebar_menu_compact: false
     sidebar_menu_foldable: true

文件名标题

title 给块加一条可见标题栏,通常写文件名或路径。它同时成为这个块的无障碍名称。

源码
```yaml {title="hugo.yml"}
markup:
  goldmark:
    parser:
      attribute:
        block: true
    renderer:
      unsafe: true
```
hugo.yml
markup:
  goldmark:
    parser:
      attribute:
        block: true
    renderer:
      unsafe: true

filenametitle 的历史别名,两个一起写会构建失败。

行号、起始行与高亮

lineNosinline(行号与代码同一列)或 table(行号独立成列,可单独选中不被复制)。lineNoStart 改显示的起始编号。hl_lines 标记要强调的行,计数按围栏内的源码行,从 1 开始,与 lineNoStart 无关。

源码
```ini {title="postgresql.conf" lineNos="inline" lineNoStart=120 hl_lines="2 4-5"}
shared_buffers = 8GB
max_connections = 200
work_mem = 64MB
wal_level = replica
max_wal_senders = 10
```
postgresql.conf
120shared_buffers = 8GB
121max_connections = 200
122work_mem = 64MB
123wal_level = replica
124max_wal_senders = 10

lineNos="table" 把行号放进独立的一列(两种模式下复制按钮都会剔除行号):

源码
```bash {title="部署三条命令" lineNos="table"}
./configure -c rich
./install.yml
pig ext install pg_duckdb
```
部署三条命令
1
2
3
./configure -c rich
./install.yml
pig ext install pg_duckdb

tabWidth 决定制表符展开成几个空格,与 style 一样原样转交 Chroma。本站使用基于 class 的 Chroma 调色板(深浅色各一套),style 只在把 Hugo 切回内联样式模式时才生效。

长行换行

wrap=true 只改变显示:源码不变,复制出来的文本也不变。不加它时长行横向滚动。

源码
```text {title="config/artifacts.env" wrap=true}
ARTIFACT_URL=https://repo.pigsty.io/pkg/infra/v3.6.0/infra-pkg-v3.6.0.el9.x86_64.tgz
CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2
```
config/artifacts.env
ARTIFACT_URL=https://repo.pigsty.io/pkg/infra/v3.6.0/infra-pkg-v3.6.0.el9.x86_64.tgz
CHECKSUM=sha256:6d3dce4f7acb18f586469adcb80ab35f3e859f9837786e151cfbc2b3c0f587b2

wrap=true 与表格行号不能共存:行号列与代码列是两个表格单元格,换行后会错位。写在一起构建失败,报错提示改用 lineNos="inline" 或去掉换行。

折叠长代码

collapse=N 让块初始只显示 N 行,底部给一个「显示全部 N 行」按钮。服务器输出完整代码,折叠是浏览器量出第 N 行位置后的视觉裁切:没有 JavaScript 时、读屏器中、打印时代码都是完整的。

源码
```yaml {title="hugo.yml" collapse=8}
baseURL: https://oink.pgsty.com/
title: OINK
defaultContentLanguage: en
languages:
  en:
    languageName: English
    weight: 1
  zh:
    languageName: 简体中文
    weight: 2
params:
  offline_search: true
  ui:
    sidebar_menu_foldable: true
```
hugo.yml
baseURL: https://oink.pgsty.com/
title: OINK
defaultContentLanguage: en
languages:
  en:
    languageName: English
    weight: 1
  zh:
    languageName: 简体中文
    weight: 2
params:
  offline_search: true
  ui:
    sidebar_menu_foldable: true

行数不超过 collapse 时按钮不出现。换行与折叠可以一起用:折叠测量的是第 N 个源码行节点的底边,换行的行不会被截断。

复制内容

默认复制整块源码。终端会话(consoleshell-session 两个 lexer)默认只复制命令:带提示符的行留下,提示符本身与输出行去掉。下面这个块复制出来只有两条命令,没有 $ 也没有输出。

源码
```console
$ pig ext list duckdb
name       version  category
pg_duckdb  1.0.0    OLAP
$ pig ext install pg_duckdb
INFO installing pg_duckdb
```
$ pig ext list duckdb
name       version  category
pg_duckdb  1.0.0    OLAP
$ pig ext install pg_duckdb
INFO installing pg_duckdb

要连提示符与输出一起复制就写 copy="all"。把 copy="command" 用在 bashsh 之类普通 lexer 上会构建失败,因为它们分不出提示符、命令与输出。多行命令请在续行里写出续行提示符(通常是 >),否则那一行会被当成输出而排除。

会话 lexer 的块里一行提示符都没有时,复制按钮报失败:图标转为错误状态,控制台留一条错误,剪贴板不变。它不会退化成复制全文。

copy=false 关掉这一块的复制按钮,用于不应被抄走的反例片段:

源码
```yaml {title="反例:属性行离开了它的块" copy=false}
params:
  ui:
    image_zoom: true   # 错:image_zoom 是一张表,不是布尔值
```
反例:属性行离开了它的块
params:
  ui:
    image_zoom: true   # 错:image_zoom 是一张表,不是布尔值

整站关掉复制用 params.ui.code_copy: false,它优先于每个块自己写的 copy(见配置总览)。复制按钮只有图标,成功与失败会换图标并播报本地化状态;复制内容保留缩进、空行与 Unicode,去掉行号,末尾只留一个换行。

把「看第 3 行」做成链接需要两步:给围栏一个明确的 id,再打开 anchorLineNos=true。行号随即变成锚点链接,锚点是 #<id>-<行号>

源码
```sql {id="ex-explain" title="explain.sql" lineNos="table" anchorLineNos=true}
EXPLAIN (ANALYZE, BUFFERS)
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE n_live_tup > 1000
ORDER BY n_live_tup DESC;
```

跳到 [第 4 行](#ex-explain-4)。
explain.sql
1
2
3
4
5
EXPLAIN (ANALYZE, BUFFERS)
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE n_live_tup > 1000
ORDER BY n_live_tup DESC;

跳到 第 4 行

不写 id 时主题也会生成一个页面内唯一的 ID,但它依赖围栏在页面里的顺序,前面插入一个新围栏就会变。只有作者书写的 id 才是永久链接。ID 不能含空白与控制字符,也不能与页面上其它块的 viewport、标签、面板、标题、行锚点 ID 重复,重复即构建失败。

编号例

写书或长手册时给代码片段编号:numcaption,这个围栏就成了一条 Book「示例」目标,可以被 xref 引用,也会进入全书的示例目录。编号由作者书写,主题不自动计数;id 默认是 eg-<num>

源码
```sql {num="4-1" caption="按表统计膨胀率" #eg-bloat}
SELECT schemaname, relname, n_dead_tup, n_live_tup
FROM pg_stat_user_tables
WHERE n_dead_tup > n_live_tup * 0.2;
```

参见 {{< xref eg="4-1" anchor="eg-bloat" >}}。
示例 4-1 按表统计膨胀率
SELECT schemaname, relname, n_dead_tup, n_live_tup
FROM pg_stat_user_tables
WHERE n_dead_tup > n_live_tup * 0.2;

参见 示例 4-1

numcaption 必须成对出现,只写一个会构建失败;num 与标签页属性 tab 互斥。图、表、公式的编号写法与索引见书籍出版

一组围栏做成标签页

连续几个带 tab 的围栏会在浏览器里合成一个标签页集,第一个围栏上的 group 让它可分享、可同步、可记住选择。

源码
```bash {tab="Homebrew" group="oink-install" value="brew"}
brew install hugo
```
```bash {tab="APT" value="apt"}
sudo apt install hugo
```
Homebrew
brew install hugo
APT
sudo apt install hugo

完整规则(分组语法、URL hash、跨组同步、正文标签页)在标签页

易错写法

  • 在文档里展示 shortcode:围栏不阻止 Hugo 解析,写在代码块里的 {{< tabs >}} 仍会执行。要让它原样显示,在两侧定界符的内侧各加一对注释符号,写成 {{</* tabs */>}},百分号形式对应 {{%/* steps */%}}。本页每一处展示 shortcode 的地方都是这么写的。
  • 围栏里套围栏:外层用四个反引号、内层三个,本页每一段「源码」都是这么写的;内层还有围栏时外层再加一个。
  • 属性写在信息行上:围栏的属性跟在开栏那一行的语言后面,表格与图片的属性才写在块的下一行。写到下一行会变成正文里一段可见的花括号。
  • 未知属性会失败,不会被忽略,错误信息里列出允许的名字。stylesrcdocon* 被拒绝;data-td-code* 前缀以及 data-languagedata-line-countdata-collapse-lines 是主题的保留名,写上去同样构建失败。
  • 列表项里的围栏:缩进要与列表项内容对齐(1. 之后恒定三个空格),否则围栏会脱离列表。

输出形态

输出呈现
HTML<div class="td-code"> 外壳 + Chroma 的 .highlight/.chroma;复制、折叠按钮在服务器输出里是 hidden,脚本确认可用后才显示
打印完整代码,去掉复制、折叠、渐隐;长块允许跨页;标题栏保留
Markdown原样输出源码围栏,连 {…} 属性一起
RSS静态代码块,无按钮

没有复制或折叠控件的页面不加载 code-block.js;打印、Markdown 与 RSS 输出不加载。

参数参考

开栏那一行、语言之后的 {…} 里,OINK 自己的属性:

title , 非空字符串 , default
可见标题栏(通常是文件名),同时是无障碍名称
filename , 非空字符串 , default
title 的历史别名;两者同时出现构建失败
copy , all command true false , default会话 lexer 为 command,其余为 all
true 等价于 allcommand 只允许 console/shell-session
wrap , 布尔 , defaultfalse
视觉换行,不改源码;与表格行号互斥
collapse , 正整数 , default
初始显示的最大行数;行数不足时不生效
label , 非空字符串 , default由标题派生
无障碍名称,不显示在页面上;与 aria-label 互斥
id , 非空 token , default自动生成
稳定的块 ID 与行锚点前缀;不能含空白
tab , 非空字符串 , default
标签名,见标签页;与 num 互斥
group , ^[a-z][a-z0-9_-]*$ , default
写在一组的第一个围栏上,启用 hash / 同步 / 持久化;需要 tab
value , ^[a-z0-9][a-z0-9_-]*$ , default
分组内每个围栏必填,无分组时禁止;需要 tab
num , [0-9A-Za-z.-]+ , default
编号示例(Book eg);必须与 caption 同时出现
caption , 纯文本 , default
编号示例的说明;必须与 num 同时出现
class , class 列表 , default
追加到 .td-code 根元素
data-* / aria-* / role , 字符串 , default
透传到根元素

titlefilenamelabel 已经为块生成了无障碍名称与 role="group"。它们中的任意一个与 aria-labelaria-labelledbyrole 同时出现都会构建失败;这三个属性只在块没有标题也没有 label 时可以透传。

同一行还能写 Chroma 选项,主题原样转交 Hugo:

lineNos , false inline table , defaultfalse
行号形态;tablewrap=true 互斥
lineNoStart , 正整数 , default1
显示的起始行号,不影响 hl_lines 的计数
hl_lines , 行号与区间 , default
"2 4-5",按围栏内源码行计数
anchorLineNos , 布尔 , defaultfalse
行号变成锚点链接,前缀取自块的 id
tabWidth , 正整数 , defaultHugo 默认
制表符展开的空格数

限制与常见问题

  • 不换高亮器:没有 Shiki、Twoslash、浏览器端高亮,也没有可执行的代码演练场。补丁用 diff 围栏,Chroma 的 .gi/.gd 就是增删行的样式。
  • copy="command" 只认会话 lexer:写在别的语言上是构建错误,不会退化成复制全部。
  • 自动生成的 ID 不是永久链接:要发链接就写 id
  • mermaidmathchemmarkmapplantumlechartsinfographicchecksumsfiletreegallery 不是代码块:它们有各自的渲染钩子,不套这层外壳,也没有复制按钮。
  • 标签页 — 相邻围栏合成标签页的完整规则
  • 引用 — 把仓库里的真实文件当代码块插进来
  • 书籍出版 — 编号示例、交叉引用与示例目录
  • 打印支持 — 长代码在打印里的形态

4.4 - 标签页

给相邻的围栏或表格加一个 {tab=} 属性就得到标签页;加上 group 之后可分享链接、跨组同步、记住读者的选择。

标签页并列等价的几种写法:包管理器、发行版、YAML / TOML / JSON、环境变量与配置项。有先后的步骤、互不相关的内容不适合标签页,读者一次只看见其中一个。

原生形态是给相邻的块加 tab 属性。正文(多个段落、列表、提示块)要做成标签页时才用 tabs/tab shortcode。两种形态共用一个运行时、一套 DOM 与一样的键盘行为。

最简例子

连着写两个带 tab 的围栏,中间只隔空行。

源码
```bash {tab="Homebrew"}
brew install hugo
```
```bash {tab="Debian / Ubuntu"}
sudo apt install hugo
```
Homebrew
brew install hugo
Debian / Ubuntu
sudo apt install hugo

服务器输出两个带标题的代码块,没有面板被隐藏;页面加载后运行时把相邻的同类块重组为标签页。在 GitHub 上、打印时、关闭 JavaScript 时,读者看到的是连续两块完整内容。

分组:链接、同步与记忆

只在第一个块上写 group,这一组就有了公开的 URL hash #<group>-<value>、页内同步与浏览器持久化;分组内的每个块都要写 value

源码
```bash {tab="npm" group="pkgmgr" value="npm"}
npm create hugo-site@latest
```
```bash {tab="pnpm" value="pnpm"}
pnpm create hugo-site
```
```bash {tab="Yarn" value="yarn"}
yarn create hugo-site
```
npm
npm create hugo-site@latest
pnpm
pnpm create hugo-site
Yarn
yarn create hugo-site

value 是机器值(^[a-z0-9][a-z0-9_-]*$),tab 是给人看的标签名,两者互不相干。上面这组的 pnpm 面板对应的 hash 是 #pkgmgr-pnpm,带这个 hash 访问本页会直接选中它。

同组联动

下面这组用了同一个 group="pkgmgr"。在上面那组切换包管理器,这组会跟着切;在这组切换,上面那组也跟着切。选择写入 localStoragetd-tabs:v1:pkgmgr 键,在其它页面同组的标签页上仍然生效。

源码
```bash {tab="npm" group="pkgmgr" value="npm"}
npm run build
```
```bash {tab="pnpm" value="pnpm"}
pnpm build
```
npm
npm run build
pnpm
pnpm build

这组没有 yarn 面板。同步时缺哪个值就保持不动,不会出现「一组没有选中项」的状态。初始选哪个的优先级是:URL hash,存储的值,shortcode 的 default 或第一个块,第一个标签。带 hash 打开页面只切换,不覆盖读者已经存下的偏好。

表格也能做标签页

同一套属性写在表格的属性行上,连着的表格就组成一组标签页。

源码
| 参数 | 默认值 |
| --- | --- |
| `shared_buffers` | 25% RAM |
| `max_connections` | 100 |
{tab="PostgreSQL 18" group="pgver" value="pg18"}

| 参数 | 默认值 |
| --- | --- |
| `shared_buffers` | 128MB |
| `max_connections` | 100 |
{tab="PostgreSQL 13" value="pg13"}
PostgreSQL 18
参数默认值
shared_buffers25% RAM
max_connections100
PostgreSQL 13
参数默认值
shared_buffers128MB
max_connections100

围栏与表格是两种块类型,相邻也不会合成同一组:一组标签页里只能全是围栏或全是表格。两者混排使用下面的 shortcode 形态。

标签名与文件名共存

围栏的 tabtitle 可以一起写:标签名进标签栏,文件名标题栏留在面板里。

源码
```yaml {tab="YAML" title="hugo.yml" group="conffmt" value="yaml"}
params:
  ui:
    sidebar_menu_foldable: true
```
```toml {tab="TOML" title="hugo.toml" value="toml"}
[params.ui]
sidebar_menu_foldable = true
```
YAML
hugo.yml
params:
  ui:
    sidebar_menu_foldable: true
TOML
hugo.toml
[params.ui]
sidebar_menu_foldable = true

单独一个块只是带标题的块

一个块要凑够两个相邻的同类块才会变成标签页。落单的块保留标题,不会变成只有一个标签的标签栏。

源码
```ini {tab="只有这一块"}
listen_addresses = '*'
```
只有这一块
listen_addresses = '*'

块之间只允许空行。三种情况会断开一组:中间隔了正文(段落、标题、列表都算);中间有一条 HTML 注释,<!-- prettier-ignore-end --> 是常见的一处;后一个块自己写了 group,一组里只有第一个块可以带 group

正文标签页

面板里要放段落、列表、提示块或多个块时,用 tabs/tab shortcode。正文是完整的 Markdown。

源码
{{< tabs group="deploy" default="pages" label="部署方式" >}}
{{< tab label="GitHub Pages" value="pages" >}}
仓库自带 `.github/workflows/`,推到 `main` 就会构建并发布。

> [!NOTE]
> `baseURL` 要写成仓库的 Pages 地址。
{{< /tab >}}
{{< tab label="Cloudflare Pages" value="cloudflare" >}}
在 Cloudflare 控制台里连接仓库,构建命令:

```bash
hugo --gc --minify
```
{{< /tab >}}
{{< /tabs >}}

仓库自带 .github/workflows/,推到 main 就会构建并发布。

说明

baseURL 要写成仓库的 Pages 地址。

在 Cloudflare 控制台里连接仓库,构建命令:

hugo --gc --minify

default 指定初始选中的面板,它必须是某个子项的 value,并且需要 group。没有 group 时不能写 value,主题自动生成 tab1tab2 等值,这组标签页只在本地切换,不动 URL 也不写存储。shortcode 形态比属性形态严格:写错的地方在构建期就报出来,不留到浏览器里。

输出形态

输出呈现
HTML<div class="td-tabs"> + role="tablist" 的按钮与面板;运行时接管前所有面板都可见
打印连续的带标题静态分节,没有标签栏
Markdown围栏形态保持源码围栏(含 {tab=} 属性);shortcode 形态输出 **标签名** 加正文
RSS与打印相同,堆叠的带标题分节

只有用到标签页的页面才加载 tabs.js;打印、Markdown 与 RSS 输出不加载。

参数参考

写在围栏信息行或表格属性行上的属性:

tab , 非空字符串 , default
可见标签名;单独出现时就是这个块的标题
group , ^[a-z][a-z0-9_-]*$ , default
写在一组的第一个块上,启用 hash、页内同步与持久化;需要 tab
value , ^[a-z0-9][a-z0-9_-]*$ , default
分组内每个块必填,无分组时禁止;需要 tab

tabs shortcode:

group , ^[a-z][a-z0-9_-]*$ , default
同上,启用 hash、同步与持久化
default , 某个子项的 value , default第一个子项
初始选中的面板;需要 group
label , 纯文本 , default本地化的「选项卡」
标签栏的无障碍名称,不显示在页面上

tab shortcode:

label , 纯文本 , required
可见标签名
value , ^[a-z0-9][a-z0-9_-]*$ , required
无分组时禁止书写,自动生成 tab1tab2 等值

行为约定:面板 ID 在分组里是 <group>-<value>,同一页出现第二组同名 group 时后续各组的 ID 加 -2-3 后缀(深链目标始终是第一组),未分组时由主题生成;存储键是 td-tabs:v1:<group>;用户点击或按键会用 replaceState 更新 hash 并写入存储,带 hash 访问只切换不写入。键盘上左右方向键(感知 RTL)与 Home/End 移动并激活标签,焦点停留在标签上。

限制与常见问题

  • 构建失败的写法:属性形态里 valuegroupgroupvaluetabtab 与编号属性 num 同时出现;shortcode 形态里一组内 value 重复、tabs 没有 tab 子项、子项之间夹着正文、default 不匹配任何子项的 value
  • 属性形态的分组错误不中断构建,只在浏览器控制台留警告:分组内漏写 value 时整组丢掉 group,退化成只在本地切换的标签页,hash、同步与持久化都没有;value 重复时整组跳过,那几个块保持为各自带标题的块。
  • 围栏与表格不会混成一组,正文与代码混排请用 shortcode 形态。
  • 标签页不是折叠块。只想收起长输出用 > [!DETAILS](见提示块)。
  • 同名 group 是全站共享的:读者在 A 页选了 pnpm,B 页同组的标签页也会是 pnpm。这是它的用途,也意味着 group 名要按含义取,不用 tabs1 这种。
  • 代码块 — 围栏的其余属性(标题、复制、行号、折叠)
  • 表格 — 表格属性行的其余取值
  • 提示块 — 折叠而不是并列时用它
  • 步骤 — 步骤里可以放标签页

4.5 - 表格

普通 GFM 表格加一行属性,就得到标题、兼容矩阵、参数表、编号表或标签页;宽表格自己横向滚动,不撑宽页面。

表格是普通的 GFM 管道表格。主题的表格渲染钩子把每张表包进一块可横向滚动的区域,表格下面那一行 {…} 属性决定它是哪一种表:带标题的表、兼容矩阵、参数表、编号表或标签页。合并单元格、排序与筛选不在能力范围内,需要它们的场景请改换呈现方式。

最简例子

不写属性行就是一张普通表。对齐方式照旧来自分隔行,表头单元格是 th scope="col"

源码
| 组件 | 端口 | 用途 |
| --- | :---: | --- |
| PostgreSQL | 5432 | 数据库 |
| Pgbouncer | 6432 | 连接池 |
| Patroni | 8008 | 高可用编排 |
组件端口用途
PostgreSQL5432数据库
Pgbouncer6432连接池
Patroni8008高可用编排

宽表格自己滚动

列太多的表不会把页面撑宽,它在自己的区域里横向滚动。这块区域可以用键盘聚焦: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 |
集群角色版本状态延迟连接数大小备份
pg-metaprimary18.1running4212 GB2026-08-17
pg-testreplica18.1streaming12 ms812 GB2026-08-17

表格标题

{caption="…"} 加一个可见的 <caption>,纯文本,不给表编号。

源码
| 条目 | 取值 |
| --- | --- |
| 主题版本 | v0.6.0 |
| Hugo 下限 | 0.160.1 Extended |
| 许可证 | Apache-2.0 |
{caption="本站当前使用的主题事实"}
本站当前使用的主题事实
条目取值
主题版本v0.6.0
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 | ✅ | ✅ | ✅ | ✅ | ❌ |
{.matrix}
OS / PGPG18PG17PG16PG15PG14
EL 9
EL 8
Debian 13
Ubuntu 24.04

用整个画布

{.full-width} 让表格越出正文栏宽,占满文章可用的宽度。适合列多但每列都短的表。

源码
| 语言 | 代码 | 侧栏 | 搜索 | 目录 | 打印 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| 简体中文 | `zh` | ✅ | ✅ | ✅ | ✅ | 已审校 |
| English | `en` | ✅ | ✅ | ✅ | ✅ | 已审校 |
{.full-width}
语言代码侧栏搜索目录打印状态
简体中文zh已审校
Englishen已审校

参数表

{.fields} 把表格变成定义列表:第一列是名称,最后一列是说明,中间列是元数据。它是记录配置项、命令参数、API 字段的形态,写法见参数表

源码
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `offline_search` | boolean | `false` | 构建本地搜索索引 |
| `page_width` | string | `normal` | 正文栏宽度 |
{.fields meta="type default"}
offline_search , boolean , defaultfalse
构建本地搜索索引
page_width , string , defaultnormal
正文栏宽度

编号表

写书或长手册时给表编号:num 加可选的 #idcaption。表格会被包进一个带本地化「表 N.」标签的 <figure>,并注册成 Book 目标,可以被 xref 引用、进入全书表格目录。编号由作者书写,主题不自动计数;id 缺省是 tbl-<num>

源码
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
| --- | --- | --- | --- |
| 读已提交 | 否 | 是 | 是 |
| 可重复读 | 否 | 否 | 是 |
| 可串行化 | 否 | 否 | 否 |
{#tbl-iso num="9-1" caption="PostgreSQL 各隔离级别允许的异象"}

参见 {{< xref tbl="9-1" anchor="tbl-iso" >}}。
隔离级别脏读不可重复读幻读
读已提交
可重复读
可串行化
表 9-1 PostgreSQL 各隔离级别允许的异象

参见 表 9-1

表格做成标签页

连着的表格加 {tab="…"} 就组成一组标签页,规则与相邻围栏一致:第一张表上的 group 启用 hash、同步与持久化,此后每张表都要 value。完整规则见标签页

源码
| 目录 | 内容 |
| --- | --- |
| `content/` | 页面 |
| `data/` | 首页与发布数据 |
{tab="内容" group="repo-layout" value="content"}

| 目录 | 内容 |
| --- | --- |
| `assets/` | SCSS 与图片资源 |
| `static/` | 原样拷贝的文件 |
{tab="资源" value="assets"}
内容
目录内容
content/页面
data/首页与发布数据
资源
目录内容
assets/SCSS 与图片资源
static/原样拷贝的文件

输出形态

输出呈现
HTML<div class="td-table-scroll"> 可聚焦滚动区 + <table>;矩阵与全宽是这个包装器上的修饰 class
打印完整表格按页宽排版;包装器仍在,但标成 td-table-scroll--static,不再是可聚焦视口
Markdown原样输出源码表格与属性行
RSS完整静态表格

表格不加载任何脚本。

参数参考

表格下一行的属性行:

.full-width , 标记 , default
越出正文栏宽,占满文章画布
.matrix , 标记 , default
第一列作行表头,表头与首列吸附,其余单元格居中
.fields , 标记 , default
渲染成定义列表,见参数表
caption , 纯文本 , default
可见表格标题;在 .fields 上是列表的标签
meta , 角色列表 , default
命名 .fields 中间列的语义,取值 type required default -;必须与 .fields 同用
#id , 标识符 , defaultnum 时为 tbl-<num>
[A-Za-z][A-Za-z0-9_.:-]*;写在 <table>(编号表则写在 <figure>)上
num , 字符串 , default
[0-9A-Za-z.-]+;注册为 Book 表目标,标题前加「表 N.」
tab / group / value , 标签页 , default
相邻表格组成标签页
class , class 列表 , default
站点 CSS 用,原样留在 <table>
data-* / aria-* , 字符串 , default
透传

styleon* 与任何其它键都会让构建失败。

限制与常见问题

  • 互斥规则:.fields 不能和 .matrix.full-widthnum 一起用;numtab 互斥;group/value 需要 tabmeta 需要 .fields
  • 属性行必须紧贴表格:中间空一行,它就变成正文里一段可见的花括号。Markdown 格式化工具常移动这一行,把它包进 <!-- prettier-ignore-start --> / <!-- prettier-ignore-end -->
  • 没有合并单元格、没有排序、没有筛选:GFM 管道表格能表达的就是全部。需要合并表头的复杂表请拆成两张表或改成一张矩阵。
  • 单元格里放不下块内容:多段说明、列表、围栏要用 fields/field shortcode。
  • .matrix 的居中由 CSS 实现:分隔行里写了对齐就以分隔行为准。

4.6 - 参数表

用一张普通表格加 {.fields} 记录配置项、命令参数与 API 字段:名称、类型、默认值、说明各就各位,窄屏不挤,每条都能单独链接。

参数表(Fields)把「一串具名值 + 元数据 + 说明」渲染成响应式定义列表:名称独占一行,类型、是否必填、默认值是名称旁边的小字,说明另起一行,每一条自带锚点。用于配置项、命令参数与 API 字段。要按同一批列横向比较很多行时用普通表格,内容是操作顺序时用步骤。

写法有两种:普通表格加 {.fields}(默认选它),以及 fields/field shortcode(说明需要多个段落、列表或代码块时才用)。两种形态渲染出相同的条目。

最简例子

一张至少两列的管道表格,下一行写 {.fields}。第一列是名称,最后一列是说明,中间每一列都是元数据,标签就是表头文字本身。

源码
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `offline_search` | boolean | `false` | 构建本地搜索索引并启用命令面板 |
| `offline_search_max_results` | integer | `10` | 搜索结果条数上限 |
| `page_width` | string | `normal` | 正文栏宽度,可选 `narrow` `normal` `wide` |
{.fields}
offline_search , 类型boolean , 默认值false
构建本地搜索索引并启用命令面板
offline_search_max_results , 类型integer , 默认值10
搜索结果条数上限
page_width , 类型string , 默认值normal
正文栏宽度,可选 narrow normal wide

这里的元数据显示成「表头: 值」。主题不推断表头的含义,类型 只是一个标签;要让它变成标准芯片见下一节。单元格接受行内 Markdown(代码、强调、链接),空的中间单元格省略。

语义列 meta=

meta 按顺序说明每一个中间列扮演什么角色:type(类型)、required(必填)、default(默认值),或者 -(保留表头当标签)。有了它,表格形态渲染出的芯片与 shortcode 形态一致。

源码
| 参数 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `baseURL` | string | 是 | | 站点地址,含子路径 |
| `title` | string | 是 | | 站点名,出现在顶栏与页签 |
| `defaultContentLanguage` | string | | `en` | 默认语言,决定无前缀路径属于哪种语言 |
{.fields meta="type required default"}
baseURL , string , required
站点地址,含子路径
title , string , required
站点名,出现在顶栏与页签
defaultContentLanguage , string , defaulten
默认语言,决定无前缀路径属于哪种语言

规则:

  • meta 必须为每一个中间列写一个角色,个数等于总列数减二;写多写少都构建失败。
  • required 列是「非空即真」:单元格里写「是」「yes」「✔」都一样,渲染出来的是不翻译的 required 芯片;留空就不显示。
  • typedefault 单元格如果本身没有行内标记,会自动套上代码格式,与 shortcode 形态对齐。
  • 三种语义芯片按 typerequireddefault 的顺序显示,与列的顺序无关;- 列跟在后面,按列顺序排。

- 可以和语义角色混用,用来保留一列自定义标签:

源码
| 环境变量 | 类型 | 作用域 | 说明 |
| --- | --- | --- | --- |
| `HUGO_MODULE_WORKSPACE` | string | 构建 | 指向 `go.work`,让主题从本地 checkout 解析 |
| `HUGO_ENV` | string | 构建 | 设为 `production` 时启用压缩与指纹 |
{.fields meta="type -"}
HUGO_MODULE_WORKSPACE , string , 作用域构建
指向 go.work,让主题从本地 checkout 解析
HUGO_ENV , string , 作用域构建
设为 production 时启用压缩与指纹

标签与容器 ID

caption 给整张表加一个可见标签(同时是无障碍名称),id 命名外层容器,方便从别处链接过来或写站点 CSS。

源码
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `enable` | boolean | `false` | 打开图片缩放 |
| `selector` | string | `.td-content` | 扫描候选图片的根选择器 |
{.fields caption="params.ui.image_zoom" id="zoom-params" meta="type default"}

params.ui.image_zoom

enable , boolean , defaultfalse
打开图片缩放
selector , string , default.td-content
扫描候选图片的根选择器

每一条都能单独链接

每个条目获得一个 field-<名称> 形式的锚点,鼠标移上去时名称右边出现自链接图标。上面第一张表里的 page_width 就是 #field-page_width,回答问题时可以把这一行的链接单独发出去。

同一页里重名的字段按 -2-3 顺延,规则与 Goldmark 处理重名标题一致。锚点只在 HTML 里生成:打印和 RSS 会把很多页拼成一个文档,页内锚点在那里会冲突。

shortcode 形态

说明需要多个段落、列表或代码块时,表格单元格装不下,改用 fields/field

源码
{{< fields label="pig 命令常用参数" >}}
{{< field name="--config" type="path" required=true >}}
配置文件路径。相对路径按当前工作目录解析。

如果同时设置了 `PIG_CONFIG` 环境变量,命令行参数优先。
{{< /field >}}
{{< field name="--log-level" type="string" default="info" >}}
日志级别,从低到高:

- `debug`:打印每一次远程调用
- `info`:默认值
- `error`:只在失败时输出
{{< /field >}}
{{< field name="--dry-run" type="boolean" default=false >}}
只打印将要执行的动作,不改任何东西:

```bash
pig ext install pg_duckdb --dry-run
```
{{< /field >}}
{{< /fields >}}

pig 命令常用参数

--config , path , required

配置文件路径。相对路径按当前工作目录解析。

如果同时设置了 PIG_CONFIG 环境变量,命令行参数优先。

--log-level , string , defaultinfo

日志级别,从低到高:

  • debug:打印每一次远程调用
  • info:默认值
  • error:只在失败时输出
--dry-run , boolean , defaultfalse

只打印将要执行的动作,不改任何东西:

pig ext install pg_duckdb --dry-run

required=truedefault=false 是布尔值,不加引号。default 接受任何标量:default=0default="" 都会如实显示(空字符串显示成 ""),不写 default 就不显示这一项。每个 field 必须有非空正文,并且必须是 fields 的直接子项。

两种形态的选择

情况用法
每条说明一句话,能放进表格单元格表格 + {.fields}
说明要分段、带列表或代码块fields/field shortcode
读者需要按同一批列横向比较很多行用普通表格,不转成参数表
内容是操作顺序步骤

表格形态在 GitHub 上仍然是一张可读的表,OINK 的 Markdown 输出也保持表格原样,这是默认选它的理由。

输出形态

输出呈现
HTML<div class="td-fields"> + 语义 <dl>;条目带 #field-<名称> 锚点与自链接
打印完整定义列表,不带条目锚点
Markdown表格形态保留源码表格;shortcode 形态输出「名称 — 类型;required;default: 值」加缩进说明的项目符号列表
RSS完整静态 <dl>,不带条目锚点

不加载任何脚本。

参数参考

表格属性行(写在表格下一行):

.fields , 标记 , default
必需;把表格渲染成参数表
meta , 角色列表 , default
空格分隔,取值 type required default -;个数等于中间列数;语义角色不可重复
caption , 纯文本 , default
可见标签,同时是列表的无障碍名称
id , 标识符 , default
外层容器的 ID
class , class 列表 , default
透传给站点 CSS
data-* / aria-* , 字符串 , default
透传

fields shortcode:

label , 非空字符串 , required
可见标签,作用同表格的 caption
id , 标识符 , required
外层容器 ID;不能含空白、引号、<>&
class / data-* / aria-* , 字符串 , required
与表格属性行同一套策略

field shortcode:

name , 非空字符串 , required
字段名
type , 非空字符串 , required
类型标签,如 boolean string[] duration
required , 布尔 , required
true 时显示不翻译的 required 芯片,默认 false
default , 标量 , required
字符串 / 布尔 / 整数 / 浮点;false0"" 都会显示

限制与常见问题

  • 第一列必须非空,且在同一张表内唯一:重名或空名构建失败。
  • .fields 不能与 .matrix.full-widthnum 组合,meta 不能用在没有 .fields 的表上。
  • 表格单元格里放不下块内容:需要段落、列表、围栏就换 shortcode 形态。
  • requireddefault 是不翻译的 API 词汇,在所有语言下都显示英文,它们是契约词,不是界面文案。
  • 暂不支持 kindsincedeprecatedlocation、字段级链接与嵌套结构,也不会在构建时解析 TypeScript 或 OpenAPI schema。
  • 表格 — 属性行的其它取值与互斥规则
  • 配置总览 — 站点参数全表就是用参数表写的
  • 页面参数 — front matter 全表
  • 步骤 — 顺序动作不要写成参数表

4.7 - 步骤

有序列表加 {.steps} 就是带编号圆点与竖线的操作步骤;步骤要带标题、要进目录时改用 steps shortcode。

步骤(Steps)是带编号圆点与竖线的有序列表:一个普通有序列表,加一行 {.steps} 标记,编号圆点与串起它们的竖线由 CSS 绘制,不加载脚本。用于有先后的操作流程。并列而无先后的内容用普通列表或卡片。

写法有两种:有序列表加 {.steps}(默认选它),以及 {{% steps %}} shortcode,每一步要有自己的标题、标题还要进右侧目录时用它。

最简例子

每一项都写 1.,让 Markdown 自己数。这样插入、删除、调换步骤都不用手改编号,而且内容缩进恒定是三个空格。

源码
1. 安装 Hugo Extended
1. 克隆文档站
1. 启动本地预览
{.steps}
  1. 安装 Hugo Extended
  2. 克隆文档站
  3. 启动本地预览

{.steps} 必须紧贴列表最后一行,中间空一行它就会变成正文里一段可见的花括号。

步骤内容

列表项里可以放任何块级内容:段落、代码围栏、提示块、表格、嵌套列表、图片。缩进对齐到列表项的内容列(三个空格)即可。

源码
1. 克隆文档站,它本身就是主题的完整示例。

   ```bash
   git clone https://github.com/pgsty/oink.pgsty.com my-docs
   cd my-docs
   ```

1. 启动本地服务器。

   ```bash
   hugo server
   ```

   > [!NOTE]
   > 首次构建会通过 Go 模块代理拉取主题,需要本机安装 Go。

1. 替换三处内容,它就是你的站点。

   | 位置 | 替换为 |
   | --- | --- |
   | `hugo.yml` 的 `title` | 你的站名 |
   | `hugo.yml` 的 `baseURL` | 你的域名 |
   | `content/` | 你的内容 |
{.steps}
  1. 克隆文档站,它本身就是主题的完整示例。

    git clone https://github.com/pgsty/oink.pgsty.com my-docs
    cd my-docs
  2. 启动本地服务器。

    hugo server
    说明

    首次构建会通过 Go 模块代理拉取主题,需要本机安装 Go。

  3. 替换三处内容,它就是你的站点。

    位置替换为
    hugo.ymltitle你的站名
    hugo.ymlbaseURL你的域名
    content/你的内容

{{< … >}} 形式的 shortcode(标签页、卡片、徽章等)也可以写在列表项里;{{% … %}} 形式不行,见下面的限制

一步里按平台分开

某一步在不同平台上命令不同时,把带 {tab=} 的围栏并排写进那个列表项,它们照样会合成标签页。

源码
1. 安装 Hugo Extended。

1. 安装依赖:

   ```bash {tab="EL / RHEL" group="stepdemo" value="rpm"}
   sudo dnf install golang git
   ```
   ```bash {tab="Debian / Ubuntu" value="deb"}
   sudo apt install golang-go git
   ```

1. 运行 `hugo server` 预览。
{.steps}
  1. 安装 Hugo Extended。

  2. 安装依赖:

    EL / RHEL
    sudo dnf install golang git
    Debian / Ubuntu
    sudo apt install golang-go git
  3. 运行 hugo server 预览。

接着上一组往下编号

正文隔断了一组步骤时,把新一组的第一项写成它实际的序号,Markdown 会输出 start,编号从那里继续(支持到 40)。

源码
4. 配置 `baseURL` 与部署工作流。
1. 推送到 `main`,等待 GitHub Actions 构建完成。
{.steps}
  1. 配置 baseURL 与部署工作流。
  2. 推送到 main,等待 GitHub Actions 构建完成。

带标题的步骤

步骤本身很长、每一步该有个能被链接和被目录收录的标题时,用 {{% steps %}}:它的正文是页面级 Markdown,里面的每一个直接子标题就是一步,正文不用缩进。下面三步的标题就在这一页的右侧目录里。

源码
{{% steps %}}

### 安装工具链 {#install-toolchain}

需要 Hugo Extended ≥ 0.160.1 与 Go。

### 启动服务器 {#run-server}

{{< tabs group="oink-os" default="macos" >}}
{{< tab label="macOS" value="macos" >}}
`brew install hugo go`
{{< /tab >}}
{{< tab label="Debian" value="debian" >}}
`sudo apt install hugo golang-go`
{{< /tab >}}
{{< /tabs >}}

### 发布 {#publish}

推送到 `main`,仓库自带的工作流会构建并发布。

{{% /steps %}}

安装工具链

需要 Hugo Extended ≥ 0.160.1 与 Go。

启动服务器

brew install hugo go

sudo apt install hugo golang-go

发布

推送到 main,仓库自带的工作流会构建并发布。

它是主题里唯一的 {{% … %}} shortcode。百分号形式的正文交给 Goldmark 当页面级 Markdown 处理:只有这样,里面的标题才能进目录,里面才能放 tabscardsfields 这些容器 shortcode。代价是它自己不能嵌进列表项,也不能嵌进另一个百分号容器。

同一组步骤的标题保持同一层级,不要把一个 steps 套进另一个里。

两种形态的选择

情况用法
步骤是一两句话加一段命令有序列表 + {.steps}
每一步需要标题、需要被链接、需要进目录{{% steps %}}
步骤里要放 tabscardsfields 容器{{% 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} 的链接列表就是卡片。链接是标题, 之后是描述。

源码
- [快速上手](/zh/docs/start/) — 克隆这个文档站,删掉不需要的页面,替换为你的站点信息。
- [创作内容](/zh/docs/write/) — 页面怎么组织、front matter 有哪些键。
- [定制站点](/zh/docs/customize/) — 导航、搜索、品牌、多语言。
{.cards}
  • 快速上手 — 克隆这个文档站,删掉不需要的页面,替换为你的站点信息。
  • 创作内容 — 页面怎么组织、front matter 有哪些键。
  • 定制站点 — 导航、搜索、品牌、多语言。

整张卡片是点击热区,不只是标题文字。没有 columns 参数:列数由容器宽度决定,窄屏收成一列。

只有标题的卡片

描述可以省略。一行一个链接,{.cards} 收尾。

源码
- [提示块](/zh/docs/components/callout/)
- [标签页](/zh/docs/components/tabs/)
- [步骤](/zh/docs/components/steps/)
- [参数表](/zh/docs/components/fields/)
{.cards}

松散列表与多段描述

一句话装不下时改用松散列表:链接单独一段,描述另起一段,列表项之间空一行。标题独占一行,描述在标题下方。{.cards} 仍然紧贴最后一段,中间 不能有空行

源码
- [页面参数](/zh/docs/write/frontmatter/)

  每个页面参数在这里有唯一定义:类型、默认值、取值范围,以及讲它的那一页。

- [配置总览](/zh/docs/customize/config/)

  站点参数按功能分组,同样每行反向链接到讲它的指南页。
{.cards}
  • 页面参数

    每个页面参数在这里有唯一定义:类型、默认值、取值范围,以及讲它的那一页。

  • 配置总览

    站点参数按功能分组,同样每行反向链接到讲它的指南页。

图标与徽章

链接列表不支持图标、徽章、图片与多段描述,这些用 cards / card shortcode。icon 是恰好一对 Font Awesome class,badge 是一段纯文本。

源码
{{< cards >}}
{{< card title="快速上手" link="/zh/docs/start/" icon="fa-solid fa-rocket" badge="从这里开始" >}}
Fork 文档站本身,十分钟内完成本地预览。
{{< /card >}}
{{< card title="发布与下载页" link="/zh/docs/write/releases/" icon="fa-solid fa-box-open" badge="v0.5" >}}
`release` 事实记录 + 资产表 + 校验和,全部本地生成。
{{< /card >}}
{{< card title="键盘导航" link="/zh/docs/customize/keyboard/" icon="fa-solid fa-keyboard" >}}
全站快捷键与焦点顺序。
{{< /card >}}
{{< /cards >}}
快速上手从这里开始

Fork 文档站本身,十分钟内完成本地预览。

release 事实记录 + 资产表 + 校验和,全部本地生成。

图标格式不符(不是 fa-solid fa-xxx 这样的一对 class)时构建失败,不会静默丢弃。

Markdown 正文

card 的正文按页面级 Markdown 渲染:行内代码、强调、链接、列表都可以。titlebadge 这些参数是纯文本,不解析 Markdown。

源码
{{< cards >}}
{{< card title="Hugo Module" icon="fa-brands fa-golang" >}}
`hugo mod get github.com/pgsty/oink`。推荐方式,升级只需改一行版本号。
{{< /card >}}
{{< card title="Git Submodule" icon="fa-solid fa-code-branch" >}}
无需安装 Go:

- `git submodule add`
- 主题落在 `themes/oink`
{{< /card >}}
{{< /cards >}}
Hugo Module

hugo mod get github.com/pgsty/oink。推荐方式,升级只需改一行版本号。

Git Submodule

无需安装 Go:

  • git submodule add
  • 主题落在 themes/oink

不写 link 的卡片渲染成加粗标题,不生成链接。

带图片的卡片

image![alt](src) 的解析顺序一致:页面资源 → 全局资源 assets/ → 静态路径 /images/… → 远程 URL。本地资源带上固有尺寸,避免加载跳版。

image 必须配一个替代文字来源:image_alt="…"(有信息的图)或 decorative=true(纯装饰)。两个都写、两个都不写都会构建失败。

源码
{{< cards >}}
{{< card title="OINK 文档外壳" link="/zh/docs/about/features/" image="images/content-primitives/oink.webp" image_alt="OINK 文档页面:侧栏、正文与目录三栏" >}}
侧栏、正文、目录,三栏可以单独关闭。
{{< /card >}}
{{< card title="发布说明" link="/zh/docs/write/releases/" image="/images/releasenote.webp" decorative=true >}}
装饰性封面:`decorative=true` 输出空 alt,读屏器会跳过它。
{{< /card >}}
{{< /cards >}}

装饰性封面:decorative=true 输出空 alt,读屏器会跳过它。

卡片图片不参与图片缩放,整张卡片本身已经是链接。

栏目首页的自动卡片

栏目首页(_index.md)不需要手写卡片列表:主题读子页的 titledescriptionicon 自动生成一组卡片。本站在 hugo.yml 中全局启用:

hugo.yml
params:
  ui:
    section_index: cards # list | cards

单个栏目可以在自己的 front matter 里覆盖,也可以用 cascade 把选择推给整棵子树:

content/docs/customize/_index.zh.md
section_index: list

自动卡片与手写卡片使用同一套 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} , 列表属性行 , default
写在无序列表 之后 的一行;只对无序列表生效
列表项首个链接 , Markdown 链接 , default
卡片标题,同时是整张卡片的点击目标
其余内容 , Markdown , default
描述。紧凑列表里跟在 后面,松散列表里另起一段

card 的参数(cards 自身不接受任何参数):

title , 纯文本 , default
必填,非空。卡片标题
link , URL , default
站内路径、相对路径、http(s):mailto:;外链自动加 rel="noopener"
icon , Font Awesome class 对 , default
例如 fa-solid fa-rocket;格式不符构建失败
badge , 纯文本 , default
标题右侧的小标签
image , 图片来源 , default
页面资源 / 全局资源 / 静态路径 / 远程 URL
image_alt , 纯文本 , default
image 时与 decorative 二选一
decorative , 布尔 , defaultfalse
true 表示装饰图,输出空 alt
正文 , Markdown , default
卡片描述

没有 colscolumnsaccentdesccolor 参数;未知参数一律构建失败。

限制与常见问题

  • {.cards} 只认无序列表:有序列表加了这个标记不会变成卡片。
  • {.cards} 必须紧贴列表:中间空一行、或缩进进列表项,标记被静默丢弃,构建不报错,列表仍是列表。渲染结果不是卡片时先检查这一行。
  • card 只能待在 cards 里:单独使用、或放进别的 shortcode,构建失败并指出位置。
  • 列数不可配:网格按容器宽度自适应,只有栏目首页的自动卡片能用 params.ui.section_index_columns 指定列数。
  • 卡片不放长文:描述超过两行时改用正文段落或提示块

4.9 - 文件树

filetree 围栏画带注释的目录结构:对齐的注释列、逐条目图标、可折叠目录、可拖动的分栏。

文件树(FileTree)是一个 filetree 围栏,围栏正文就是目录清单:缩进表示层级,结尾的 / 表示目录,# 之后是注释。适合解释一份目录结构里与读者有关的那部分,并逐条加上说明。需要读者逐字复制的清单用普通代码块。

最简例子

源码
```filetree
- content/
  - _index.zh.md
  - docs/
  - blog/
- hugo.yml
- go.mod
```
  • content/
    • _index.zh.md
    • docs/
    • blog/
  • hugo.yml
  • go.mod

项目符号(-*+)可以省略,效果相同。有子项的条目是目录;没有子项时,结尾的 / 告诉主题它是目录。

加注释

每行第一个前面带空白的 # 之后是注释,渲染成对齐的右列。注释是纯文本,里面的 Markdown 按字面显示;要一个字面井号就写 \#

源码
```filetree
- content/          # 全部页面,中英双语同目录
  - docs/          # 你正在读的这棵文档树
  - blog/           # 发布说明与文章
- assets/scss/      # 站点自己的 SCSS,覆盖主题变量
- layouts/          # 站点级模板覆盖,越少越好
- static/images/    # 不需要构建期处理的图
- hugo.yml          # 站点配置:语言、菜单、params.ui
```
  • content/全部页面,中英双语同目录
    • docs/你正在读的这棵文档树
    • blog/发布说明与文章
  • assets/scss/站点自己的 SCSS,覆盖主题变量
  • layouts/站点级模板覆盖,越少越好
  • static/images/不需要构建期处理的图
  • hugo.yml站点配置:语言、菜单、params.ui

注释列的起点在构建期算出,由最宽的一行决定,因此每行的 # 从同一列开始,与源码里是否对齐无关。注释列最多占面板的右半边,最少占三成。中间的虚线是分隔条,可以拖动,也可以用 Tab 聚焦后按方向键调整(Home / End 到两端)。

过长的名称与注释各自在本列内用省略号截断,鼠标悬停时由 title 提示完整文本。分隔条是文件树唯一的 JavaScript,只有 带注释 的树才加载它。

源码
```filetree {title="两列都发生截断"}
- runbooks/
  - a-deliberately-long-runbook-filename-for-a-failover-drill.md  # 同样超长的注释,写在一行里,因此必须在注释列内截断
  - restart.md                                                    # 短名字
```

两列都发生截断

  • runbooks/
    • a-deliberately-long-runbook-filename-for-a-failover-drill.md同样超长的注释,写在一行里,因此必须在注释列内截断
    • restart.md短名字

标题栏

围栏属性 {title="…"} 在树上方渲染一条标题栏;不写时没有标题栏。

源码
```filetree {title="oink.pgsty.com 仓库根目录"}
- content/          # 页面
- assets/           # 参与构建的资源
- data/             # 首页、Landing、下载页的数据
- layouts/          # 模板覆盖
- static/           # 原样拷贝的文件
- tests/            # Playwright 与 node --test
- hugo.yml
- go.mod            # 用 Hugo Module 引入主题
- Makefile          # make d / make b / make c
```

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 命令的输出可以整段粘贴,包括开头的根目录行与结尾的统计行,统计行会被丢弃。

源码
```filetree
content/docs
├── about
│   ├── _index.zh.md
│   └── features.zh.md
├── components
│   ├── filetree.zh.md
│   └── image
│       └── index.zh.md
└── _index.zh.md

3 directories, 5 files
```
  • content/docs
    • about
      • _index.zh.md
      • features.zh.md
    • components
      • filetree.zh.md
      • image
        • index.zh.md
    • _index.zh.md

退回到未打开过的缩进层级时构建失败,报错里带围栏内的行号。

折叠与显式类型

有子项的目录默认展开,{open=false} 使其初始收起。目录用原生 <details> 渲染,键盘可操作,不需要 JavaScript。open 只能写在目录上。没有子项、名字也不以 / 结尾的条目按文件处理,{type=dir} 覆盖这个判断,{type=file} 同理。

源码
```filetree {title="内容目录"}
- content/
  - docs/                # 新文档树
    - components/         # 22 个组件页        {open=false}
      - callout.zh.md
      - filetree.zh.md
      - image/            # 页面包:正文 + 图  {type=dir}
    - customize/          # 站点级配置          {open=false}
      - config.zh.md
  - blog/
    - release.zh.md
```

内容目录

  • content/
    • docs/新文档树
      • components/22 个组件页
        • callout.zh.md
        • filetree.zh.md
        • image/页面包:正文 + 图
      • customize/站点级配置
        • config.zh.md
    • blog/
      • release.zh.md

图标与配色

图标默认按名字推断:目录用文件夹图标,随开合切换;文件先按完整文件名匹配(LICENSEMakefilego.modpackage.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

源码
```filetree {title="部署目录:权限与要点"}
- /etc/pigsty/                 # 0755 root:root · 配置根目录        {icon="fa-solid fa-server" tone=info}
  - pigsty.yml                 # 0644 root:root · 集群清单
  - ca/                        # 0700 root:root · 自签 CA,不要提交进 Git  {icon="fa-solid fa-lock" tone=danger open=false}
    - ca.key                   # 0600 root:root
- /var/lib/pgsql/18/data/      # 0700 postgres:postgres · 数据目录   {tone=warning}
  - postgresql.conf            # 0600 postgres:postgres
- /usr/bin/pig                 # 0755 root:root · 命令行工具         {icon="fa-solid fa-terminal" tone=success}
```

部署目录:权限与要点

  • /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 校验与其它组件是同一套。

源码
```filetree {title="本站的组件页"}
- content/docs/components/
  - [callout.zh.md](/zh/docs/components/callout/)     # 提示块
  - [filetree.zh.md](/zh/docs/components/filetree/)   # 当前页面
  - [gallery.zh.md](/zh/docs/components/gallery/)     # 画廊
  - image/                                             # 页面包
    - [index.zh.md](/zh/docs/components/image/)       # 图片
- [hugo.yml](https://github.com/pgsty/oink.pgsty.com/blob/main/hugo.yml)   # 站点配置(GitHub)
```

本站的组件页

按平台分成标签页

围栏带 tab=(以及 group= value=)时成为一组标签页中的一页,可以与代码围栏混排。

源码
```filetree {tab="Linux" group="platform" value="linux"}
- /etc/pigsty/          # 配置
- /var/lib/pgsql/       # 数据
- /usr/bin/pig          # 可执行文件
```
```filetree {tab="macOS" value="macos"}
- ~/Library/Application Support/pigsty/   # 配置
- /opt/homebrew/bin/pig                   # 可执行文件
```
Linux
  • /etc/pigsty/配置
  • /var/lib/pgsql/数据
  • /usr/bin/pig可执行文件
macOS
  • ~/Library/Application Support/pigsty/配置
  • /opt/homebrew/bin/pig可执行文件

输出形态

输出呈现
HTML<div class="td-filetree">,可选标题栏,目录是原生 <details>;带注释时多一条可拖动分隔条(唯一的运行时)
打印同一棵树,全部展开,没有分隔条,注释换行不截断
Markdown原样输出 filetree 围栏
RSS围栏源码放进 <pre>

窄屏(小于 sm 断点)时布局收成单列:注释移到名称下方,不再截断,分隔条隐藏。不带注释的树是单列,也不加载任何脚本。

参数参考

围栏属性(写在 ```filetree 后面):

title , 纯文本 , default
树上方的标题栏;不写就不画;不能为空
tab , 纯文本 , default
让这棵树成为一个标签页
group / value , 字符串 , default
标签页分组与同步值;必须与 tab 同时出现
class , class 列表 , default
透传给站点 CSS

条目属性(写在每行末尾的 {…} 里):

icon , Font Awesome class 对 , default按名字 / 扩展名匹配
例如 fa-solid fa-lock;格式不符构建失败
tone , 枚举 , defaultneutral
neutral info success warning danger,只给图标上色
open , 布尔 , defaulttrue
仅目录;false 表示初始收起
type , 枚举 , default自动判断
dirfile,覆盖自动判断

行语法本身:

缩进
两个空格 / 四个空格 / 制表符 / tree│ ├── └── 连线都行
- name
项目符号可省略;- * + 等价
name/
结尾斜杠表示目录;名字原样渲染,斜杠保留
[name](url)
带链接的条目
# 注释
第一个前面带空白的 # 之后的内容;\# 是字面井号
N directories, M files
tree 的统计行,自动丢弃

未知属性、未知取值、写在文件上的 open、格式错误的 {…}、退回到未打开过的缩进层级,都会构建失败,并给出围栏内的行号。

限制与常见问题

  • 只有 filetree 围栏这一种形态:没有 {.filetree} 列表标记,也没有 shortcode。
  • 注释与名字都是纯文本:写 **粗体** 会原样显示,围栏源码在任何环境里都读得通。
  • 不读取磁盘:树是手写或粘贴的静态内容,不随仓库变化。
  • 不提供搜索、多选、复制整棵树:需要逐字复制时用普通代码块。
  • 分栏宽度不持久化:拖动过的位置刷新后回到构建期算出的默认值。

4.10 - 公式

用 KaTeX 写行内与块级数学公式,构建期渲染完毕,读者不下载任何脚本。

公式由 KaTeX 在构建期渲染成 HTML + MathML,页面只额外加载一份本地 KaTeX 样式表,没有 JavaScript,也不请求远程数学服务。行内公式写 \(…\),块级公式写 $$…$$\[…\],另有 mathchem 两种围栏。需要 TikZ 绘图或 KaTeX 不支持的宏包时,改用预渲染的图片

最简例子

行内公式写在句子中,前后的空格与标点留在分隔符外面。

源码
共享缓冲区命中率是 \(\mathrm{hit} = \frac{H}{H + R}\),其中 \(H\) 是 `blks_hit`,\(R\) 是 `blks_read`

共享缓冲区命中率是 hit=HH+R\mathrm{hit} = \frac{H}{H + R},其中 HHblks_hitRRblks_read

块级公式

独占一段的公式用 $$ 包起来,居中显示,字号更大。\[…\] 是等价写法。

源码
一棵扇出为 \(f\)、共 \(N\) 个键的 B 树,其高度为:

$$
h = \left\lceil \log_{f} N \right\rceil
$$

一棵扇出为 ff、共 NN 个键的 B 树,其高度为:

h=logfN h = \left\lceil \log_{f} N \right\rceil

一行装不下的长公式在正文列内横向滚动,不会把版面撑宽;打印时保持静态。

math 围栏

math 围栏是块级公式的另一种写法,不依赖站点的 passthrough 配置。源码在 GitHub 上是一个普通代码块。

源码
```math
N_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}
```
Nconn=λtˉrespN_{\text{conn}} = \lambda \cdot \bar{t}_{\text{resp}}

上式是 Little 定律在连接池上的形式:稳态下需要的并发连接数等于到达速率乘以平均响应时间。连接池大小通常远小于客户端数量。

化学式与单位

chem 围栏使用 KaTeX 的 mhchem 扩展,正文写 \ce{…}。同一个扩展也能排物理单位。

源码
```chem
\ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}
```
COX2+HX2OHX2COX3HX++HCOX3X\ce{CO2 + H2O <=> H2CO3 <=> H+ + HCO3^-}

语法见 mhchem 手册

编号公式

块级公式下面跟一行属性即成为编号公式。num 是作者书写的字符串(3-15.3),主题不自动计数;#id 不写时默认为 eq-<num>。编号显示在公式右侧,前缀「公式」按站点语言本地化。

源码
$$
\text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
$$
{#eq-wal num="3-1" caption="每日 WAL 产量的估算"}

见[公式 3-1](#eq-wal):乘上保留天数就是归档盘容量的下限。
WALdayTPS×sˉrecord×86400 \text{WAL}_{\text{day}} \approx \text{TPS} \times \bar{s}_{\text{record}} \times 86400
公式 3-1 每日 WAL 产量的估算

公式 3-1:乘上保留天数就是归档盘容量的下限。

caption(纯文本)可以省略。#idcaption 必须与 num 同时出现,不存在「半编号」的公式。同一页里重复的 ID、或同一编号指向两个 ID,都会构建失败。

交叉引用

正文可以用普通链接引用编号公式,上一节即是这种写法。跨页引用、或需要自动带上「公式 N」标签时用 xref

源码
容量规划从 {{< xref eq="3-1" anchor="eq-wal" />}} 开始。

容量规划从 公式 3-1 开始。

xref 可以写在目标之前,前向引用合法。整本书的公式目录、book-equations 索引见书籍出版

eq shortcode

eq 供无法开启 passthrough 的站点使用,正文交给同一个 KaTeX 渲染器。不带参数时是一个不注册编号的块级公式;带 num 时与上一节的属性行形态等价。

源码
{{< eq >}}\sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}{{< /eq >}}

{{< eq num="3-2" caption="顺序扫描与索引扫描的代价平衡点" >}}
c_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
{{< /eq >}}
σidx=rowsmatchedrowstotal\sigma_{\text{idx}} = \frac{\text{rows}_{\text{matched}}}{\text{rows}_{\text{total}}}
cseqP=crandσTc_{\text{seq}} \cdot P = c_{\text{rand}} \cdot \sigma \cdot T
公式 3-2 顺序扫描与索引扫描的代价平衡点

本站已开启 passthrough,日常写作用 $$eq 用于迁移来的书稿与不能修改 hugo.yml 的场合。

站点前置配置

mathchem 围栏无需配置。$$\[…\]\(…\) 这些分隔符依赖 Goldmark 的 passthrough 扩展。Hugo 不合并主题的 markup 配置,这段必须写在站点自己的配置文件里。本站使用下面这份:

hugo.yml
markup:
  goldmark:
    parser:
      attribute:
        block: true # 编号公式的属性行需要它
    extensions:
      passthrough:
        enable: true
        delimiters:
          block: [['\[', '\]'], ['$$', '$$']]
          inline: [['\(', '\)']]

各键的完整定义见配置总览。分隔符不能与站点正文冲突:单个 $ 没有配进去,避免「$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 , 字符串 , default
[0-9A-Za-z.-]+;注册为编号公式,右侧显示「公式 N」
#id , 标识符 , defaulteq-<num>
[A-Za-z][A-Za-z0-9_.:-]*;锚点与交叉引用目标
caption , 纯文本 , default
编号后面的说明;需要 num

eq shortcode 的参数:

num , 字符串 , default
同上;不写就是一个不编号的普通块级公式
id , 标识符 , defaulteq-<num>
需要 num
caption , 纯文本 , default
需要 num
class , class 列表 , default
需要 num;透传给站点 CSS
正文 , TeX , default
必填,非空

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,按图片使用。

最简例子

源码
```mermaid
flowchart LR
  内容["content/"] --> Hugo
  配置["hugo.yml"] --> Hugo
  主题["OINK 主题"] --> Hugo
  Hugo --> 站点["public/"]
```
flowchart LR
  内容["content/"] --> Hugo
  配置["hugo.yml"] --> Hugo
  主题["OINK 主题"] --> Hugo
  Hugo --> 站点["public/"]

围栏语言写 mermaid 即可,没有其它开关。主题检测到这个围栏后才把 Mermaid 运行时加入这一页,同一页里画十张图也只加载一次。

时序图

sequenceDiagram 描述参与者之间按时间发生的消息,适合说明请求链路与加载顺序。

源码
```mermaid
sequenceDiagram
  autonumber
  participant 读者 as 读者浏览器
  participant CDN as 静态托管
  participant JS as 页面脚本包
  读者->>CDN: GET /zh/docs/components/mermaid/
  CDN-->>读者: HTML(含 <pre class="mermaid">)
  读者->>CDN: GET 本页的脚本包
  CDN-->>读者: mermaid.min.js
  JS->>JS: 把围栏源码渲染成 SVG
  Note over JS: 未使用的运行时不下载
```
sequenceDiagram
  autonumber
  participant 读者 as 读者浏览器
  participant CDN as 静态托管
  participant JS as 页面脚本包
  读者->>CDN: GET /zh/docs/components/mermaid/
  CDN-->>读者: HTML(含 <pre class="mermaid">)
  读者->>CDN: GET 本页的脚本包
  CDN-->>读者: mermaid.min.js
  JS->>JS: 把围栏源码渲染成 SVG
  Note over JS: 未使用的运行时不下载

甘特图

gantt 画时间区间。下面是 PostgreSQL 各大版本从发布日算起的五年社区支持期,1825d 即五年。

源码
```mermaid
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
```
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 画实体与基数。两者都常用来解释数据模型。

源码
```mermaid
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 / rss
```
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 / rss
源码
```mermaid
erDiagram
  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
  }
```
erDiagram
  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 主题一次发布依次经过的五个状态。这五个状态互不等价,本地构建通过不属于其中任何一个。

源码
```mermaid
stateDiagram-v2
  [*] --> 源码完成
  源码完成 --> 已验证 : 主题检查脚本 + 站点测试套件全绿
  已验证 --> 已发布 : 推送不可变的签名 vX.Y.Z 标签
  已发布 --> 已文档化 : 站点 go.mod 钉住该标签
  已文档化 --> 已部署 : 生产构建上线
  已部署 --> [*]
  已发布 --> 源码完成 : 发现问题只能出新补丁版本,标签不移动
```
stateDiagram-v2
  [*] --> 源码完成
  源码完成 --> 已验证 : 主题检查脚本 + 站点测试套件全绿
  已验证 --> 已发布 : 推送不可变的签名 vX.Y.Z 标签
  已发布 --> 已文档化 : 站点 go.mod 钉住该标签
  已文档化 --> 已部署 : 生产构建上线
  已部署 --> [*]
  已发布 --> 源码完成 : 发现问题只能出新补丁版本,标签不移动

单张图的标题与配置

围栏正文最前面可以写 Mermaid 自己的 YAML 头,它不是 Hugo front matter。title 给图加标题,config 覆盖这一张图的 Mermaid 配置。写死 config.theme 的图不再跟随站点深浅色。

源码
```mermaid
---
title: 只有用到的运行时才会进包
config:
  flowchart:
    curve: linear
---
flowchart TD
  页面 --> 判断{用了什么组件?}
  判断 -->|Mermaid 围栏| M[mermaid.min.js]
  判断 -->|ECharts 围栏| E[echarts.min.js]
  判断 -->|都没用| B[只有基础包]
```
---
title: 只有用到的运行时才会进包
config:
  flowchart:
    curve: linear
---
flowchart TD
  页面 --> 判断{用了什么组件?}
  判断 -->|Mermaid 围栏| M[mermaid.min.js]
  判断 -->|ECharts 围栏| E[echarts.min.js]
  判断 -->|都没用| B[只有基础包]

深浅色

页面初始化时主题读取当前配色模式:深色模式下用 Mermaid 的 dark 主题,浅色模式下用站点配置的主题。Mermaid 不支持重新初始化,读者切换配色时会 重载整个页面,图的配色随之更新。

因此不要把 Mermaid 图放进需要保留输入状态的页面,例如带表单的页面。

站点级默认写在 hugo.yml 里,键名小写,主题按 Mermaid 的默认配置匹配回正确的大小写:

hugo.yml
params:
  mermaid:
    theme: neutral
    flowchart:
      diagrampadding: 6

完整键表见配置总览,可用值以 Mermaid 配置文档为准。

放进标签页与步骤

mermaid 围栏没有 tab 属性,相邻围栏标签页只对普通代码围栏生效。并排比较两张图用 tabs shortcode。

源码
{{< tabs >}}
{{< tab label="按数据流看" >}}
```mermaid
flowchart LR
  Markdown --> Goldmark --> 渲染钩子 --> HTML
```
{{< /tab >}}
{{< tab label="按输出形态看" >}}
```mermaid
flowchart LR
  页面 --> HTML
  页面 --> 打印
  页面 --> Markdown
  页面 --> RSS
```
{{< /tab >}}
{{< /tabs >}}
flowchart LR
  Markdown --> Goldmark --> 渲染钩子 --> HTML
flowchart LR
  页面 --> HTML
  页面 --> 打印
  页面 --> Markdown
  页面 --> RSS

{{% steps %}} 里的每一步是页面级 Markdown,其中可以写 mermaid 围栏,用法见步骤

输出形态

输出呈现
HTML<pre class="mermaid"> + 本地 Mermaid 运行时,浏览器画成 SVG
打印与 HTML 相同:打印视图同样加载运行时,图会画出来
Markdown原样保留 mermaid 围栏与它的源码
RSS输出 <pre class="mermaid"> 包着的图表源码,订阅端看到的是文本

参数参考

围栏属性:没有。mermaid 围栏不读属性行,写 {height=…}{class=…} 之类既不生效也不报错;尺寸由图自身与容器宽度决定。

站点参数(hugo.yml):

params.mermaid , map , default未设置
整个映射按 Mermaid 的 initialize() 配置传入;键名写小写,主题按 Mermaid 默认配置匹配回正确大小写
params.mermaid.theme , string , defaultMermaid 默认
浅色模式下的主题;深色模式下被强制为 dark

单张图的配置写在围栏正文最前面的 YAML 头里(titleconfig),属于 Mermaid 语法,不是主题参数。

限制与常见问题

  • 切换深浅色会重载页面:Mermaid 不支持重新初始化,主题在渲染正确与不刷新之间选择了前者。
  • 图不能编号、不能缩放:Mermaid 输出的是内联 SVG,不是 <img>{#id num=} 编号与图片缩放都不适用;需要编号时导出成图片,按图片的编号写法使用。
  • 围栏属性无效:宽度在图里控制(flowchart 的方向、classDiagram 的布局),或者用 CSS。
  • 语法错误只在浏览器里可见:Hugo 不解析 Mermaid 语法,写错的图在页面上显示 Mermaid 的报错框,构建照样通过,发布前要在浏览器里确认。
  • RSS 订阅者只能看到源码:结论要写在正文里,不要只画在图上。
  • PlantUML — UML 更全,但需要一个渲染服务
  • 思维导图 — 大纲式的层级图
  • ECharts — 有数值的统计图
  • 图片 — 手绘 SVG、需要编号与缩放的图

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 声明参与者,-> 是同步消息,--> 是返回。

源码
```plantuml
@startuml
actor 读者
participant 浏览器
participant 渲染端点 as Server
读者 -> 浏览器 : 打开页面
浏览器 -> Server : GET /plantuml/svg/{压缩编码后的源码}
Server --> 浏览器 : SVG
浏览器 -> 浏览器 : 用一个 img 元素替换掉围栏
@enduml
```

画出来是四条泳道、四条消息的一张时序图:读者打开页面 → 浏览器带着编码后的源码请求端点 → 端点返回 SVG → 运行时把围栏替换成图片。

类图

class 写成员,"1" -- "0..*" 写关系基数,用来解释数据模型。

源码
```plantuml
@startuml
class Publication {
  + pubname : name
  + puballtables : bool
  + pubinsert / pubupdate / pubdelete : bool
}
class Subscription {
  + subname : name
  + subconninfo : text
  + subslotname : name
}
class ReplicationSlot {
  + slot_name : name
  + plugin : name
  + confirmed_flush_lsn : pg_lsn
}
Publication "1" -- "0..*" Subscription : 被订阅
Subscription "1" -- "1" ReplicationSlot : 绑定
@enduml
```

三个方框各带一列字段,两条带基数标注的连线:一个发布可以被多个订阅使用,每个订阅绑定一个复制槽。

组件图

package 圈出部署单元,[组件] 是方块,--> 是依赖方向。

源码
```plantuml
@startuml
package "监控节点" {
  [Grafana] as grafana
  [Prometheus] as prom
  [Alertmanager] as alert
}
package "数据库节点" {
  [node_exporter] as node
  [pg_exporter] as pgexp
  [PostgreSQL] as pg
}
pg --> pgexp : 查询统计视图
node --> prom : /metrics
pgexp --> prom : /metrics
prom --> alert : 规则命中
grafana --> prom : PromQL
@enduml
```

两个虚线框,框里各三个组件方块,五条带标注的箭头串起采集链路。

活动图

start / stopif … then … else … endif 画带分支的流程。这类图不含箭头字符,是当前版本里能正常渲染的一类。

源码
```plantuml
@startuml
start
:写 content/docs/**/*.zh.md;
:补英文对等页,抄渲染出来的标题 ID;
if (hugo --panicOnWarning 通过?) then (是)
  :npm test;
else (否)
  :按报错的文件名与行号修正;
  stop
endif
if (测试全绿?) then (是)
  :提交 PR;
  stop
else (否)
  :返回修改;
  stop
endif
@enduml
```

一条竖向流程线,两个菱形判断各分出「是 / 否」两支,四个终点。

用例图

actor 是小人,(用例) 是椭圆,rectangle 圈出系统边界,适合放在文档的「读者是谁」一节。

源码
```plantuml
@startuml
left to right direction
actor 读者 as reader
actor 作者 as author
actor 维护者 as maintainer
rectangle 文档站 {
  reader --> (全文搜索)
  reader --> (切换中英文)
  reader --> (导出打印版)
  author --> (新增页面)
  author --> (本地预览)
  maintainer --> (升级主题版本)
  maintainer --> (发布上线)
}
@enduml
```

左边三个小人,右边一个方框里七个椭圆,连线表示谁能做什么。

深色模式下的配色

服务端不知道站点的配色模式,渲染出来的 SVG 底色是固定的白色。skinparam backgroundColor transparent 去掉底色,图落在页面背景上。线条与文字设成中性色后,两种模式下都可读。

源码
```plantuml
@startuml
skinparam backgroundColor transparent
skinparam defaultFontName sans-serif
skinparam ArrowColor #7C7C7C
skinparam ActivityBorderColor #7C7C7C
skinparam ActivityBackgroundColor #B0BEC522
start
:hugo mod get -u github.com/pgsty/oink;
:hugo --gc --minify;
:上传 public/;
stop
@enduml
```

PlantUML 的 !theme 指令(例如 !theme plain)也可用,主题包由服务端提供,自建端点需要确认已安装。

渲染服务

围栏本身没有开关,能否渲染取决于站点配置:

hugo.yml
params:
  plantuml:
    enable: true
    svg_image_url: https://plantuml.internal.example/plantuml/svg/
    svg: false
  • enable: true 却没写 svg_image_url → 构建报错 params.plantuml.enable requires an explicit params.plantuml.svg_image_url。主题不代替站点选择公共服务。
  • 自建可以用官方镜像 plantuml/plantuml-serversvg_image_url 指向它的 /svg/ 路径,结尾的斜杠不能省略,编码后的源码拼在它后面。
  • 端点的跨域策略、站点 CSP 的 img-srcsvg: true 时还有 connect-src)都要放行;子路径部署时写绝对 URL。

这几个键的完整定义在配置总览

输出形态

输出呈现
HTML先输出 <pre><code class="language-plantuml"> 源码,启用后由运行时替换成 <img>svg: true 时是 <svg data-src>
打印与 HTML 相同:打印视图同样加载运行时并请求端点
Markdown原样保留 plantuml 围栏与它的源码
RSS只有围栏源码,订阅端看到的是文本

未启用、或运行时没有加载时,页面上留下的是一段可读的源码块,不会出现坏图标。

参数参考

围栏属性:没有。plantuml 围栏不读属性行;它也不走 OINK 的代码块外壳,titlecopy、行号这些代码块参数在这里都无效。

站点参数(hugo.yml):

params.plantuml.enable , bool , defaultfalse
关闭时围栏保持为代码块,不加载运行时
params.plantuml.svg_image_url , string , default
渲染端点,编码后的源码直接拼在它后面;enable: true 时必填,否则构建失败
params.plantuml.svg , bool , defaultfalse
false<img src>true<svg data-src> 并额外加载外部 SVG 加载器,SVG 内容进 DOM、可被 CSS 影响

主题只读这三个键,其它键写了没有效果。

限制与常见问题

  • <>&" 会被二次转义:当前主题版本的 plantuml 围栏对内容多做了一次转义,页面上留下 --&gt;&#34; 这样的字面文本,端点收到后返回一张 Syntax Error? 图。带箭头的图(时序、组件、用例、状态)目前渲染不出来,只有活动图这类不含这些字符的能正常渲染。修复前请改用 Mermaid 或预渲染的图片
  • 必须有服务:主题不提供、也不默认任何公共端点。
  • 图表源码会离开浏览器:涉密内容不要写进 PlantUML 围栏。
  • 不跟随深浅色:服务端不知道读者的配色模式,只能靠 skinparam 自己调。
  • 不能编号、不能缩放:运行时插入的 <img> 不经过图片渲染钩子,{#id num=} 与图片缩放都用不上。
  • Mermaid — 不需要服务、跟随深浅色,日常首选
  • Draw.io — 同样需要自建服务的另一个图表集成
  • 图片 — 预渲染 SVG,可编号、可缩放、无外部依赖
  • 配置总览params.plantuml.* 的完整定义

4.13 - 思维导图

markmap 围栏把一段 Markdown 大纲变成可展开、可缩放的思维导图,源码本身就是能读的提纲。

markmap 围栏的正文是一段普通的 Markdown 大纲:标题与列表决定层级,浏览器把它画成一棵可展开、可折叠的树。适合把「这一节讲了什么」的层级一次呈现。节点之间有方向、有条件的流程用 Mermaid

最简例子

源码
```markmap
# OINK
## 本地优先
- 运行时全部随主题分发
- 不依赖任何 CDN
## Markdown 原生
- 组件是围栏和属性行
- 不写 shortcode 也能用
## 四态输出
- HTML
- 打印
- Markdown
- RSS
```
# OINK
## 本地优先
- 运行时全部随主题分发
- 不依赖任何 CDN
## Markdown 原生
- 组件是围栏和属性行
- 不写 shortcode 也能用
## 四态输出
- HTML
- 打印
- Markdown
- RSS

一级标题是根节点,其余标题与列表项按缩进挂在它下面。点击节点上的圆点折叠或展开这一支,鼠标滚轮缩放,拖动平移。右下角一排工具按钮提供缩放、适应窗口与下载 SVG。

多层级

层级越深字号越小,画布自动排布。下面是本站文档的六个栏目与它们的页数。

源码
```markmap
# OINK 文档
## 简介(4 页)
### 它是什么
### 功能一览
### 案例
### 许可
## 快速上手(3 页)
### Fork 本站
### 目录结构
### 从零开始
## 创作内容(8 页)
### 组织内容
### 编写页面
### 页面参数
### 博客
### 书籍
### 发布与下载
### OpenAPI
## 组件(22 页)
### 提示块 / 标签页 / 步骤 / 卡片
### 图片 / 画廊 / 表格 / 参数表
### 图表:Mermaid / PlantUML / 思维导图 / ECharts
## 定制站点(15 页)
### 品牌 / 导航 / 搜索 / 多语言
### 首页 / 版本 / 分类 / 打印
## 维护管理(7 页)
### 预览 / 部署 / 升级
### 评论 / 统计 / 排错
```
# OINK 文档
## 简介(4 页)
### 它是什么
### 功能一览
### 案例
### 许可
## 快速上手(3 页)
### Fork 本站
### 目录结构
### 从零开始
## 创作内容(8 页)
### 组织内容
### 编写页面
### 页面参数
### 博客
### 书籍
### 发布与下载
### OpenAPI
## 组件(22 页)
### 提示块 / 标签页 / 步骤 / 卡片
### 图片 / 画廊 / 表格 / 参数表
### 图表:Mermaid / PlantUML / 思维导图 / ECharts
## 定制站点(15 页)
### 品牌 / 导航 / 搜索 / 多语言
### 首页 / 版本 / 分类 / 打印
## 维护管理(7 页)
### 预览 / 部署 / 升级
### 评论 / 统计 / 排错

链接、代码与强调

节点里可以写行内 Markdown:链接可点击,行内代码用等宽字体,粗体与斜体照常生效。

源码
```markmap
# 日常命令
## 预览
- `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)
```
# 日常命令
## 预览
- `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,节点里的 $…$ 会被渲染成公式。

源码
```markmap
# 常看的几个 PostgreSQL 指标
## 缓存命中率
- $\frac{blks\_hit}{blks\_hit + blks\_read}$
- 低于 0.99 时检查 shared_buffers
## 复制延迟
- $lsn_{primary} - lsn_{replica}$
## 事务吞吐
- $TPS = \frac{\Delta xact\_commit}{\Delta t}$
```
# 常看的几个 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
---
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
```
---
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],由读者自己展开。折叠块里的每一行都要以 > 开头,围栏也不例外。

源码
> [!DETAILS] 主题仓库长什么样
> ```markmap
> # pgsty/oink
> ## layouts/
> - baseof.html 与各类型的壳
> - _partials/shell/
> - _markup/ 渲染钩子
> - _shortcodes/
> ## assets/
> - scss/ 令牌与组件样式
> - js/ 浏览器运行时
> - third_party/ 随主题分发的库
> ## i18n/
> - 32 个语言文件,键完全对齐
> ## docs/
> - 冻结契约文档
> ```
主题仓库长什么样
# 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 , bool , defaultfalse
关闭时围栏保持为代码块,不加载任何运行时

键的完整定义见配置总览。每张图的行为写在围栏正文最前面的 markmap: YAML 头里(initialExpandLevelcolorFreezeLevelmaxWidth 等),属于 Markmap 语法,可用键以 Markmap 文档为准。

限制与常见问题

  • 输出是固定 300px 高的内联 SVG:高度由一条 .markmap > svg 规则统一,围栏改不了,层级太多时用 initialExpandLevel 收起或拆成两张图;内联 SVG 也不适用 {#id num=} 编号与图片缩放。
  • 不跟随深浅色:连线颜色由 Markmap 自己的调色板决定,两种模式下都需要检查对比度。
  • 没开 params.markmap 就只是代码块:不用这个组件的站点不加载任何运行时。
  • 右下角工具栏里的「下载 SVG」是浏览器行为,导出的是当前展开状态的快照。
  • 大纲里避开 <>&":当前主题版本的 markmap 围栏会把这几个字符二次转义,节点上会出现 &gt;&#34; 这样的字面文本;写链接用 [文字](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 只是惯例。

源码
![Hugo 构建流水线:content 目录经 Hugo 产出 public 目录](pipeline.drawio.svg)
{width="620" height="140"}
Hugo 构建流水线:content 目录经 Hugo 产出 public 目录

这张图嵌着一份 mxfile 副本,因此被包进了 .drawio 容器。鼠标移到图上时,右下角出现一个铅笔按钮;点击后在当前页面盖一层全屏 iframe,加载站点配置的编辑器。

副本检测

运行时的判断依据只有一条:文件内容里有没有 mxfile 字样,与文件名无关。下面这张同样是 SVG、同样是块级图片,但它是手写的,没有副本,也就没有按钮。

源码
![文档外壳的三栏:侧栏、正文、目录](plain-shell.svg)
{width="620" height="140"}
文档外壳的三栏:侧栏、正文、目录

带图注

Draw.io 图片走的是普通图片渲染钩子,图片的属性照常可用。加 caption 得到带图注的 figure,编辑按钮仍然出现在图上。

源码
![Hugo 构建流水线](pipeline.drawio.svg)
{caption="内容、配置与主题模板汇进 Hugo,产出 public/ 目录" width="620" height="140"}
Hugo 构建流水线
内容、配置与主题模板汇进 Hugo,产出 public/ 目录

编号成书里的图

{#id num=…} 得到一张可交叉引用的编号图,与别的图片一样能被 xref 引用、进入图目录。

源码
![Hugo 构建流水线](pipeline.drawio.svg)
{#fig_pipeline num="1-1" caption="从内容到静态站点" width="620" height="140"}
Hugo 构建流水线
图 1-1 从内容到静态站点

编号与交叉引用的完整规则见书籍出版

SVG 还是 PNG

两种都识别。Draw.io 导出 PNG 时同样能带上副本,存在 PNG 的文本块里,运行时的判断逻辑相同。

源码
![Hugo 构建流水线(PNG 导出)](pipeline.drawio.png)
{width="620" height="140"}
Hugo 构建流水线(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/ 里那一份,再自行提交。

编辑按钮供读者取走图去改,不是站点的在线编辑功能。

编辑器地址

hugo.yml
params:
  drawio:
    enable: true
    drawio_server: https://drawio.internal.example/
  • 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 , bool , defaultfalse
关闭时不加载任何脚本,图片就是图片
params.drawio.drawio_server , string , default
编辑器地址;enable: true 时必填

限制与常见问题

  • 运行时只在渲染内容含 .svg.png 候选图的页面加载;同一 URL 的图片合并检查,只读取一次以查找 mxfile
  • 导出时忘了勾「Include a copy of my diagram」,图就只是一张图,没有按钮。
  • 编辑依赖编辑器,且不写回仓库:离线环境里图片正常显示,按钮点了没有反应;编辑器保存等于浏览器下载,替换文件与提交都要手动做。
  • 按钮只在悬停时出现:触屏设备上没有 hover,读者不容易发现它,不要把可编辑当成关键功能来讲。
  • 配色不跟随深浅色:导出的 SVG 颜色是固定的;把填充设成 none、线条与文字用中性灰,两种模式下都能看(本页这两张图就是这么做的)。
  • 图片 — 图注、编号、尺寸、缩放的完整规则
  • PlantUML — 另一个需要自建服务的图表集成
  • Mermaid — 不需要任何服务的文本画图
  • 配置总览params.drawio.* 的完整定义

4.15 - ECharts

echarts 围栏里用 YAML 或 JSON 写图表选项,Hugo 构建期校验,浏览器用本地 ECharts 画出跟随深浅色的统计图。

echarts 围栏的正文是一段 YAML 或 JSON 的 ECharts 选项对象,不是代码。适用于需要坐标轴、序列与图例的定量图表;只表达关系与流程时用 Mermaid,只表达顺序与层级时用 Infographic。Hugo 在构建期解析选项,解析失败则构建失败;浏览器用随主题分发的 ECharts 绘图,只有用到它的页面加载运行时。

最简例子

一个柱状图只需要三段:xAxisyAxisseries。下面是本站文档六个栏目各有多少页。

源码
```echarts {height="320px"}
tooltip:
  trigger: axis
xAxis:
  type: category
  data: [简介, 快速上手, 创作内容, 组件, 定制站点, 维护管理]
yAxis:
  type: value
  name: 页数
series:
  - name: 页数
    type: bar
    data: [4, 3, 8, 22, 15, 7]
```
tooltip:
  trigger: axis
xAxis:
  type: category
  data: [简介, 快速上手, 创作内容, 组件, 定制站点, 维护管理]
yAxis:
  type: value
  name: 页数
series:
  - name: 页数
    type: bar
    data: [4, 3, 8, 22, 15, 7]

两种格式都接受,YAML 不需要引号与逗号,写起来更短。缩进写错、正文解析成数组而不是映射,构建在这一行失败,不会输出一张空白图。

多序列折线

series 是数组,多一项就是多一条线;legend 让读者单独隐藏其中一条。下面是 PostgreSQL 各大版本的发布年份,以及按社区五年支持策略推算的终止年份。

源码
```echarts {height="360px"}
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]
```
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 按用途的构成。

源码
```echarts {height="340px"}
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 }
```
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 去掉正文的宽度限制,让图铺满内容区。适用于数据点多、标签长的图。

源码
```echarts {height="260px" 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]
```
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]

无效的高度(36036pt)会让构建失败,不会退回默认值。

深浅色

不写 theme 时,图按读者当前的配色模式初始化;切换配色时图原地重绘,不刷新页面。容器尺寸变化时自动 resize。把本页切到深色,上面每张图的底色与文字随之改变。

写定 theme 则固定配色,两种模式下都是同一套:

源码
```echarts {height="240px" theme="dark"}
xAxis:
  type: category
  data: [HTML, 打印, Markdown, RSS]
yAxis:
  type: value
series:
  - type: bar
    data: [1, 1, 1, 1]
```
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

源码
<script>
  window.OinkEchartsFunctions = window.OinkEchartsFunctions || {};
  window.OinkEchartsFunctions.pageShare = function (params) {
    var p = params[0];
    return p.name + ':' + p.value + ' 页,占全站 ' + Math.round((p.value / 59) * 100) + '%';
  };
</script>

```echarts {height="300px"}
tooltip:
  trigger: axis
  formatter: "$fn:pageShare"
xAxis:
  type: category
  data: [简介, 快速上手, 创作内容, 组件, 定制站点, 维护管理]
yAxis:
  type: value
series:
  - type: bar
    data: [4, 3, 8, 22, 15, 7]
```
tooltip:
  trigger: axis
  formatter: "$fn:pageShare"
xAxis:
  type: category
  data: [简介, 快速上手, 创作内容, 组件, 定制站点, 维护管理]
yAxis:
  type: value
series:
  - type: bar
    data: [4, 3, 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 , CSS 长度 , default400px
只接受非负数字加 px rem em vh vw %;其它写法构建失败
theme , string , default未设置
固定使用某个 ECharts 主题,从此不再跟随站点配色;内置只有 dark
full , bool , defaultfalse
true 去掉正文宽度限制,图铺满内容区
class , 空格分隔的 class , default
透传给容器,交给站点 CSS

styleon* 与其它未知属性都会让构建失败。围栏正文必须能解析成一个 YAML/JSON 映射,解析失败或解析成数组同样失败。选项键本身是 ECharts 的,以官方选项手册为准。

没有站点级参数:ECharts 不需要在 hugo.yml 里开关,用到时才加载。

限制与常见问题

  • 围栏里不能写 JavaScript:需要函数时通过 $fn: 桥接,未注册的名字解析为 undefined,没有报错。
  • 围栏不读外部数据:data/ 目录、front matter 与 shortcode 都引用不到,数字写在围栏里。
  • 打印与 RSS 里只有源码,结论要写进正文。
  • YAML 的类型转换:分类轴上的 109.6onyes 会被解析成数字或布尔值,需要引号。
  • 颜色不是唯一的区分手段:多序列图同时区分线型或标记形状,两种配色模式下都要检查图例对比度。
  • Infographic — 表达结构与顺序的信息图,不是统计图
  • 表格 — 数据少、需要精确读数时用表格
  • Mermaid — 关系图与流程图
  • 代码块 — 围栏属性行的通用规则

4.16 - Infographic

infographic 围栏挑一个 AntV 模板,把标题与条目渲染成流程、时间线、漏斗、网格或层级信息图。

infographic 围栏挑一个 AntV 模板,把「标题 + 一串条目」渲染成信息图。适用于表达顺序、层级与对比这类结构。需要坐标轴与数值精度时用 ECharts,需要条件分支的流程时用 Mermaid。围栏正文是数据,在 GitHub 上仍是一段可读的文本。

最简例子

第一行是 infographic 模板名,其后是一个 data 块:title 是标题,items 下面每个条目至少要有 label

源码
```infographic
infographic list-row-simple-horizontal-arrow
data
  title 一次文档改动的三步
  items
    - label 写
      desc 先写中文 .zh.md
    - label 校
      desc 构建零告警,例子真渲染
    - label 发
      desc 补英文对等页,提交 PR
```
infographic list-row-simple-horizontal-arrow
data
  title 一次文档改动的三步
  items
    - label 写
      desc 先写中文 .zh.md
    - label 校
      desc 构建零告警,例子真渲染
    - label 发
      desc 补英文对等页,提交 PR

缩进决定结构,两个空格一级。标签要短,说明放 desc

时间线

sequence-timeline-* 系列把条目排成一条时间轴,label 是时间点,desc 是事件。

源码
```infographic {height="420px"}
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 子系统
```
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 {height="420px"}
infographic sequence-funnel-simple
data
  title 一次主题发布要经过的五个状态
  items
    - label 源码完成
      desc 代码写完,仅此而已
    - label 已验证
      desc 主题检查脚本与站点测试套件全绿
    - label 已发布
      desc 不可变的签名标签,能从 Go 代理拉到
    - label 已文档化
      desc 文档站钉住了这个标签
    - label 已部署
      desc 生产环境运行的就是这个版本
```
infographic sequence-funnel-simple
data
  title 一次主题发布要经过的五个状态
  items
    - label 源码完成
      desc 代码写完,仅此而已
    - label 已验证
      desc 主题检查脚本与站点测试套件全绿
    - label 已发布
      desc 不可变的签名标签,能从 Go 代理拉到
    - label 已文档化
      desc 文档站钉住了这个标签
    - label 已部署
      desc 生产环境运行的就是这个版本

网格卡片

条目之间没有先后关系时用 list-grid-*,它把条目排成网格而不是队列。

源码
```infographic {height="380px"}
infographic list-grid-compact-card
data
  title 同一页内容的四种输出
  desc 每个内容组件都要在这四态里给出可用的结果
  items
    - label HTML
      desc 交互式,按需加载运行时
    - label 打印
      desc 折叠展开,去掉缩放与复制按钮
    - label Markdown
      desc 纯文本,按字节比对金样本
    - label RSS
      desc 静态,与打印同源
```
infographic list-grid-compact-card
data
  title 同一页内容的四种输出
  desc 每个内容组件都要在这四态里给出可用的结果
  items
    - label HTML
      desc 交互式,按需加载运行时
    - label 打印
      desc 折叠展开,去掉缩放与复制按钮
    - label Markdown
      desc 纯文本,按字节比对金样本
    - label RSS
      desc 静态,与打印同源

带数值的条目

条目上加 value,能表达比例的模板(饼、环、进度)会用到它。

源码
```infographic {height="400px"}
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
```
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

层级与手绘风格

条目下面可以再嵌 childrenhierarchy-mindmap-* 把它画成两层的结构图。顶层的 theme 块换整张图的风格,typelightdarkhand-drawn

源码
```infographic {height="320px"}
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 随主题分发的库
```
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-*层级(配合 childrenhierarchy-mindmap-level-gradient-compact-card
chart-pie-* chart-bar-* chart-column-*value 的示意图chart-pie-donut-plain-text
relation-network-* relation-dagre-flow网络与流向(配合 relationsrelation-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 , auto 或 CSS 长度 , defaultauto
非负数字加 px rem em vh vw %;其它写法构建失败
full , bool , defaultfalse
true 去掉正文宽度限制
class , 空格分隔的 class , default
透传给容器

styleon* 与未知属性让构建失败;空的 DSL 正文也让构建失败。

DSL 的顶层键(属于 AntV,不是主题):

infographic / template
模板名,第一行
data
titledescitems(也可以是 sequences compares nodes values relations root,取决于模板结构)、order
theme
typelight / dark / hand-drawn)、palettecolorPrimarystylize
width / height
DSL 层的画布尺寸,一般交给围栏属性 height
design
逐部件的细调,少用

items 里每个条目可用 labeldescvalueiconchildrengroupid。DSL 的完整定义以 AntV Infographic 文档为准;随主题分发的版本与校验值记在主题仓库的 VENDOR.json 里。

限制与常见问题

  • 模板名写错不会让构建失败:Hugo 只检查围栏属性,DSL 由浏览器运行时解析,模板不存在时容器里显示一行错误文字。改动模板名后在页面上确认。
  • 不跟随深浅色:theme 写在 DSL 里,两种配色模式下都要检查对比度。
  • 打印与 RSS 里只有 DSL,关键结论要写进正文。
  • SVG 不是语义结构:屏幕阅读器读到的顺序未必是排版顺序。标题、列表、表格能表达的内容优先用它们。
  • 标签要短:长文本在窄屏下会被截断或挤压,改动后在手机宽度下确认。
  • ECharts — 需要坐标轴与精确数值时用它
  • 步骤 — 需要读者照做的流程用步骤
  • 卡片 — 可点击的入口网格
  • Mermaid — 带分支与条件的流程

4.17 - 画廊

gallery 围栏把一组相关截图排成响应式网格,每张可带说明或链接,并复用页面的图片缩放对话框。

画廊(Gallery)把一组相关图片排成响应式网格,围栏里每行一张图。适用于同一件事的几个视图:几张截图、几种状态、几套配色。单张图用图片;相互之间没有顺序与对比关系的图片不适合放进同一个画廊。

最简例子

围栏里一行一张图,语法是 Markdown 的 ![替代文字](来源)

源码
```gallery
![OINK 文档站的浅色首页](/images/hero-light.webp)
![OINK 文档站的深色首页](/images/hero-dark.webp)
```

替代文字必须写:它是这一项的标题、读屏器唯一能读到的文字,也决定这张图是否参与缩放。列数没有参数,网格随容器宽度自适应,窄屏减列。

加说明

图片后面用 # 起头写说明,显示在图下方。说明是纯文本,里面的 Markdown 按字面显示;要一个字面井号写 \#

源码
```gallery
![OINK 文档页面的三栏布局](/images/oink.webp) # 默认外壳:侧栏、正文、目录
![Docsy 的经典文档布局](/images/docsy.webp) # OINK 的上游 Docsy,内容模型一脉相承
![发布说明页面](/images/releasenote.webp) # 发布页由 data/download 里的事实生成,不联网
```

说明长短可以不一致:网格按最高的一项对齐,说明换行不影响相邻的图。图片先被解析,替代文字与路径里的 # 不需要转义。

行尾的 {link=…} 让这一项成为链接,站内路径、相对路径、http(s): 都可以。

源码
```gallery
![OINK 的默认文档外壳](/images/oink.webp) # 点击进入「图片」组件页 {link=/zh/docs/components/image/}
![发布说明页面](/images/releasenote.webp) # 点击进入「发布与下载页」 {link=/zh/docs/write/releases/}
```

带链接的项不参与缩放,点击已有别的含义。同一个画廊里两种项可以混排:有链接的打开页面,没有链接的打开大图。

图片来源

来源解析顺序与普通图片一致:页面资源(页面包里的同目录文件)→ 全局资源 assets/ → 静态路径 /images/… → 远程 URL。本地资源带上固有尺寸,加载时不跳版;远程图构建期不下载,也取不到尺寸。

源码
```gallery
![OINK 文档总览(全局资源)](images/content-primitives/oink.webp) # assets/images/… 下的图,可以做构建期处理
![浅色首页(静态路径)](/images/hero-light.webp) # static/images/… 下的图,原样发布
```

页面资源与全局资源找不到时构建失败;静态路径与远程 URL 不检查存在性。

装饰图与缩放

替代文字留空表示这是装饰性图片:没有标题,读屏器跳过,也不参与缩放。

图片缩放是站点级开关,默认关闭。本页在 front matter 中开启了它,上面每张有替代文字、没有链接的图都可以点开看大图(Esc 关闭,焦点回到原处)。

这一页的 front matter
image_zoom: true
源码:一张装饰图配一张正常图
```gallery
![](/images/docsy.webp) # 装饰性配图,不参与缩放
![Pigsty 发布说明页面](/images/releasenote.webp) # 有替代文字,可以点开
```

画廊没有自己的缩放运行时,复用整页共用的那个对话框。页面上没有可缩放的图时,运行时不加载。细节见图片 · 缩放

加 class 与分标签页

class 可以加在整个围栏上(写在语言后面)或某一项上(行尾),主题不解释它,原样透传给站点 CSS。围栏带 tab=(以及 group= value=)时成为一组标签页里的一页。

源码
```gallery {tab="浅色" group="theme" value="light"}
![浅色模式的首页](/images/hero-light.webp) # 默认配色
```
```gallery {tab="深色" value="dark"}
![深色模式的首页](/images/hero-dark.webp) # 跟随系统或手动切换
```
浅色
深色

输出形态

输出呈现
HTML<ul class="td-gallery">,每项一个 <li>;符合条件的图带 data-td-image-zoom 标记;全部懒加载
打印同一组图堆叠排列,没有缩放标记
Markdown原样输出 gallery 围栏
RSS与打印相同的静态堆叠

画廊不加载 JavaScript。

参数参考

行语法 ![alt](src) [# 说明] [{key=value …}]

![alt](src) , 必填
必须顶在行首。alt 是这一项的标题;留空表示装饰图
src , 必填
页面资源 / 全局资源 / 静态路径 / 远程 URL
# 说明 , 必填
纯文本,显示在图下方;\# 是字面井号;不能为空
{link=…} , 必填
让这一项成为链接,因而不可缩放
{class=…} , 必填
给这一项加站点 CSS class

围栏属性:

tab , 纯文本 , default
让这个画廊成为一个标签页
group / value , 字符串 , default
标签页分组与同步值;必须与 tab 同时出现
class , class 列表 , default
透传给站点 CSS

没有 columnscaptiontitle 属性。行首不是图片、# 之外的尾随文字、空说明、未知属性、格式错误的 {…} 都会让构建失败,报错给出围栏内的行号。

限制与常见问题

  • 只有围栏一种形态:没有 {.gallery} 列表标记,也没有 shortcode。代价是源码在 GitHub 上不渲染成图片,收益是四态输出与缩放资格由主题保证。
  • 不能指定列数,也不裁成统一宽高比:网格按视口自适应,图片按原始比例排列。
  • 没有幻灯片、轮播与上一张 / 下一张:缩放对话框一次显示一张。
  • 不下载远程图:构建期没有网络请求,远程图在浏览器加载前尺寸未知,可能跳版。
  • 说明不解析 Markdown:需要富文本时写在画廊下方的段落里。
  • 图片 — 单张图、图注、编号、缩放开关
  • 卡片 — 带图片的链接网格
  • 标签页 — 按平台 / 主题并列多组图
  • 文件树 — 行语法与画廊同源

4.18 - 徽章

在功能名、版本号或表格单元格旁边放一枚语义状态标签,五种 tone,不需要自定义颜色。

徽章(Badge)是紧跟在名字旁边的行内状态标签:Beta、已弃用、v0.5、需自建服务。适用于一两个词能说完的状态;作者只选语义 tone,颜色由主题决定,浅色与深色模式下的对比度都有保证。状态需要解释、操作步骤或截止日期时,改用正文或提示块

最简例子

源码
{{< badge text="Beta" tone="warning" >}}
Beta

text 是唯一必填参数,必须是非空字符串。

五种 tone

只有这五个取值,没有自定义颜色。

源码
{{< badge text="默认" >}}
{{< badge text="信息" tone="info" >}}
{{< badge text="已支持" tone="success" >}}
{{< badge text="实验性" tone="warning" >}}
{{< badge text="已弃用" tone="danger" >}}

默认 信息 已支持 实验性 已弃用

不写 tone 时使用 neutral。其它取值让构建失败,报错给出源文件位置。

夹在句子里

徽章是行内元素,跟在名字后面,不占单独一行。

源码
`params.ui.image_zoom` {{< badge text="默认关闭" tone="neutral" >}} 打开后,
有替代文字的块级图片可以点开看大图。PlantUML {{< badge text="需自建服务" tone="warning" >}}
与 Draw.io {{< badge text="需自建服务" tone="warning" >}} 没有配置服务端点时会让构建失败,
而不是连接公共服务。

params.ui.image_zoom 默认关闭 打开后, 有替代文字的块级图片可以点开看大图。PlantUML 需自建服务 与 Draw.io 需自建服务 没有配置服务端点时会让构建失败, 而不是连接公共服务。

标题旁边

标题里不要写 shortcode。 Hugo 先生成目录、后替换 shortcode,所以徽章在标题上渲染正常,目录里却会留下一段 Hugo 的内部占位符文本。把状态写进标题下面的第一段:

源码
### OpenAPI 页面 {#openapi-example}

{{< badge text="0.5 新增" tone="success" >}} 这一节介绍……

OpenAPI 页面

0.5 新增 徽章紧跟在标题下方,目录保持干净, 锚点链接分享出去也不会带上徽章文字。

表格单元格里

对照表里用徽章标状态,比整列写「是」「否」更容易扫读。

源码
| 组件 | 形态 | 状态 |
| --- | --- | --- |
| 提示块 | `> [!NOTE]` | {{< badge text="稳定" tone="success" >}} |
| 画廊 | ` ```gallery ` 围栏 | {{< badge text="稳定" tone="success" >}} |
| PlantUML | ` ```plantuml ` 围栏 | {{< badge text="需自建服务" tone="warning" >}} |
| `image` shortcode | — | {{< badge text="已移除" tone="danger" >}} |
组件形态状态
提示块> [!NOTE]稳定
画廊```gallery 围栏稳定
PlantUML```plantuml 围栏需自建服务
image shortcode已移除

列表与步骤里

源码
1. 安装 Hugo Extended {{< badge text="≥ 0.160.1" tone="info" >}}
1. 克隆文档站,修改 `hugo.yml` 里的 `baseURL`
1. `hugo server` 预览 {{< badge text="1313 端口" tone="neutral" >}}
{.steps}
  1. 安装 Hugo Extended ≥ 0.160.1
  2. 克隆文档站,修改 hugo.yml 里的 baseURL
  3. hugo server 预览 1313 端口

卡片里

卡片有自己的 badge 参数(纯文本,固定在标题右侧);卡片正文里可以放徽章 shortcode。

源码
{{< cards >}}
{{< card title="Hugo Module" icon="fa-brands fa-golang" badge="推荐" >}}
一行 `hugo mod get` 完成安装 {{< badge text="需要 Go" tone="info" >}}
{{< /card >}}
{{< card title="离线归档" icon="fa-solid fa-box-archive" >}}
不联网的机器也能构建 {{< badge text="手动升级" tone="warning" >}}
{{< /card >}}
{{< /cards >}}
Hugo Module推荐

一行 hugo mod get 完成安装 需要 Go

离线归档

不联网的机器也能构建 手动升级

link 后徽章变成链接(<a>),站内路径、相对路径、http(s):mailto: 都可以。

源码
当前版本 {{< badge text="v0.5" tone="info" link="/zh/blog/" >}},
升级步骤见 {{< badge text="版本升级" tone="neutral" link="/zh/docs/admin/upgrade/" >}}。

当前版本 v0.5, 升级步骤见 版本升级

链接非法(协议不在白名单里)会让构建失败。

输出形态

输出呈现
HTML无链接时 <span class="td-badge td-badge--<tone>">,有链接时 <a class="td-badge …">
打印同 HTML,静态行内元素
Markdown**Beta**,有链接时 [**Beta**](/…)
RSS同打印

不加载 JavaScript。徽章不是实时状态区域,新增徽章不会触发读屏器播报。

参数参考

text , 纯文本 , default
必填,非空。读者看到的文字
tone , 枚举 , defaultneutral
neutral info success warning danger
link , URL , default
设置后徽章变成链接

只接受命名参数。没有 iconclasscoloroutlinesize 参数;写了未知参数、空 text、非法 tone 或非法链接都会让构建失败。

限制与常见问题

  • 颜色不是唯一的含义载体:tone 是补充,文字要自己说清楚。{{< badge text="🔴" >}} 对读屏器没有信息。
  • 没有图标参数:需要图标时改用卡片提示块
  • 文字要短:徽章不换行地跟在名字后面,超过五六个字的内容写进正文。
  • 同一处不超过三枚:连排的徽章会盖过它修饰的名字。
  • 徽章只有 shortcode 一种形态,没有原生 Markdown 写法;纯 Markdown 阅读器里它退化成加粗文字。
  • 卡片card 自己的 badge 参数
  • 文件树tone 用的是同一套词汇
  • 按键 — 另一枚行内 shortcode
  • 提示块 — 状态需要解释时用它

4.19 - 按键

kbd 写快捷键:一个 shortcode 接一串按键名,输出语义化的按键序列,打印与 Markdown 输出里同样可读。

按键(Kbd)把读者要按下的键与正文区分开。适用于快捷键与组合键:一个按键一个位置参数,主题负责画框、补分隔符,并给读屏器一个可读的序列。命令名、选项名与要输入的文本用行内代码,它们不是物理按键。

最简例子

源码
按 {{< kbd "Ctrl" "K" >}} 打开命令面板。

CtrlK 打开命令面板。

参数必须加引号,一个按键一个参数。少于一个按键、空字符串、命名参数都会让构建失败。

单个按键

一个参数对应一个键,符号键按原样写。

源码
{{< kbd "Escape" >}} 关闭对话框;
{{< kbd "/" >}} 进入搜索;
{{< kbd "t" >}} 切换亮色 / 暗色;
{{< kbd "l" >}} 循环切换语言。

Escape 关闭对话框; / 进入搜索; t 切换亮色 / 暗色; l 循环切换语言。

组合键

多个参数按顺序渲染,中间补 +。这个加号对辅助技术隐藏,读屏器读到的是本地化的连接词。

源码
{{< kbd "⌘" "Shift" "P" >}} 与 {{< kbd "Ctrl" "Shift" "P" >}} 是同一个动作。
需要按字面的加号时,把它当成独立的一个按键:{{< kbd "Ctrl" "+" >}} 放大页面。

ShiftPCtrlShiftP 是同一个动作。 需要按字面的加号时,把它当成独立的一个按键:Ctrl+ 放大页面。

平台差异

按键名写读者键盘上印的标签:macOS 写 ,Windows / Linux 写 Ctrl。不要把两个平台合进同一个序列,Ctrl/⌘ 这类写法读屏器无法正确朗读。在句子里说明平台,或分成标签页

源码
macOS 按 {{< kbd "⌘" "K" >}},Windows 与 Linux 按 {{< kbd "Ctrl" "K" >}}。

macOS 按 K,Windows 与 Linux 按 CtrlK

快捷键表

速查表是按键最常见的位置。下面是本站生效的一部分全局键:

源码
| 按键 | 作用 |
| --- | --- |
| {{< kbd "Ctrl" "K" >}} | 打开命令面板(macOS 是 {{< kbd "⌘" "K" >}}) |
| {{< kbd "/" >}} | 面板的完整搜索态 |
| {{< kbd "t" >}} | 切换亮色 / 暗色 |
| {{< kbd "q" >}} / {{< kbd "e" >}} | 上一篇 / 下一篇 |
| {{< kbd "w" >}} {{< kbd "s" >}} {{< kbd "a" >}} {{< kbd "d" >}} | 在侧栏树里上下移动、折叠、展开 |
| {{< kbd "Escape" >}} | 从侧栏树回到正文 |
按键作用
CtrlK打开命令面板(macOS 是 K
/面板的完整搜索态
t切换亮色 / 暗色
q / e上一篇 / 下一篇
w s a d在侧栏树里上下移动、折叠、展开
Escape从侧栏树回到正文

全站快捷键的完整清单见键盘导航

步骤里

源码
1. 按 {{< kbd "Ctrl" "K" >}} 打开命令面板
1. 输入 `>` 进入纯命令态,或输入关键词搜索
1. 用 {{< kbd "↑" >}} {{< kbd "↓" >}} 选中一项,{{< kbd "Enter" >}} 前往
1. {{< kbd "Escape" >}} 关闭,焦点回到按下之前的位置
{.steps}
  1. CtrlK 打开命令面板
  2. 输入 > 进入纯命令态,或输入关键词搜索
  3. 选中一项,Enter 前往
  4. Escape 关闭,焦点回到按下之前的位置

原始 <kbd> 标签

Markdown 里写原始的 <kbd> 标签得到同样的样式,GitHub 也这么渲染。区别是分隔符与无障碍序列要自己维护:单个键两种写法都可以,组合键用 shortcode。

源码
按 <kbd>F5</kbd> 刷新;在编辑器里按 <kbd>Ctrl</kbd>+<kbd>S</kbd> 保存。

F5 刷新;在编辑器里按 Ctrl+S 保存。

输出形态

输出呈现
HTML<span class="td-kbd-sequence"> 包着每个键一个 <kbd>;可见的 + 对读屏器隐藏,另有一个本地化连接词
打印同 HTML,静态
Markdown纯文本 Ctrl + K⌘ + Shift + P
RSS同打印

没有 CSS 与 JavaScript 时,操作说明仍然可读。

参数参考

位置参数 1..n , 字符串 , default
至少一个,每个都必须非空且加引号;顺序就是显示顺序

只接受位置参数。没有 separatorlabelplatformclasssize 这些命名参数:Hugo 的 shortcode 不允许在一次调用里混用位置参数与命名参数。

限制与常见问题

  • 一个序列表示同时按下的一组键:先按 A 再按 B 这类连续操作写成两个 kbd 加一句说明(先按 Escape,再按 Enter)。
  • 不做平台检测:页面不会按访客的操作系统把 Ctrl 换成
  • 不做按键映射与录制:菜单路径、手势、游戏杆不在范围内。
  • 漏写引号会让构建失败:{{< kbd Ctrl K >}} 里的 Ctrl 不是字符串参数。
  • 不用它标命令:hugo server 写成行内代码,Ctrl 是按键。

4.20 - 引用

用 include 插入外部文件,用 param 插入站点参数,用 comment 写不会出现在任何输出里的注释。

三个 shortcode 各做一件事:include 把另一个文件的内容放进当前页面,param 打印一个页面或站点参数,comment 丢弃一段内容。适用于跨页复用的片段与散落在多页的常量:同一段安装步骤出现在三页时用 include,版本号出现在几十页时用 param,改一处即可。只在一页出现的内容写在那一页。

最简例子

include 只有一个必填参数 file

源码
{{< include file="parts/install-oink.zh.md" >}}

被引的文件是一段普通 Markdown,放在 assets/ 下:

assets/parts/install-oink.zh.md
把 OINK 安装到一个已有的 Hugo 站点,三条命令:

```sh
hugo mod init github.com/you/your-site
hugo mod get github.com/pgsty/oink
hugo server
```

> [!NOTE]
> `hugo mod get` 需要本机安装 Go;用离线归档或 submodule 时不需要。

当前发布版本是 {{< param version >}}。

渲染结果与写在本页里相同:代码块有复制按钮,提示块是提示块。

把 OINK 安装到一个已有的 Hugo 站点,三条命令:

hugo mod init github.com/you/your-site
hugo mod get github.com/pgsty/oink
hugo server
说明

hugo mod get 需要本机安装 Go;用离线归档或 submodule 时不需要。

当前发布版本是 v0.6.0。

被引的文件不是一篇独立页面:它不出现在侧栏、不参与翻译配对、没有自己的 URL。

文件位置

file 按下面的顺序解析,第一个命中的胜出:

顺序找哪里写法
1当前页面的页面资源(页面包里的文件)file="config.yaml"
2全局资源 assets/ 下的文件file="snippets/dsn.txt"
3content/ 下的文件:/ 开头是内容根目录,否则相对当前页面所在目录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= 指定高亮语言。引用仓库里的真实配置文件,文档与实际文件不会不一致。

源码
{{< include file="parts/module.zh.yml" code=true lang="yaml" >}}
module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.160.1

代码块与围栏走同一条渲染管线:高亮、行号、复制按钮都有。围栏属性(title=collapsehl_lines=)传不进来,需要它们时把文件内容写成普通代码块

片段内容

片段是页面级 Markdown,在当前页面的上下文里渲染:提示块、表格、列表、图片、步骤与 shortcode 都可以用。上面那段片段结尾的「当前发布版本是 v0.6.0」,是片段里的 {{< param version >}} 在本页展开的结果。

一个片段被两页引用时,两页各自渲染一遍,各自生成标题锚点与代码块 ID,互不冲突。

适合做成片段的内容

安装命令、连接串、支持矩阵、法务声明:会变动、且变动时必须处处同步的内容。只在一页出现的内容写在那一页。

插入站点参数

param 打印一个参数:先查本页 front matter,查不到再查站点配置(Hugo 的 .Param 规则)。

源码
本站发布版本 {{< param version >}},版权起始年 {{< param copyright.from_year >}},
本页 front matter 里写了 `pigsty_pg_major: 18`,这里取到 {{< param pigsty_pg_major >}}。

本站发布版本 v0.6.0,版权起始年 2026, 本页 front matter 里写了 pigsty_pg_major: 18,这里取到 18。

嵌套键用 . 连接,copyright.from_year 取的是 params.copyright.from_year。参数不存在、或者值是 map 与列表而不是标量时构建失败,不会留下空白。

在命令、表格与链接里插参数

param 的输出是转义后的纯文本,可以放进代码围栏、表格单元格与链接地址。安装命令里的版本号适合这么写:

源码
```sh
hugo mod get github.com/pgsty/oink@{{< param tdVersion.latest >}}
```

| 项目 | 值 |
| --- | --- |
| 当前版本 | {{< param version >}} |
| Hugo 下限 | {{< param hugoMinVersion >}} |

[发布说明](https://github.com/pgsty/oink/releases/tag/{{< param tdVersion.latest >}})
hugo mod get github.com/pgsty/oink@v0.6.0
项目
当前版本v0.6.0
Hugo 下限0.160.1

发布说明

站点参数在哪里定义、有哪些可用,见配置总览;页面参数见页面参数

构建期删除的注释

comment 的内容在 HTML、打印、Markdown、RSS 四种输出里都不出现。HTML 注释不同:它留在页面源码里,也会进入 llms.txt

源码
PostgreSQL 18 起 `pg_stat_io` 拆分了 WAL 统计。

{{< comment >}}
待办:v0.5 发布后把上面的版本号换成 19,并补一张 pg_stat_io 的截图。
这段文字不会出现在任何输出里,包括 llms.txt。
{{< /comment >}}

升级前先在测试库上验证监控面板。

PostgreSQL 18 起 pg_stat_io 拆分了 WAL 统计。

升级前先在测试库上验证监控面板。

上面两段之间有一段注释,查看页面源码也找不到它。

输出形态

输出include(Markdown)include code=trueparamcomment
HTML片段渲染成正常内容高亮代码块 + 复制按钮转义后的纯文本
打印同 HTML同 HTML,无复制按钮同 HTML
Markdown片段的源码原样输出源码围栏值本身
RSS同 HTML同 HTML同 HTML

Markdown 输出里片段是源码而不是 HTML,片段里的 shortcode 保持 {{< param version >}} 的原样。这与「Markdown 输出保留源码」一致,不是漏渲染。三个 shortcode 都不加载脚本。

参数参考

include(只接受具名参数):

file , 路径(必填) , default
解析顺序见文件放在哪;含 ..、文件缺失、空值都构建失败
code , 布尔 , defaultfalse
true 时按代码块渲染;必须写成 code=true,带引号的 code="true" 是字符串,构建失败
lang , 字符串 , default
代码语言;只能与 code=true 同用,单独出现构建失败

其它任何参数名都会构建失败,报错里带文件名与行号。

param(一个位置参数):

参数名 , 字符串(必填) , default
嵌套键用 . 连接;先页面 front matter 后站点 params;缺失或非标量(map / 列表)构建失败

comment 没有参数,成对使用,{{< comment >}}{{< /comment >}} 之间的内容整段丢弃。

限制与常见问题

  • include 不是模板:不能向片段传变量、不能条件引入、不能给引入的代码块加围栏属性(title=collapse)。按平台分版本时写两个片段配标签页
  • 片段的语言要自己维护:include 不做语言回退。中文页引中文片段,英文页引英文片段,两份文件并列存放(install-oink.zh.mdinstall-oink.md)。
  • param 只打印标量:结构化数据(版本矩阵、下载列表)用 data/ 目录里的数据配对应组件渲染。
  • comment 不是「暂时不发布」:内容每次构建都被丢弃,临时下线整页用 draft: true
  • 不把 include 当目录页:一页引入十个片段时,读者需要的是十条链接。
  • 代码块 — 围栏的全部属性,include code=true 用的是同一套渲染
  • 标签页 — 按平台 / 语言分版本的片段
  • 配置总览param 能取到的站点参数
  • 页面参数 — 页面级参数,优先于站点配置

4.21 - Asciinema

把 .cast 终端录像放进页面:文字仍然是可选中的文字,播放器随主题分发,不连 CDN。

asciinema 把一段 .cast 录像渲染成页面里的终端播放器。适用于命令行流程的演示:终端里的文字仍然是文字,可以选中复制,一段六分多钟的安装过程约 190 KB。图形界面的操作用截图或视频,本组件只播放终端录像。播放器与样式随主题分发,构建期不下载、运行期不连 CDN,只有用到它的页面加载这套运行时。

最简例子

只有 file 是必填的:

源码
{{< asciinema file="images/install.cast" >}}
images/install.cast

这段录像是 Pigsty 在一台 Debian 机器上的单机安装,120×36 的终端,约 6 分 40 秒。文件在本站的 static/images/install.cast,路径写站点根路径。放在 assets/ 下也写相对路径:主题先在资源里查找,找不到再当成站点根路径。不写 title 时,窗口标题显示 file 的值。

窗口标题与主题

title 设置窗口标题,theme 设置配色:

源码
{{< asciinema file="images/install.cast" title="Pigsty 单机安装" theme="dracula" >}}
Pigsty 单机安装

theme 默认 auto:跟随站点的深浅色,浅色用 td-light,深色用 td-dark,读者切换配色时播放器就地重挂一次。要固定成某套终端配色时,可选值是播放器自带的 asciinemadraculagruvbox-darkmonokainordsetisolarized-darksolarized-lighttango,以及主题提供的 td-light / td-dark。固定的主题不跟随深浅色,深色站点配 solarized-light 的对比度不合适。终端字体不用单独设置:播放器使用站点的代码字体,与页面上的代码块一致。

速度、起点与封面

长录像用三个参数控制起点:speed 设倍速,startAt 跳过开头,poster 决定未播放时定格的画面。

源码
{{< asciinema file="images/install.cast" title="从第 60 秒开始,两倍速"
  speed="2" startAt="60" poster="npt:1:30" >}}
从第 60 秒开始,两倍速

speedstartAt 是数字(秒),poster 用播放器的 npt: 记法定位时间点,npt:1:30 是第 1 分 30 秒。上面这个播放器停在第 90 秒的画面,点播放从第 60 秒开始。

idleTimeLimit 把静默段压缩到最多 N 秒。这段录像在录制时已经压缩过(.cast 头里是 idle_time_limit: 0.5),此处不必再设。只有录制时没有限制静默时长的文件才需要它。

尺寸与适配

播放器默认按容器宽度缩放(fit="width"),终端的行列数来自 .cast 文件头。cols / rows 可以覆盖它:

源码
{{< asciinema file="images/install.cast" title="只留 16 行高" rows="16" >}}
只留 16 行高

比录像本身小的行列数会裁掉内容,上面这个只显示 36 行里的 16 行。cols / rows 用于修正录像头里的错误尺寸,不是排版工具。要让播放器变矮,重录一次小终端。

fit 的四个值:width(默认,按宽度缩放)、height(按高度)、both(两个方向都装下)、none(不缩放,按字号原样显示,宽终端会溢出)。

循环与预加载

loop 播完自动重播,preload 在页面加载时取回 .cast,点播放不必等待:

源码
{{< asciinema file="images/install.cast" title="循环播放:登录后的第一分钟"
  startAt="0" speed="3" loop="true" preload="true" >}}
循环播放:登录后的第一分钟

autoplay="true" 让页面打开即播。不建议使用:系统的「减少动态效果」偏好只关闭播放器控件的过渡动画,不阻止自动播放。确实需要自动播放时,配上 loop、很短的内容,并且一页只放一个。

放进步骤里

录像放在某一步旁边:文字说明要做什么,录像展示实际输出。

源码
1. 安装依赖,获取安装脚本:

   ```sh
   curl -fsSL https://repo.pigsty.io/get | bash
   ```

2. 执行安装,全程约六分钟:

   {{< asciinema file="images/install.cast" title="pig install" speed="4" >}}

3. 打开 `http://<节点地址>:3000`,用 `admin / pigsty` 登录 Grafana。
{.steps}
  1. 安装依赖,获取安装脚本:

    curl -fsSL https://repo.pigsty.io/get | bash
  2. 执行安装,全程约六分钟:

    pig install
  3. 打开 http://<节点地址>:3000,用 admin / pigsty 登录 Grafana。

一页可以放多个播放器,脚本与样式只加载一次。

录制 cast 文件

主题只负责播放。用 asciinemaasciinema rec --idle-time-limit=2 --cols=100 --rows=28 install.cast 录制,asciinema play install.cast 本地回放确认。

  • 终端宽度控制在 100 列以内,窄屏上仍可读;录制前先 clear
  • 录制前清理密钥:.cast 是纯文本,录像里的每个字符都能 grep 到,提交前检查一遍。
  • 文件放进 static/images/ 或页面包并提交进仓库,不引用外站的 .cast URL。

输出形态

输出呈现
HTML<div class="td-asciinema"> 窗口外框 + 播放器;播放器 CSS/JS 与初始化脚本按需加载,一页一次
打印同 HTML(打印输出也加载播放器);印到纸上只会留下当时的一帧
Markdown同一段容器 HTML 加一个 JSON 配置块;纯文字里能读到的只有窗口标题
RSS同一段静态标记;阅读器不执行脚本,只剩一个空窗口外框

录像不能是唯一的信息来源。关键命令与关键输出要在录像旁边用文字或代码块写一遍:离线读者、llms.txt 的抓取方与打印读者只能看到这些文字。

参数参考

file , 路径(必填) , default
具名或第一个位置参数;先按全局资源找,找不到当站点根路径;带协议的完整 URL 原样透传
title , 纯文本 , defaultfile 的值
窗口标题
theme , 枚举 , defaultauto
auto 跟随站点深浅色;或 td-light td-dark asciinema dracula gruvbox-dark monokai nord seti solarized-dark solarized-light tango
fit , 枚举 , defaultwidth
width height both none;其它值构建失败
cols / rows , 整数 , default来自 .cast 文件头
覆盖终端行列数;比录像小会裁掉内容
speed , 数字 , default1
播放倍速
startAt , 数字(秒) , default0
起播位置
idleTimeLimit , 数字(秒) , default来自 .cast 文件头
静默段最多播这么久
poster , 字符串 , default
未播放时定格的画面,npt:分:秒
autoplay , "true" / 不写 , default
页面加载即播;不建议
loop , "true" / 不写 , default
循环播放
preload , "true" / 不写 , default
页面加载时就取回 .cast
pauseOnMarkers , "true" / 不写 , default
播到章节标记处暂停
markers , 时间:标签,时间:标签 , default
章节标记;见下面的限制,标签目前到不了播放器

布尔类参数比较的是文本 trueloop="true"loop=true 都表示开启,其它值表示关闭。fit 的取值由主题校验,非法值报错并给出参数名。数字类参数(speed cols rows startAt idleTimeLimit)写成非数字会在转换时让构建失败。

限制与常见问题

  • markers 的标签会丢失:主题把 时间:标签 的列表拼成一维数组交给播放器,播放器只接受成对写法,时间轴上会多出没有标签的标记点。需要章节时用录像旁边的文字列表。
  • 播放器需要 JavaScript:禁用脚本时,以及 Markdown 与 RSS 输出里只有一个空窗口,见输出形态
  • 录像不进搜索:站内搜索索引页面文字,录像里出现过的命令搜不到。
  • 不引用远程 .castfile 收到带协议的 URL 会原样透传给播放器,页面因此依赖一个外站。
  • 控制单段长度:超过五六分钟的录像少有人看完,长流程拆成几段短录像,各配一段文字。
  • 代码块 — 关键命令与输出写成可复制的代码块
  • 步骤 — 把录像放在某一步旁边
  • 图片 — 静态截图;终端内容优先用录像,图形界面用截图
  • 引用 — 同一段命令要在几页复用时

5 - 定制站点

站点级配置:品牌、导航、布局、搜索、多语言、多版本、打印与 Agent 输出。

本栏目覆盖站点级配置: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 moduleHugo 本身,行为见 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各内容运行时自己的开关与端点

最小的可用配置只需要前两层:

hugo.yml
title: 产品文档
baseURL: https://docs.example.com/
defaultContentLanguage: zh
enableGitInfo: true

module:
  imports:
    - path: github.com/pgsty/oink
  hugoVersion:
    extended: true
    min: 0.160.1

params:
  offline_search: true
  github_repo: https://github.com/example/product-docs

配置原则

  • 主题默认保守,只写要改的键。交互功能(本地搜索、图片缩放、评论、反馈、深浅色菜单)默认关闭,主题不替站点做策略决定。从一份「完整配置」逐条删减,比按需添加更容易留下用不上的键。
  • 没有主题总开关。不存在 oink.enabled,也没有 params.oink.* 命名空间,更没有在「Docsy 外壳」与「OINK 外壳」之间切换的选项。这一页查不到的开关即不存在。
  • 非法值告警并回退到文档里写明的默认值params.ui.typography: solarizedinvalid params.ui.typography "solarized" (allowed: technical | system) -- using "technical",站点照常构建;footer_style: thinpage_width: hugesection_index: grid 同理。一个笔误因此只降级一个设置,而不是让 hugo server 下每个 URL 都返回 HTTP 500。它也不会因此静悄悄上线:所有发布关卡都带 --panicOnWarning 构建,那条警告在那里仍然是硬失败。
  • 仍有少数情况会中断构建,它们都属于「继续构建就会发布出错误内容」而非「发布出朴素内容」。需要外部端点的功能——PlantUML、Draw.io、Algolia——缺少端点时报错,因为主题不会代为连接公共服务;残缺的上游署名报错,因为半条声明读起来和完整的一模一样。params.offline_search_indexrelease 事实,以及解析不到目标的内容引用同理。

页面级覆盖优先级

Hugo 的 .Param 查找让大部分参数可以逐页覆盖,优先级从高到低:

  1. 页面自己的 front matter;
  2. 祖先分区 _index.md 里的 cascade(离页面越近越优先);
  3. 站点 params

写进 front matter 时要去掉 ui. 前缀。 站点上的 params.ui.scroll_spy 在页面里就写成 scroll_spy。front matter 里出现 ui: 块的话,里面的键没有人读,也没有人报错——某个设置看着没生效时,先对照页面参数核一遍键名。

content/docs/wide-reference.md
---
title: 宽版参考
page_width: wide
navbar_enabled: false
footer_style: slim
scroll_spy: true
---

分区级用 cascade 一次设定整棵子树:

content/docs/_index.md
---
title: 文档
cascade:
  type: docs
  footer_style: slim
  feedback: true
---

覆盖用于真实的内容差异。逐页重建一套视觉系统的配置,会在主题升级后失配。

三项 goldmark 前置

Hugo 不会 把主题模块的 markup 配置合并进站点,这三项必须写在站点自己的 hugo.yml 里,否则属性行、组件 HTML 与数学公式都不工作:

hugo.yml
markup:
  goldmark:
    parser:
      # 块级图片可以带属性行({caption=…}、编号图)
      wrapStandAloneImageWithinParagraph: false
      attribute:
        block: true
    renderer:
      # `{{% … %}}` 型 shortcode 输出的 HTML 必须保留
      unsafe: true
    extensions:
      passthrough:
        enable: true
        delimiters:
          block: [['\[', '\]'], ['$$', '$$']]
          inline: [['\(', '\)']]
  highlight:
    # 代码高亮用 class 输出,深浅色才能各用一套配色
    noClasses: false
  tableOfContents:
    endLevel: 4

attribute.block 时,{.fields} {.steps} {caption=…} 会原样显示成文字;缺 passthrough\(x\) 不会变成公式;缺 unsafe 时步骤与卡片的结构会被转义。

renderer.unsafe: true 同时允许 Markdown 正文里的原始 HTML 通过,面向的是受信任的作者,不是投稿过滤器。内容来自不可信来源时,审查应放在提交流程里。

站点身份与品牌

Hugo 原生顶层键:

title , string
站名,显示在顶栏、<title> 与页脚
baseURL , string
生产域名;子路径部署时带上路径段
copyright , string
版权行的兜底值,params.copyright 未设时按 HTML 原样渲染
enableGitInfo , boolean , defaultfalse
打开后才有「最后修改」与 commit 信息
enableRobotsTXT , boolean , defaultfalse
生成 robots.txt
enableEmoji , boolean , defaultfalse
允许 :smile: 简码

主题参数:

params.logo , string , defaulticons/logo.svg
品牌图标,可指向 assets/ 资源或 static/ 路径,见品牌外观
params.wordmark , string
横向字标;设置后顶栏用它替代「图标 + 站名」
params.description , string
站点描述,页面没有 description 时作为 meta 兜底
params.copyright , string 或 map
字符串按 Markdown 渲染;map 接受 authors from_year to_yearpresent 表示今年)
params.footer_center_info , string , defaultPowered by Oink
页脚中间的行内 Markdown,设为空字符串即隐藏
params.author , string 或 map
RSS 的作者;map 接受 nameemail

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 , list , default[docs, book, blog, swagger]
哪些 type 使用带侧栏的阅读外壳,见布局与页面类型
params.ui.docs_section , string , defaultdocs
文档栏目的根目录名,只用于导航解析
params.ui.blog_section , string , defaultblog
博客栏目的根目录名
params.ui.docs_sidebar_root , enum , defaultsection
section 时 docs 页的侧栏根是文档栏目;home 时是站点首页。非法值告警并回退
params.ui.quick_links , list , default[docs_section, blog_section]
命令面板空查询时列出的顶层菜单 identifier,见命令面板
params.ui.sidebar_root_enabled , boolean , defaulttrue
允许子分区用 sidebar_root_for: self 自成一棵侧栏树
params.ui.sidebar_root_menu , boolean , defaulttrue
侧栏顶部显示栏目切换器;只有一个入口时退化为普通链接
params.ui.section_index , enum , defaultlist
栏目首页子页列表样式:listcards,可按分区覆盖
params.ui.section_index_columns , integer , default2
section_index: cards 时的列数

博客

三个键决定博客栏目的样子。它们作用于 params.ui.blog_section 指定的栏目,每一个都能通过博客根目录的 front matter 或 cascade 按栏目覆盖。

params.ui.featured_image , enum , defaultnone
文章正文里怎么渲染自己的题图:none 不渲染,banner 在标题上方框出一张 16:9 的图,wash 把它铺在文章头部背后、只留十分之一的不透明度。用的就是这一页在卡片与 og:image 里已经在用的那张图,两处不会打架。没有题图的文章在两种模式下都不渲染任何东西
params.ui.blog_index , enum , defaultlist
博客栏目列表页的形态:list 是行列表,cards 是内容卡片网格,卡片带 16:9 题图、日期与栏目行,以及三行摘要。按年分组、分页与 manual_link 在两种形态下行为一致
params.ui.blog_index_columns , integer , default3
blog_index: cards 时的列数;md 到 xl 之间恒为两列,md 以下一列,不受此值影响

作者与系列是 taxonomy 而不是参数,见分类法写博客

params.ui.navbar_enabled , boolean , defaulttrue
是否渲染站点顶栏,可用页面顶层 navbar_enabled 覆盖,见导航与菜单
params.ui.navbar_autohide , boolean , defaultfalse
顶栏收到视口上方,指针进入唤醒区才出现;小于 768px 或粗指针时不生效
params.ui.footer_style , enum , defaultfat
fat 多列网格 + 版权行,slim 只有版权行,none 不渲染。非法值告警并回退
params.ui.dark_mode , boolean 或 map , defaultfalse
true 同时启用深色调色板与主题控件;只要控件写 dark_mode: { show_menu: true }
params.ui.breadcrumb , boolean , defaulttrue
面包屑;设为 false 关闭。顶层分区本来就省略只有一级的面包屑
params.ui.page_context_menu.enable , boolean , defaulttrue
标题旁的页面操作拆分按钮
params.ui.page_context_menu.assistant_links , boolean , defaultfalse
显示「在 ChatGPT / Claude 中打开」;读者点击时完整 URL 会离开本站
params.ui.page_context_menu.links , list , default[]
自定义外部操作,url 支持 {url} {title} {markdown_url} 占位符
params.ui.github_stars , string 或 number
顶栏 GitHub 徽标上的星数,本地常量,不发请求
params.ui.alt_site , map
单语言站在页脚显示的姊妹站链接,必填 label 与绝对 http(s)url

胖页脚的列数据来自 data/footer/<语言>.yaml,不是参数,见导航与菜单

侧栏

params.ui.sidebar_menu_compact , boolean , defaulttrue
只展开当前分支与邻近条目
params.ui.sidebar_menu_foldable , boolean , defaulttrue
允许读者展开/折叠分区
params.ui.sidebar_menu_truncate , integer , default2000
一个分区最多渲染的条目数,超出截断
params.ui.sidebar_cache_limit , integer , default500
站点页数超过它就复用共享导航标记,active 状态改由浏览器还原
params.ui.sidebar_width_min , integer , default220
桌面端拖拽调宽的下限,像素
params.ui.sidebar_width_max , integer , default480
拖拽调宽的上限,像素
params.ui.sidebar_item_overflow , enum , defaultellipsis
ellipsis 长标题省略,wrap 换行
params.ui.sidebar_icon_policy , enum , defaultall
图标密度:all 全部、groups 只有根与有子页的节点、none 全不显示。非法值警告并回落 all
params.ui.sidebar_expand_levels , integer , default2
默认展开的树层级数
params.ui.sidebar_headings , boolean 或 integer , defaultfalse
只对 type: book 生效:在侧栏当前行下展开标题分支;整数取值 2–4,true 等于 2
params.ui.sidebar_enabled , boolean , defaulttrue
左侧栏;设为 false 关掉,通常按页面而不是按站点设置
params.ui.taxonomy_icons , map
按分类复数名指定右栏分组图标,例如 tags: fa-solid fa-tags

侧栏怎么用见布局与页面类型;目录树本身由 content/ 的结构决定,见组织内容

目录 TOC

右栏大纲的层级由 Hugo 原生配置决定,主题只控制跟踪行为:

markup.tableOfContents.startLevel , integer , default2
Hugo 原生:收录的最高标题级别
markup.tableOfContents.endLevel , integer , default3
Hugo 原生:收录的最低标题级别
params.ui.scroll_spy , boolean , defaultfalse
滚动位置跟踪;设为 true 打开活动项高亮

单页隐藏大纲用 front matter notoc: true,见页面参数

翻页与页尾

页尾组件顺序固定为分享 → 反馈 → 页面信息 → 翻页 → 评论,五者独立开关。

params.ui.share , list , default[]
页尾分享目标,按给定顺序渲染,取值来自 x bluesky mastodon facebook linkedin reddit hackernews telegram whatsapp line pinterest weibo chatgpt claude email copy。为空则不出现分享栏。每一项都是纯粹的 intent 链接——没有 SDK、没有 iframe、没有第三方脚本、没有分享计数,见写博客。未知目标告警并丢弃
params.ui.pager_types , list , default[docs, book, blog]
哪些 type 显示上一页/下一页;单页用 front matter pager: false 退出。未知 type 告警并丢弃
params.ui.annotation , boolean , defaulttrue
正文末尾的「最后修改」与出处区块;上游署名由页面的 upstream_link 一族键驱动,见页面参数
params.ui.translation_notice , 语言代码或 false , defaultfalse
权威版本的语言代码,译文页据此显示一条指回原文的说明;页面写 translation_notice: false 退出
params.ui.reading_time , boolean , defaultfalse
页面标题下显示阅读时长
params.ui.book_draft_banner , boolean , defaultfalse
Book 草稿页开头额外加一条横幅

本地搜索默认关闭;打开后命令面板才会出现(顶栏放大镜、Cmd/CtrlK/\)。

params.offline_search , boolean , defaultfalse
生成每语言一份本地索引并启用命令面板,见全文检索
params.offline_search_on_serve , boolean , defaulttrue
hugo server 预览时也构建索引,预览行为与线上一致;站点极大时设 false 跳过以加快本地重建
params.offline_search_index , enum , defaultcontent
索引范围,逐级累加:title heading summary content。非法值构建失败
params.offline_search_summary_length , integer , default70
summary 档摘录截断的字数
params.offline_search_max_results , integer , default10
结果条数上限,同时约束 Lunr 与中文子串兜底
params.ui.landing_search , boolean , defaulttrue
layout: landing 页面是否保留搜索入口
params.ui.command_palette.commands , list , default[]
自定义命令,每条二选一:url 或内置 action;见命令面板
params.gcs_engine_id , string
Google 可编程搜索引擎 ID,启用后引入外部服务
params.search.algolia , map
Algolia DocSearch,必须显式给出 appId apiKey indexName,缺一构建失败

自定义命令的每条记录只接受 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 , boolean , defaulttrue
单键导航(WASD/方向键走树、j/k 跳标题、q/e 翻页、面板与外壳开关)。设为 false 后运行时不进包,见键盘导航

图片缩放

params.ui.image_zoom , boolean , defaultfalse
允许正文图片点击放大;页面用 front matter image_zoom 覆盖。非布尔告警并回退

哪些图片会成为缩放候选见图片

字体排版

params.ui.typography , enum , defaulttechnical
technical 用随主题分发的 Inter / Chakra Petch / IBM Plex Mono;system 只用平台字体栈,不请求品牌字体。非法值告警并回退
params.page_width , enum , defaultnormal
外壳整体宽度:normal wide full,可逐页覆盖
params.reading_width , enum , defaultnormal
Book 页正文的阅读行宽:slim normal wide,不影响外壳

自定义字体与配色走 SCSS 入口而不是 YAML,见品牌外观

评论与反馈

params.comments.enable , boolean , defaultfalse
站点级评论开关,页面用 front matter comments 覆盖,见启用评论
params.comments.type , string , defaultgiscus
目前只有 giscus 会真正渲染
params.comments.giscus.repo , string
承载讨论的 GitHub 仓库,必填
params.comments.giscus.repoId , string
仓库 ID,必填
params.comments.giscus.category , string
讨论分类名,必填
params.comments.giscus.categoryId , string
讨论分类 ID,必填
params.comments.giscus.mapping , string , defaultpathname
页面与讨论的映射方式
params.comments.giscus.term , string
mappingspecificnumber 时的讨论标题或编号;不设置时不输出这个属性
params.comments.giscus.strict , string , default0
严格标题匹配
params.comments.giscus.reactionsEnabled , string , default1
显示主贴表情
params.comments.giscus.emitMetadata , string , default0
向父页面发送讨论元数据
params.comments.giscus.inputPosition , string , defaulttop
输入框在评论列表上方还是下方
params.comments.giscus.theme , string , defaultauto
giscus 主题,auto 跟随站点深浅色
params.comments.giscus.lightTheme , string , defaultlight
浅色模式下使用的 giscus 主题或自定义 CSS URL
params.comments.giscus.darkTheme , string , defaultdark
深色模式下使用的 giscus 主题或自定义 CSS URL
params.comments.giscus.loading , string , defaultlazy
iframe 加载策略
params.comments.giscus.lang , string , default按站点语言推导
giscus 界面语言。不设置时中文站解析为 zh-CN / zh-TW / zh-HK,其它语言取主语言代码,giscus 不支持则回落 en
params.comments.giscus.ariaLabel , string , defaultComments
评论区容器的 aria-label;默认值是英文,多语言站点需按语言各写一份
params.comments.giscus.errorMessage , string , defaultComments could not be loaded.
加载失败时显示的文字;默认值是英文,多语言站点需按语言各写一份
params.ui.feedback.enable , boolean , defaultfalse
页尾「这页有帮助吗」两个按钮;无后端,有 gtag 时记录结构化事件
params.ui.feedback.reasons , boolean , defaulttrue
选「否」后展开四个可选原因

四个 giscus 必填项缺任意一个,评论区就不渲染:不报错,也不出现。

仓库链接与页面信息

params.github_repo , string
内容仓库 URL,解析「编辑本页」「查看历史」「新建子页」「提文档 issue」,见仓库与页面信息
params.github_project_repo , string , defaultgithub_repo
产品仓库 URL,用于「提项目 issue」与顶栏 GitHub 入口
params.github_branch , string , defaultmain
编辑链接指向的分支
params.github_subdir , string
内容站在 monorepo 里的子目录
params.path_base_for_github_subdir , string 或 map
源路径重写;map 形式接受 fromto
params.github_url , , default
已移除,改写 params.github_repo。那份负责提示替代键名的迁移登记表已经删掉,所以旧键现在只是一个没人读的键
params.ui.lastmod_commit , enum , defaultsubject
「最后修改」后面附什么:subject commit 标题、hash 短哈希、none 不附。非法值告警并回退
params.images , string 数组 , default
站点级社交卡片:页面自己没有封面时用它填 og:image;只进元数据,不会渲染成列表缩略图
params.default_featured , , default
已移除,改写 params.images 或栏目 cascade 里的 images。同上,旧键现在只是一个没人读的键

内容运行时

Mermaid、KaTeX、ECharts、Infographic、Asciinema、Swagger UI 与 Redoc 按内容自动检测,页面用到才加载,没有站点开关。需要开关或外部端点的只有这几个:

params.markmap , boolean , defaultfalse
站点级启用思维导图围栏,见思维导图
params.mermaid , map
透传给 mermaid.initialize() 的配置;键名全小写,深色模式自动覆盖 theme
params.plantuml.enable , boolean , defaultfalse
启用 PlantUML 围栏,见 PlantUML
params.plantuml.svg_image_url , string
PlantUML 服务的 SVG 端点,启用时必填,缺失构建失败
params.plantuml.svg , boolean
用内联 SVG 而不是 <img> 渲染
params.drawio.enable , boolean , defaultfalse
启用 .drawio.svg 图片的编辑按钮,见 Draw.io
params.drawio.drawio_server , string
Draw.io 编辑器地址,启用时必填,缺失构建失败
params.highlight_classes , boolean , defaulttrue
代码高亮输出 Chroma class;设 false 回到 Hugo 的行内样式
params.ui.code_copy , boolean , defaulttrue
代码块的复制按钮;设为 false 全局去掉,围栏上的 copy= 仍然优先

数学公式不需要参数,只需要 passthrough 前置

输出格式

主题声明了两种自定义输出格式,但 不替站点打开:要哪种就在 outputs 里写哪种。

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]
  page: [HTML, markdown]
  section: [HTML, RSS, print, markdown]
格式产物说明
HTMLindex.html交互形态,必选
markdownindex.md每页的纯 Markdown 版本,页面操作里的「复制 Markdown」「查看源码」依赖它,见 Agent 支持
LLMSllms.txt主题声明的纯文本格式,通常只挂在 home
print_print/index.html主题声明的整分区打印页,见打印支持
RSSindex.xmlHugo 原生,挂在 section 上让每个栏目都有订阅源

打印输出的两个参数:

params.print.toc , boolean , defaulttrue
打印页开头生成目录;设为 false 不生成
params.print.section_break_wordcount , integer , default50
打印页中一节多少词以上才另起一页

多语言与版本

语言用 Hugo 原生的 languages 块定义,主题只读它建立的翻译关系:

defaultContentLanguage , string , defaulten
不带路径前缀的首要语言
languages.<lang>.label , string
该语言的自称,显示在语言菜单里
languages.<lang>.locale , string
完整 locale,用于 <html lang> 与 SEO
languages.<lang>.weight , integer
语言顺序,也是点击语言图标时的循环顺序
languages.<lang>.title , string
该语言的站名
languages.<lang>.languageDirection , string , defaultltr
RTL 语言设为 rtl

写作侧的对等文件、锚点对齐与缺译回退见多语言

版本相关参数:

params.version , string
当前站点变体的版本标识(不一定是 Git ref),见多版本
params.version_menu , string , defaultVersion
版本菜单的标题
params.version_menu_pagelinks , boolean
切版本时先尝试目标站点的同一路径
params.versions , list
版本条目:version url kindname: '---' 是分隔线
params.archived_version , boolean
顶部显示「这是归档版本」横幅
params.url_latest_version , string
归档横幅里指向最新版的链接
params.time_format_blog , string , defaultMonday, January 02, 2006
博客日期格式,按语言覆盖
params.time_format_default , string , defaultJanuary 2, 2006
其它日期格式,按语言覆盖

其它

taxonomies , map
Hugo 原生:启用 tag: tags / category: categories,见分类体系
params.taxonomy.page_header , list
只在文章头部显示这几种分类;不设则显示全部
services.googleAnalytics.id , string
Hugo 原生:分析脚本只在生产构建注入,见分析与 SEO
module.hugoVersion.min , string , default0.160.1
主题声明的 Hugo 下限,低于它构建失败
module.hugoVersion.extended , boolean , defaulttrue
必须是 Hugo Extended(要编译 SCSS)

验证配置变更

改完配置跑一次严格构建:

hugo --printPathWarnings --panicOnWarning

输出 Total in … 且没有 ERROR / WARN 才算通过。常见报错与原因:

报错片段原因
invalid params.ui.typography预设只有 technicalsystem
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 indexNameAlgolia 三项必须齐全
params.ui.image_zoom must be a boolean写成了字符串 "true"
command … must define exactly one of url or action自定义命令同时给了 urlaction,或两个都没给
invalid params.ui.sidebar_icon_policy …; using all只是警告,但取值拼错了

配置改动还要至少验证三件事:每种语言各一页、缺译页的回退、生产 baseURL 下的链接(子路径部署容易漏)。

主题声明的 Hugo 下限是 0.160.1,当前验证版本是 0.164.0。改动配置后按这两个版本各构建一次,可以及早发现只在新版本可用的特性:

# 下限版本的二进制
/path/to/hugo-0.160.1 --printPathWarnings --panicOnWarning
# 当前验证版本
hugo --printPathWarnings --panicOnWarning

下限版本写在主题的 hugo.yamltheme.toml 里,站点自己的 module.hugoVersion.min 应与它一致。

5.2 - 品牌外观

替换站名、Logo、favicon、主色、深浅色与字体,只需改配置与两个 SCSS 入口文件。

本页覆盖站点外观:站名与 Logo 写在 hugo.yml,配色与字体走 SCSS 入口,页宽与页脚形态是参数。前提是站点已能构建(十分钟上手)。

需要改动的文件有四个:hugo.ymlstatic/ 下的图标、assets/scss/_variables_project.scssassets/scss/_styles_project.scss不要改主题目录里的文件:主题是 Hugo Module,升级时整个目录会被替换。

站名

站名出现在顶栏、浏览器标题与页脚。多语言站每种语言各写一个:

hugo.yml
title: 产品文档

languages:
  en:
    title: Product Docs
    label: English
    locale: en-US
    weight: 1
  zh:
    title: 产品文档
    label: 简体中文
    locale: zh-CN
    weight: 2

顶层 title 是兜底,languages.<lang>.title 优先。

主题默认使用自带的 assets/icons/logo.svg。替换步骤是把图标文件放进站点的 assets/static/,再在配置里指向它。

hugo.yml
params:
  logo: images/product-mark.svg
  wordmark: logo.svg
  • 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.icorel="icon"
static/favicon.svgrel="icon" type="image/svg+xml"
static/favicon-32x32.pngrel="icon"sizes,按尺寸升序输出
static/apple-touch-icon.pngrel="apple-touch-icon"
static/apple-touch-icon-180x180.pngrel="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 自定义属性)。

先改语义色,它决定按钮、链接、提示块的色调:

assets/scss/_variables_project.scss
$primary: #315f8f;
$secondary: #b4762e;
$success: #2c7a4b;
$warning: #9a6700;
$danger: #b42318;

这个文件在 Bootstrap 与 OINK 默认值 之前 加载,是覆盖 Sass 变量的位置。需要引用 Bootstrap 已定义的变量或 map 时,改用 _variables_project_after_bs.scss

品牌层是一组 CSS 自定义属性,浅色和深色 必须成对覆盖,否则一种模式下会漏色:

assets/scss/_styles_project.scss
:root {
  --td-brand-copper: #a66722;
  --td-brand-mark-from: #1d588c;
  --td-brand-mark-to: #a66722;
}

[data-bs-theme='dark'] {
  --td-brand-copper: #e0a35c;
  --td-brand-mark-from: #7fb8e8;
  --td-brand-mark-to: #e0a35c;
}

可覆盖的品牌属性有 --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(品牌渐变)。

深浅色模式

主题默认 不显示 深浅色控件。开启方式:

hugo.yml
params:
  ui:
    dark_mode: true

开启后顶栏出现一个主题控件:点击在浅色与深色之间切换,悬停或键盘聚焦展开「跟随系统 / 浅色 / 深色」。读者的选择存在浏览器本地,没有选择时跟随 prefers-color-scheme。切换脚本在首屏绘制前设置好 data-bs-theme,不会出现主题闪烁。

只要深色调色板、不要控件时写 dark_mode: { show_menu: false, enable: true }dark_mode: false(默认)两者都不启用。

自定义组件在两种模式下都要给出可读的悬停、聚焦、禁用、选中状态,正文对比度至少 4.5:1、大号文字 3:1。

字体

字体有两档预设,在构建期决定,不涉及 JavaScript:

hugo.yml
params:
  ui:
    typography: technical # technical | system
  • technical(默认):界面与正文用随主题分发的 Inter(可变字重,拉丁 / 西里尔 / 希腊 / 越南语子集,中文与 emoji 落到平台字体),标题装饰用 Chakra Petch,代码用 IBM Plex Mono。字体文件都是本地的,不请求 Google Fonts。
  • system:界面、展示、元数据、打印与等宽角色全部回到平台字体栈,浏览器不请求品牌字体。字体文件仍随主题分发,只是不被引用。

非法取值让构建失败(invalid params.ui.typography)。选中的值写入 <html data-td-typography="…">,可在浏览器中确认。

自定义字体

字体角色是七个 CSS 自定义属性,覆盖它们即可,不必查找组件选择器:

属性用在哪
--td-ui-font-family导航、控件与界面文字
--td-body-font-family正文与博客
--td-heading-font-family正文标题
--td-code-font-family代码与终端
--td-display-font-family字标与展示型大标题
--td-meta-font-family技术标签与元数据
--td-print-font-family打印正文

.woff2 放进站点 static/webfonts/,在项目样式里声明字面,再改写角色:

assets/scss/_styles_project.scss
@font-face {
  font-family: 'My Sans';
  font-display: swap;
  font-style: normal;
  font-weight: 400 800;
  src: url('../webfonts/my-sans-variable.woff2') format('woff2');
}

:root {
  --td-ui-font-family: 'My Sans', 'Noto Sans SC', sans-serif;
  --td-body-font-family: var(--td-ui-font-family);
  --td-heading-font-family: var(--td-ui-font-family);
  --td-display-font-family: var(--td-heading-font-family);
}

角色按 CSS 规则继承,只给某类内容换字体也不必复制组件选择器:

assets/scss/_styles_project.scss
body.td-blog {
  --td-body-font-family: 'My Serif', 'Noto Serif SC', serif;
  --td-heading-font-family: var(--td-body-font-family);
}

等宽字体要带中文兜底,否则中英混排的代码块会对不齐:

assets/scss/_styles_project.scss
:root {
  --td-code-font-family: 'My Mono', 'Sarasa Mono SC', 'Noto Sans Mono CJK SC', monospace;
}

从 Docsy 迁移过来的站点不必改写法。旧的 Sass 变量仍然喂进对应角色,写在 _variables_project.scss 里照样生效,优先级高于预设默认值:

旧 Sass 变量喂给的字体角色说明
$td-fonts-serif--td-ui-font-family / --td-body-font-familyDocsy 的界面字体栈,赋值给 $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-familyBootstrap 的正文变量,经 --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-monospacesystem 预设下,项目的显式取值优先于平台等宽栈

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:字体文件与样式都必须是可审查的本地输入。

页宽

hugo.yml
params:
  page_width: normal # normal | wide | full

page_width 控制外壳整体宽度,可逐页或按分区 cascade 覆盖。Book 页另有一个 reading_widthslim / normal / wide),改的是正文阅读行宽,不是外壳。两个键取值非法都让构建失败。

hugo.yml
params:
  ui:
    footer_style: fat # fat | slim | none
  copyright:
    authors: '[产品团队](https://example.com/)'
    from_year: 2026
    to_year: present
  footer_center_info: 'Powered by [Oink](https://oink.pgsty.com)'
  • fat(默认):多列链接网格 + 版权行;
  • slim:只有版权行;
  • none:不渲染页脚。

页面 front matter(含分区 cascade)可以覆盖它,本站的文档栏目用的是 footer_style: slim。无法识别的取值让构建失败。

多列网格的数据在 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>

验证

hugo --printPathWarnings --panicOnWarning
  • 构建输出 Total in …,没有 ERROR / WARN;
  • 页面源码里 <html> 上有 data-td-typography="technical"(或所选的预设);
  • 浏览器中顶栏显示自己的 Logo 与站名,标签页图标是自己的 favicon;
  • 切到深色模式再看一遍正文、表格、提示块、代码块与焦点框。配色改动容易只在一种模式下验证过;
  • 换一种语言,确认站名随之切换。

字体是否已替换,用浏览器开发者工具查任意一段正文的 font-family:应当是自己声明的字面,而不是 Inter

5.3 - 首页与落地页

用一份本地 YAML 组合首页:Hero、卡片、能力面板、时间线、定价、案例、下载。任意页面也能用同一套分区做成落地页。

首页不是模板,是一份数据:data/home/<语言>.yaml 里的 sections 列表决定页面从上到下有哪些分区,每个分区的内容在同一份文件里按名字取。普通页面加 layout: landing 也能用同一套分区。

分区全部在服务端渲染。价格、star 数、截图、头像、下载状态都必须在 Hugo 启动前就存在于仓库中,没有分区会在浏览器里取数据。

从 Docsy 的 blocks/* 首页迁移过来的站点要重写首页:主题没有 blocks/coverblocks/sectionblocks/feature 这些 shortcode,保留它们会让构建报 template for shortcode "blocks/cover" not found。两条出路是本页讲的 data/home/<语言>.yaml,或者给一个普通页面加 layout: landing

首页的数据来源

首页的内容文件只留标题与描述:

content/_index.zh.md
---
title: OINK
description: 本地优先、仅依赖 Hugo 的技术文档主题
---

分区数据按语言分文件:

首页数据

  • data/
    • home/
      • en.yaml英文站首页
      • zh.yaml中文站首页

查找顺序是 data/home/<当前语言>.yamldata/home/en.yaml → 单语言站点的 data/home.yaml

文件结构只有两层:一个 sections 列表,加上被列表引用的同名键。

data/home/zh.yaml 的骨架
sections:
  - hero          # 用 hero: 键的数据
  - capabilities
  - type: cards   # 用 cards 分区,但读 release: 键的数据
    key: release
  - cta

hero: { … }
capabilities: { … }
release: { … }
cta: { … }

这是本站首页的写法,完整文件见仓库的 data/home/zh.yaml

最小可用首页

粘贴下面这段,替换文字与链接即可发布。链接写成不带前导斜杠的站内路径,主题会补上当前语言前缀(docs/start//zh/docs/start/)。

data/home/zh.yaml
sections:
  - hero
  - cards
  - cta

hero:
  eyebrow: 本地优先 · 仅依赖 Hugo
  title_lines:
    - words:
        - { text: PGSTY OINK }
  lead: 组件写在 Markdown 里,资源随主题分发,一份内容产出四种输出。
  image:
    light: images/hero-light.webp
    dark: images/hero-dark.webp
    alt: OINK 工程文档插图
  actions:
    - { label: 十分钟上手, url: docs/start/, icon: fa-solid fa-rocket, style: primary }
    - { label: 看组件, url: docs/components/, style: ghost }

cards:
  eyebrow: 能做什么
  title: 工程文档需要的都在里面
  columns: 3
  items:
    - title: Markdown 原生组件
      desc: 提示块、标签页、参数表、文件树都是 Markdown 语法的一部分。
      icon: fa-solid fa-cubes
      url: docs/components/
    - title: 四态输出
      desc: HTML、打印、Markdown、RSS,同一份内容不丢信息。
      icon: fa-solid fa-file-export
      url: docs/customize/agents/
    - title: 本地优先
      desc: 字体、图标、搜索、图表运行时全部随主题分发,不连 CDN。
      icon: fa-solid fa-plug-circle-xmark
      url: docs/about/features/

cta:
  title: 从一个能跑的双语站点开始。
  text: 克隆文档站,删掉不要的,改成自己的。
  label: 开始使用
  url: docs/start/
  style: primary

Hero

Hero 是首屏,唯一一个带大标题与配图的分区。

data/home/zh.yaml
hero:
  eyebrow: OINK 0.4.0 · 本地优先          # 标题上方的小字,带状态点
  title_lines:                            # 逐行控制的大标题
    - words:
        - { text: PGSTY OINK }
  lead: 一句话说清这是什么。                 # 支持行内 Markdown 与 <br>
  note: 无需 Node.js                       # 带图标的补充行
  note_icon: fa-solid fa-circle-check
  title_size: 4.25rem                     # 只接受 rem / em / px
  image:
    light: images/hero-light.webp
    dark: images/hero-dark.webp           # 只给一个时深浅色共用
    alt: 首屏插图
  media:
    ratio: '1fr 240px'                    # 文案与配图的列宽
    max_width: 240px
    hide_below: md                        # sm | md | lg | xl 以下隐藏配图
  actions:
    - { label: 开始使用, url: docs/start/, icon: fa-solid fa-rocket, style: primary }
    - { label: GitHub, url: 'https://github.com/pgsty/oink', external: true, style: ghost }
  detail: { label: 看看它长什么样, url: docs/about/showcase/ }

不写 title_lines 时用 title,两者都没有时用站点标题。配图是 CSS 背景图,alt 有值时容器带 role="img",无值时对辅助技术隐藏。

align: center 是纯文字的居中首屏:文案块加宽居中,标题自动平衡换行,note 挪到按钮下方。它不接受 image,两者同时出现构建失败。

分区注册表

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 即变成构建失败。

常用分区的最小写法

卡片与能力面板是最常用的两种。cardscolumns 控制列数:

data/home/zh.yaml
cards:
  title: 应用场景
  columns: 4
  link_label: 了解详情
  items:
    - title: 书籍出版
      meta: 长篇
      icon: fa-solid fa-book-open
      desc: 编号图表式例、交叉引用、索引与整本打印。
      url: docs/write/book/

capabilities 是一屏一条能力,右边配一块结构化的视觉面板,visual.type 只能是 shellcomponentscodeimagecard 五种之一:

data/home/zh.yaml
capabilities:
  eyebrow: 价值主张
  title: 工程文档所需的能力,开箱即用
  items:
    - ref: 01 / 工程文档
      title: 为工程师与文档站设计
      url: docs/start/
      motto: 从第一次构建到长期维护都没有额外阻力
      bullets:
        - '开箱即用的[部署上线](docs/admin/deploy/)体验'
        - '自带[全文检索](docs/customize/search/)与[多语言](docs/customize/i18n/)'
      value: 内容团队把时间用在文档上,而不是重复搭站点。
      visual:
        type: code
        title: build.sh
        lines:
          - { class: c, prefix: '# ', text: 一条命令,一份确定性输出 }
          - { class: p, prefix: '$ ', text: hugo --gc --minify }
          - { class: ok, prefix: '✓ ', text: public/ 可以部署 }
另外十种场景分区的最小 YAML

这些片段摘自主题仓库的可执行回归夹具 tests/site/data/landing/demo/en.yaml,字段名可照抄。

metrics:
  title: 事实
  animate: true
  items:
    - { value: 2189, compact: true, label: Stars, source: { label: 本地 CI 数据, url: 'https://example.org/' } }
    - { value: 32, suffix: '+', label: 语言 }

command-box:
  title: 安装
  code: hugo mod get github.com/pgsty/oink
  lang: bash
  note: 复制按钮由按需加载的 Landing 运行时提供。

steps:
  title: 三步上线
  items:
    - { title: 克隆, desc: 复制文档站仓库。 }
    - { title: 配置, desc: 改三处配置。, cmd: { code: hugo server } }
    - { title: 发布, desc: 推上 GitHub Pages。 }

timeline:
  title: 项目历程
  items:
    - { date: '2024', title: 原型, desc: 第一批数据驱动分区。 }
    - { date: '2026', title: 场景组件, desc: Landing 成为可复用外壳。 }

code-plate:
  title: 页面配置
  aria_label: 示例配置
  lang: yaml
  code: |
    layout: landing
    landing: pricing

preview:
  title: 所写即所得
  file: guide.md            # 源码面板抬头里的文件名,默认 page.md
  source: |                 # 右侧用站点自己的渲染钩子渲染这段 Markdown
    > [!TIP] 只用 Markdown
    > 提示块、步骤、标签页,都是普通语法。

    1. 写 Markdown
    2. 运行 `hugo`
    {.steps}

case-study:
  title: 迁移结果
  stats:
    - { value: 12, label: 可复用分区 }
    - { value: 0, label: 远程请求 }
  quote: “一份 YAML 取代了一个定制页面模板。”
  source: 站点维护者

pricing:
  title: 价格
  tiers:
    - name: 社区版
      price: 免费
      period: 永久
      desc: 完整开源能力。
      features: [全部组件, 社区支持]
      cta: { label: 下载, url: docs/start/ }
    - name: 专业版
      featured: true
      price: ¥24K
      period: /年
      features: [优先响应, 发布包]
      cta: { label: 联系我们, url: 'mailto:example@example.org' }

pricing-compare:
  title: 档位对比
  tiers: [社区版, 专业版]
  groups:
    - name: 支持
      rows:
        - { name: 优先响应, cells: [N, Y] }
        - { name: 年费, price_row: true, cells: [免费, ¥24K] }

download:
  title: 下载
  keys: [prd5]

bar-chart:
  title: 构建耗时
  unit: 
  items:
    - { label: 首次构建, value: 12.3, group: cold }
    - { label: 热缓存, value: 1.6, group: warm, note: 同一台机器上的重复构建。 }

download 分区消费的就是发布与下载页里那份 data/download/<key>.yaml,不引入第二套版本模型。

任意页面做落地页

普通内容页加两行 front matter 即成为落地页:全宽画布,保留顶栏、命令面板与页脚,去掉侧栏与目录。

content/pricing.zh.md
---
title: 价格
layout: landing
landing: pricing
---

数据放在与首页平行的目录下,同样按语言分文件:

落地页数据

  • data/
    • landing/
      • pricing/
        • en.yaml
        • zh.yaml

非首页落地页按这个顺序查找数据,找不到则构建失败,不会渲染空页面:

  1. 页面 front matter 里的 sections
  2. data/landing/<key>/<精确语言>.yaml
  3. 单文件 data/landing/<key>.yaml 里的精确语言条目;
  4. 英文或无语言后缀的记录。

数据量小时可以写在 front matter 里,但 landing:sections: 互斥

content/pricing.zh.md
---
title: 价格
layout: landing
sections:
  - type: hero
    data:
      title: 只用 Hugo 发布产品页面
      actions:
        - { label: 阅读文档, url: docs/, style: primary }
  - type: download
    data: { title: 下载, keys: [prd5] }
  - cta
---

分区条目写法

sections 的每一项可以是一个类型名字符串,也可以是一个 Map:

作用
type分区类型;省略时用 key 当类型
key从哪个键取数据,默认与 type 同名;同一种分区用两次时用它区分
data内联数据,不再到顶层查找键
id分区的锚点 ID,默认由 key / type 生成
enabled: false停用这个分区,保留数据
partial换成站点自己的 partial。属于本地模板约定,不是可移植的 Landing 数据

多语言与本地事实

叙事文字优先分语言文件(zh.yaml / en.yaml)。共享的事实记录也可以在字段级回退:<字段>_<精确语言><字段>_<主语言><字段>,语言标签里的 - 规范化成 _。中文站解析 title_zh_cntitle_zhtitle。不接受 camelCase 后缀。

分区里的显示文字是站点数据,不是主题的 i18n 字符串。只有跑马灯暂停、定价状态这类主题自带控件用翻译键。多语言站点的整体配置见多语言

落地页外壳上的几个可选事实也是本地的,写在 hugo.yml 里,运行时不会去取它们:

hugo.yml
params:
  offline_search: true
  ui:
    landing_search: true          # 布尔;只有站点开了 offline_search 才显示命令面板
    github_stars: 2189            # 已提交的数字,不请求 GitHub API
    alt_site: { label: English site, url: 'https://example.com/' }

页脚不属于首页数据:它读 data/footer/<语言>.yaml(单语言站点用 data/footer.yaml),本站两种语言各一份。data/home/<语言>.yaml 里残留的 footer 键会让构建失败并提示新位置。写法见导航与菜单

输出形态

输出呈现
HTML完整的静态分区内容,再按需加载 landing.js 做渐显、计数、复制与主题图片切换
打印内容保留,跑马灯之类的动态面变成静态网格,控件移除
Markdown标题、正文、列表、表格与代码,不带组件 class
RSS不输出 Landing 分区

禁用 JavaScript 后服务端文档仍然完整。跑马灯的副本轨道不进无障碍树,暂停用的是不依赖 JavaScript 的复选框;读者开启减少动态效果偏好时,移动与渐显关闭。

验证

  1. 构建零告警:hugo --printPathWarnings --panicOnWarning。类型写错、数据键不存在、landingsections 同时出现都在这一步暴露。
  2. 打开首页与落地页,逐个分区对照数据文件,每种语言各看一遍。
  3. 禁用 JavaScript 后刷新:内容仍在,只是没有动效。
  4. 深浅色各看一遍,确认 image.light / image.dark 都给对。
  5. 部署到子路径时,确认站内链接与图片都带上了前缀。

5.4 - 导航与菜单

配置顶栏菜单与下拉、栏目切换器、面包屑、页面操作、翻页器和页脚链接。

本页覆盖读者在页面之间移动的入口:顶栏菜单、栏目切换器、面包屑、页面操作、上一页 / 下一页与页脚。侧栏树与目录属于布局与页面类型

导航没有第二套信息架构:顶栏来自 Hugo 的 menus.main,侧栏来自 content/ 的目录结构。主题不读 docs.jsonnavigation.yaml 一类的并行导航树。

顶栏菜单

顶层入口写在各语言的 menus.main 里:

hugo.yml
languages:
  zh:
    menus:
      main:
        - identifier: docs
          name: 文档
          pageRef: /docs
          weight: 20
        - identifier: blog
          name: 博客
          pageRef: /blog
          weight: 50
        - identifier: download
          name: 下载
          pageRef: /download
          weight: 60
          params:
            icon: fa-solid fa-download

weight 越小越靠前。pageRef 指向站内页面,url 指向外链;外链自动加 target="_blank"rel="noopener noreferrer",并带一个外链角标。identifier 是配置里引用这个入口的稳定标识(quick_linkssidebar_root_menu 按它匹配),name 按语言翻译,identifier 不翻译。

菜单项也可以挂在页面 front matter 上,适用于「这一页本身就是一个顶层入口」:

content/download/_index.md
---
title: 下载
menu:
  main:
    weight: 30
---

顶栏右侧的 GitHub 入口 不是 菜单项,它来自 params.github_project_repo(未设时回落 params.github_repo)。标识为 github 的菜单项会被菜单区跳过,写了也不显示。改变这个入口的目标要改仓库参数,见仓库与页面信息

下拉菜单

用 Hugo 的 parent 建立父子关系,只支持一级子项

hugo.yml
menus:
  main:
    - identifier: docs
      name: 文档
      pageRef: /docs
      weight: 20
    - identifier: docs-start
      parent: docs
      name: 快速上手
      pageRef: /docs/start
      weight: 10
      params:
        icon: fa-solid fa-rocket
        description: 安装 Hugo,克隆本站,十分钟完成部署
    - identifier: docs-components
      parent: docs
      name: 组件
      pageRef: /docs/components
      weight: 20
      params:
        icon: fa-solid fa-cubes
  • 每个条目都是独占一行的一个图标加一个标题,整个面板是一列宽度适中的 纵向列表。子项的 params.description 只是配置数据,面板不会渲染它。
  • 父级本身是一个普通链接:悬停或键盘聚焦展开面板,点击或回车进入父级页面。没有单独的展开箭头,触屏读者落到父级页面,该页正文同样列出这些链接。
  • 键盘:向下箭头展开并聚焦第一项,Esc 关闭并把焦点还给链接,点击面板外部关闭。
  • 0.5 的 params.columns 参数已退役:设置它会发出构建警告,面板保持单列。
  • 再深一层会发出构建警告并降级成静态分组标题,不会 生成三级悬浮菜单。更深的层级放进侧栏。

菜单图标

小于 lg 时菜单项只剩图标,每个顶层入口都应有一个。图标按这个顺序解析:

  1. 目标页面 front matter 里的 icon
  2. 菜单项自己的 params.icon
  3. 按 identifier / 分区名匹配的内置默认值(docs blog examples community about download github 等);
  4. 都没有时用 fa-solid fa-link

图标写成一对 Font Awesome class,主题本地提供免费版字体:

hugo.yml
menus:
  main:
    - identifier: handbook
      name: 运维手册
      pageRef: /handbook
      weight: 40
      params:
        icon: fa-solid fa-screwdriver-wrench

标签菜单

顶层入口指向 taxonomy 页面(/tags//categories/)时不需要手工配置子菜单:面板自动渲染「标签 + 数量」的 chip 网格,按数量降序排列。

hugo.yml
menus:
  main:
    - identifier: tags
      name: 标签
      pageRef: /tags
      weight: 60

分类怎么启用见分类体系

顶栏控件

顶栏高 50px,从左到右是:品牌(Logo 或字标)、菜单区、搜索、版本、语言、主题、GitHub。首页和 Landing 页面最右侧还固定保留抽屉菜单按钮。顶栏在所有布局上渲染;文档、博客和分类页使用相同控件,但没有这个 Landing 抽屉。

顶栏分为桌面完整形态与紧凑图标形态:

视口状态
lg 及以上完整:品牌、带文字的菜单项、各工具控件;首页/Landing 最后是抽屉按钮
小于 lg紧凑:品牌保留,其余全部右对齐成图标
小于 md顶栏右侧只留搜索与抽屉按钮;版本、语言、主题与快捷键帮助仍在页脚最底层栏中

各控件的开关不在这里:搜索图标要 params.offline_search(见全文检索),版本菜单要 params.versions(见多版本),语言菜单在配置了两种及以上语言时自动出现(见多语言),主题控件要 params.ui.dark_mode(见品牌外观)。

自动隐藏

hugo.yml
params:
  ui:
    navbar_autohide: true

开启后顶栏离开正常流、停在视口上方,指针进入原位置上方 60% 的中间区域(或键盘焦点进入)才滑出,并且覆盖在正文之上,不把正文顶下去。左右各 64px 不属于唤醒区,避免盖住折叠后的侧栏与大纲恢复按钮。

小于 768px、粗指针或纯触屏时自动停用,顶栏始终可见。页面 front matter 顶层的 navbar_autohide 或分区 cascade 可以逐段覆盖。

关闭顶栏

hugo.yml
params:
  ui:
    navbar_enabled: false

也可以只关闭某一页或某一段:

content/docs/_index.md
---
title: 文档
cascade:
  navbar_enabled: false
---

关闭后主题补回原本由顶栏承担的界面:移动端子导航、侧栏顶部的品牌与搜索行、大纲轨道上的工具按钮。这个开关适用于必须独占视口的页面,不作为常规排版偏好。本站的文档栏目使用它:文档页依靠侧栏导航,顶栏是多余的一行。

栏目切换器

侧栏顶部那一行是栏目切换器,决定当前显示哪棵树。入口集合按顺序去重构造:所有顶级栏目 → 全站所有 sidebar_root_for: self 的分区 → 当前解析出的根。

hugo.yml
params:
  ui:
    sidebar_root_enabled: true
    sidebar_root_menu: true

让一棵大子树自成一个根(带版本的 API 参考、独立手册),在它的 _index.md 里:

content/docs/api-v2/_index.md
---
title: API 参考 v2
sidebar_root_for: self
sidebar_root_link_self: true
---

self 让这个分区索引与它的后代都用这棵新树;children 把索引留在父树里,只约束后代。让某个顶层分区不出现在切换器里,在它的 front matter 里设 sidebar_root_menu: false

只有一个入口时切换器退化成一个无边框链接,两个及以上才是下拉菜单。切换器下面的树仍然把栏目首页本身作为第一个链接:切换器选一棵树,根链接选一篇文档。

面包屑与页面操作

普通内容页标题上方是面包屑行,这一行右端同时承载页面操作。顶层分区省略只有一级、与标题重复的面包屑,操作按钮的位置不变。

hugo.yml
params:
  ui:
    breadcrumb: false

面包屑标签取本地化的 linkTitle,层级与侧栏一致。

页面操作菜单

页面操作是标题行末尾的拆分按钮:左半边一键复制本页 Markdown(成功后变成绿色对勾),右侧箭头展开完整菜单。菜单分两组,上半组是取走内容,下半组是改动与产出:

操作出现条件
复制 Markdown 文本站点开了 markdown 输出格式
在 ChatGPT 中打开page_context_menu.assistant_links: true
在 Claude 中打开同上
查看 Markdown 源码markdown 输出格式
查看编辑历史params.github_repo 能解析出源文件路径
编辑本页params.github_repo
新建子页面params.github_repo
提交文档 issueparams.github_repo
提交项目 issueparams.github_project_repo
打印整个分区分区开了 print 输出格式
hugo.yml
params:
  ui:
    page_context_menu:
      enable: true
      assistant_links: false
      links: []

助手入口默认关闭:读者点击时,完整的当前 URL(含 query 与 fragment)会随本地化提示词发给第三方,页面正文不上传。开启前确认 URL 里没有敏感信息,并在隐私说明里披露这个边界。页面可以用布尔型 front matter assistant_links 收紧站点策略,不能反过来替站点开启。

自定义外部操作排在菜单最后,url 支持三个已 URL 编码的占位符:

hugo.yml
params:
  ui:
    page_context_menu:
      links:
        - name: 询问内部助手
          icon: fa-solid fa-wand-magic-sparkles
          url: https://assistant.example.com/new?source={markdown_url}&title={title}

可用占位符:{url}(页面完整地址)、{title}(页面标题)、{markdown_url}(Markdown 版地址)。

在博客根分区及其一级子分区上,左半边变成 RSS 订阅链接,菜单里仍保留「复制 Markdown 文本」。没有 Markdown 输出的页面去掉左半边,箭头变成带文字的「操作」按钮。

这些操作同时是命令面板里的条目。

翻页器

正文末尾的上一页 / 下一页是两个文本链接,顺序与侧栏可见树一致:根页 → 第一篇 → 直到最后一篇。根页没有上一页,末页没有下一页。站点提供 data/docs_nav.json 时,这棵显式树同时决定翻页顺序,以及该文件声明过的 docs / book 栏目的栏目索引顺序——侧栏、翻页器与索引不会再把同一批子页排出三种顺序。文件没有声明的栏目,以及没有这个文件的站点,仍然沿内容树走。见布局与页面类型

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

pager_types 只接受 docsbookblog 三个值,其它取值告警并丢弃。单页退出用 front matter:

content/docs/appendix.md
---
title: 附录
pager: false
---

同一份顺序也写进 <head>:有上一页 / 下一页时输出 <link rel="prev"><link rel="next">,供浏览器与爬虫识别阅读序列。

页面源码
<link rel="prev" href="/zh/docs/customize/home/">
<link rel="next" href="/zh/docs/customize/layout/">

翻页只在 HTML 输出中生效。打印、Markdown 与 RSS 既没有翻页链接,也没有这两个 rel 关系。

翻页器是页尾四件套的第三件(反馈 → 页面信息 → 翻页 → 评论),顺序固定,四者独立开关。

页脚形态由 params.ui.footer_style 决定(fat / slim / none,见品牌外观)。fat 的多列链接网格读 data/footer/<语言>.yaml。它不是菜单,主题没有 menus.footer

data/footer/zh.yaml
brand:
  name: 产品文档
  tagline: 一段简短的**支持 Markdown 的**说明。
  slogan: 贴近产品,给出明确答案。
columns:
  - title: 文档
    links:
      - { label: 快速上手, url: /zh/docs/start/ }
      - { label: 组件, url: /zh/docs/components/ }
  - title: 项目
    links:
      - { label: GitHub, url: https://github.com/pgsty/oink, external: true }
      - { label: 发布记录, url: /zh/blog/release/ }
  • brand.namebrand.logo 不写时回落到站点自己的品牌名、Logo 与字标;taglineslogan 渲染 Markdown。
  • 站内 url 相对当前语言根解析;external: true 在新标签页打开并带 rel="noopener noreferrer"
  • 网格列数等于数据里的列数。
  • 单语言站可以使用 data/footer.yaml
  • 配了 fat 但没有数据时自动降级成 slim,可以先开启再补内容。

fat 页脚的版权行右端有一个折叠箭头,收起或恢复它上方的链接栅格。默认展开,读者的选择存在 localStorage 的 td-footer-collapsed 键里,跨页面保留;slimnone 没有这个按钮,它也与专注模式无关。

只要页脚有渲染,最底层栏右侧就固定保留同一组图标:版本、语言、主题、快捷键帮助。各菜单向上展开;版本按钮只显示分支图标,完整版本名仍保留在选项中。fat 页脚的折叠箭头排在这四项之后。侧栏不再重复这组控件,footer_style: none 则连同页脚一起移除底栏。

版权行与中间那句说明由参数控制,见配置总览

验证

hugo --printPathWarnings --panicOnWarning

改完导航要检查这几处:

  • 构建没有 Navbar menu … supports one interactive child level 警告;出现它说明菜单嵌了三层;
  • 桌面端:父级菜单点击进入父级页面,悬停展开面板,Esc 关闭面板;
  • 窗口缩到 lg 以下:每个顶层入口仍有图标,没有图标的项在这个宽度下是空白;
  • 缩到 md 以下:首页与 Landing 顶栏右侧只剩搜索和抽屉按钮;版本、语言、主题与快捷键帮助固定在 footer 最底层栏;
  • 侧栏顶部的切换器列出所有顶级栏目,当前项有选中标记;
  • 任意文档页按 E / Q 翻页,顺序与侧栏一致,页面源码里有对应的 rel="prev" / rel="next"
  • 打开页面操作菜单,确认该出现的项都在,不该出现的没有(例如未配置 github_project_repo 时的「提交项目 issue」)。

5.5 - 布局与页面类型

用 type 决定一页用哪种外壳,再调侧栏宽度与图标、目录深度、栏目首页样式和页宽。

本页覆盖页面骨架:有没有侧栏、侧栏多宽、目录收几级、栏目首页是列表还是卡片。内容放在哪个目录见组织内容,这里只讲外壳。

规则是 外壳看 type,不看路径。文档可以放在 content/ 下的任何位置,只要给它 type: docs

外壳类型

params.ui.shell_types 列出哪些 type 使用带侧栏的阅读外壳:

hugo.yml
params:
  ui:
    shell_types: [docs, book, blog, swagger]
type外壳
docs文档外壳:左侧栏(栏目切换器 + 目录树)+ 正文 + 右栏大纲
book文档外壳,另加编号目标、reading_width 阅读行宽与草稿横幅
blog文档外壳,侧栏默认展开,标题行左半边是 RSS
swagger文档外壳,正文交给 Swagger UI 或 Redoc,见 API 文档
其它 type普通页面:顶栏 + 单栏正文 + 页脚,没有侧栏

分类页与标签页(taxonomy / term)不在这张表里,但也走同一套外壳。

给一棵子树指定 type 用 cascade,这是把文档放在任意路径的做法:

content/handbook/_index.md
---
title: 运维手册
type: docs
cascade:
  type: docs
---

栏目根只是导航起点

hugo.yml
params:
  ui:
    docs_section: docs
    blog_section: blog

这两个键 不决定外壳,只告诉主题文档树与博客树的根在哪,用于解析侧栏根、快捷入口与默认图标。上面 content/handbook/ 的例子照样有文档外壳,docs_section 保持 docs 不影响它。

需要让 docs 页的侧栏根变成站点首页,而不是文档栏目时:

hugo.yml
params:
  ui:
    docs_sidebar_root: home # home | section

取值只有这两个,其它值构建失败。

文档挂在站点根

以文档为主的站点可以把 docs 分区发布到 URL 根路径,源码仍然放在 content/docs/ 下。这需要三段配置一起给出。

第一段用 Hugo 原生的 permalinks 去掉 URL 里的 docs/ 段:

hugo.yml
permalinks:
  page:
    docs: /:sections[1:]/:slug/
  section:
    docs: /:sections[1:]

第二段让物理站点根索引仍可作为链接目标,但不再争抢同一个输出路径。每种语言的站点根索引(content/_index.mdcontent/_index.zh.md)都要写:

content/_index.zh.md
---
title: 产品文档
build: { render: link }
---

第三段把侧栏根声明为站点首页,让侧栏与翻页共用同一棵树:

hugo.yml
params:
  ui:
    sidebar_root_enabled: true
    docs_sidebar_root: home

docs_sidebar_root: home 之后,站点首页的所有顶层分区都会进入这棵树。博客、社区、下载这类不属于阅读序列的概览分区,在自己的 _index.md 里设 toc_root: true 退出,它们既不出现在树里,也不成为翻页目标:

content/blog/_index.md
---
title: 博客
toc_root: true
---

文档此时与博客、社区等分区共享 URL 根路径。构建加 --printPathWarnings,发布前解决所有重复目标。

落地页

任意页面加 layout: landing 即使用落地页布局:顶栏 + 分区拼装的正文 + 页脚,没有侧栏。数据写法见首页与落地页

hugo.yml
params:
  ui:
    landing_search: true

landing_search: false 会把搜索入口从落地页外壳里去掉,其它页面不受影响。

侧栏

侧栏树来自 content/ 的目录结构,按 weight 排序,有 linkTitle 时用它作为标签。可调的是密度与尺寸:

hugo.yml
params:
  ui:
    sidebar_menu_compact: true
    sidebar_menu_foldable: true
    sidebar_menu_truncate: 2000
    sidebar_width_min: 220
    sidebar_width_max: 480
    sidebar_item_overflow: ellipsis # ellipsis | wrap
    sidebar_expand_levels: 2
  • 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 换行。

折叠状态、宽度与滚动位置保存在读者本地,按语言隔离。小于 md 时侧栏变成带遮罩的抽屉。

单页去掉侧栏用 front matter:

content/docs/fullscreen-report.md
---
title: 全屏报告
sidebar_enabled: false
---

显式导航树 data/docs_nav.json

侧栏树默认从 content/ 推导。站点也可以给出一份显式导航清单,三个条件同时成立时主题改用它渲染:

  • 站点存在 data/docs_nav.json 且其中有 sections 键;
  • 页面的 type 是 docsbook
  • 解析出的侧栏根不是站点首页。

文件是一棵嵌套的节点树。每个节点用 page 指向内容路径,url 是它的链接,children 是子节点;active_path_by_url 记录每个 URL 对应的祖先链,供当前项高亮使用:

data/docs_nav.json
{
  "sections": [
    {
      "page": "/docs/start",
      "url": "/docs/start/",
      "children": [{ "page": "/docs/start/install", "url": "/docs/start/install/" }]
    }
  ],
  "active_path_by_url": {
    "/docs/start/install/": ["/docs/start/"]
  }
}

URL 在比较前去掉语言前缀,一份文件服务所有语言。

这棵树同时决定翻页顺序,侧栏与上一页 / 下一页不会出现两种排序。sections 为空数组时构建失败(data/docs_nav.json does not define any Docs navigation sections),page 指向不存在的页面同样失败(Docs navigation page not found)。带 manual_link 的占位节点与 sidebar_divider 分隔行留在侧栏里,但不会成为翻页目标。

适用场景是导航顺序由外部工具生成的站点,例如从 Sphinx toctree 迁移过来、需要冻结既有章节顺序的手册。顺序由 content/weight 维护时不需要这个文件。

侧栏图标密度

页面 front matter 里的 icon 会出现在侧栏。叶子页全部带图标会降低可读性,用密度策略控制:

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

非法取值只发警告并回落到 all,不让构建失败。本站使用 groups

在侧栏里展开标题

Book 页可以在侧栏当前行下展开 h2–h4 分支,便于在长章节内跳转:

hugo.yml
params:
  ui:
    sidebar_headings: 3 # false | true | 2 | 3 | 4

整数指定展开到第几级(2–4),true 等于 2(只展开 h2),false 关闭。取值超出范围构建失败。只对 type: book 的页面生效,且只在侧栏当前行下展开。

目录 TOC

右栏大纲由 Hugo 从 Markdown 标题生成,收录层级是 Hugo 原生配置:

hugo.yml
markup:
  tableOfContents:
    startLevel: 2
    endLevel: 4
    ordered: false

主题只管跟踪行为:

hugo.yml
params:
  ui:
    scroll_spy: false

默认 关闭 滚动跟踪。设为 true 开启后,大纲绘制连续轨道、高亮当前区段并标出位置。读者可以整体折叠右栏,状态存在本地。小于 xl 时右栏隐藏,大纲内容移进侧栏抽屉。

单页隐藏大纲用 front matter notoc: true

只有进入 Hugo 目录的标题才出现在大纲里:Markdown 型 shortcode({{%/* … */%}})输出的标题会进,普通 shortcode({{</* … */>}})输出的通常不会。结构性标题应留在 Markdown 里。

栏目首页样式

_index.md 的分区会自动列出子页。两种样式:

hugo.yml
params:
  ui:
    section_index: cards # list | cards
    section_index_columns: 2
  • list(默认):每个子页一个标题 + 描述段落;
  • cards:网格卡片,读子页的 title(或 linkTitle)、descriptionicon

可以按分区覆盖,非法取值构建失败:

content/docs/components/_index.md
---
title: 组件
section_index: cards
section_index_columns: 3
---

相关的页面级开关:no_list: true 不列子页;simple_list: true 只输出一个无描述的项目符号列表;子页设 hide_summary: true 把自己从列表里去掉。不要手写子页清单:手写的清单会与侧栏失同步。

页宽

hugo.yml
params:
  page_width: normal # normal | wide | full

normal 是常规阅读宽度,wide 放宽正文栏,full 铺满视口。可以逐页或按分区覆盖;宽表格、大图与 API 参考页常用 wide

content/docs/api/reference.md
---
title: 接口参考
page_width: wide
---

Book 页另有一个 reading_widthslim / normal / wide),改的是正文本身的阅读行宽,不动外壳。两个键取值非法都让构建失败。

顶栏与页脚开关

顶栏与页脚属于逐页的布局决定,写在 front matter 顶层(不在 ui 下),可以用分区 cascade 一次设定:

content/docs/_index.md
---
title: 文档
cascade:
  navbar_enabled: false
  footer_style: slim
---

行为见导航与菜单品牌外观,键的定义见页面参数

验证

hugo --printPathWarnings --panicOnWarning
  • 构建输出 Total in …,没有 ERROR / WARN;
  • 新建的 type: docs 页面有左侧栏。没有则检查 cascade 是否覆盖到该页,以及 shell_types 是否包含这个 type;
  • 拖动侧栏分隔条,刷新后宽度保留,双击恢复默认;
  • 窗口缩到 md 以下时侧栏变成抽屉且可关闭,缩到 xl 以下时大纲移进抽屉;
  • 栏目首页的卡片数量与侧栏子页数量一致;
  • page_width: wide 的页面比相邻页面宽;
  • 文档挂在站点根时,hugo --printPathWarnings 没有重复输出路径的告警。

5.6 - 全文检索

打开本地搜索,控制索引体积与结果排序,让中文查询也能命中。

OINK 的搜索是本地搜索:Hugo 在构建时给每种语言生成一份 JSON 索引,读者的浏览器下载它,在本地完成检索。不需要爬虫、账号、CDN,也不需要联网。主题默认不启用,一行配置即可开启。

搜索的入口是命令面板,打开方式与面板的其余内容见命令面板

打开本地搜索

hugo.yml
params:
  offline_search: true

这一个键决定索引、Lunr 运行时与搜索对话框是否进入页面。三个条件同时成立时页面才带上它们:

  • params.offline_search 为真;
  • 页面是首页,或者用了外壳布局(docs / book / blog / swagger,见布局与页面类型),或者是开着 params.ui.landing_search 的落地页;
  • 当前输出不是打印。

任何一条不成立,构建就不往这个页面里放对话框、索引引用与 Lunr。这些资源不是被隐藏,而是不生成。

hugo server 下索引默认 也会生成,预览行为与线上一致。站点极大、每次改动都重建全站索引明显拖慢预览时,把它关掉:

hugo.yml
params:
  offline_search: true
  # 预览时跳过索引构建,只在超大站点上需要
  offline_search_on_serve: false

控制索引体积

offline_search_index 决定每个页面往索引里写多少内容,因此同时决定两件事:读者能否搜到正文里的词,以及第一次搜索要下载多大的文件。

hugo.yml
params:
  offline_search: true
  offline_search_index: summary
  offline_search_summary_length: 70
  offline_search_max_results: 10
取值索引进去的内容什么时候用
title标题、标签、分类、search_keywords只靠标题定位的超大站
heading上面这些 + 页内各级标题标题写得足够具体时
summary上面这些 + 描述与摘要千页级站点;本站使用这一档
content上面这些 + 全文纯文本默认值,几百页以内适用

其它取值构建失败,报 invalid params.offline_search_index

offline_search_summary_length 是结果行里摘要的截断长度(默认 70),offline_search_max_results 是结果条数上限(默认 10)。这几个键的完整定义在配置总览

每种语言一份索引,预算是未压缩 2 MiB、gzip 512 KiB。

读者搜第一个词之前要先下载整份索引。超过这个量级就把 offline_search_indexcontent 降到 summary

调整排序

页面在 front matter 里影响自己的排名:

content/docs/reference/pgsql.zh.md
---
title: PostgreSQL 参数
search_keywords: [postgres, postgresql, pg, 数据库参数, GUC]
search_boost: 1.5
---

search_keywords 是额外的匹配词,可以写一个字符串,也可以写数组。它是这两个键里更有用的一个:读者搜 pgGUC 即可命中标题只写着「PostgreSQL 参数」的页面。检索时关键词的权重仅次于标题,高于正文。

search_boost 是最终得分的正数乘子,默认 1.0,作用在文本匹配得分之上。1.5 不会把页面固定在第一位,只让它在本来就匹配的结果里前移。零、负数与非数字会告警并按 1.0 处理。

整节的默认值用 cascade 一次设定:

content/docs/_index.zh.md
---
title: 文档
cascade:
  search_boost: 1.25
---

页面自己写的值覆盖继承来的值。本站 docs/ 下的页面按这种方式使用 search_keywords:每页列出中文说法、英文原词与配置键名。

把页面挡在索引外

content/internal/draft-plan.zh.md
---
title: 内部计划
search_exclude: true
---

search_exclude 是唯一写法,exclude_searchexcludeSearch 会让构建失败并提示改名。正文为空的页面不进索引。

索引是任何人都能下载的静态 JSON 文件,不是访问控制。

不该公开的内容不要放进站点,也不要用 search_exclude 保护它。

中文与 CJK

Lunr 不能可靠地给中文分词。面板在查询里检测到 CJK 字符时整条切到子串匹配:逐篇比对标题、关键词、页内标题、描述、正文,命中哪一层给哪一层的分,最后同样乘上 search_boost。两条路径的排序规则一致。

三点需要知道:

  • 中文查询是 子串 匹配。搜「主从复制」只命中连续出现这四个字的位置,搜「复制主从」没有结果。
  • search_keywords 对中文站的收益因此最大:把读者可能使用的同义说法、英文原词、缩写都写进去。
  • 输入法组字期间面板不重算结果,文字上屏后才检索,中文输入不会逐字母刷新结果。

中文搜不到内容时,先确认中文页面进了中文那份索引(见下面的验证),再考虑分词问题。

可选:在线搜索

本地搜索之外,主题保留了两个在线搜索集成,默认关闭。同一时间只启用一种:配置了多个入口时构建告警 You have more than one site-search option configured

启用在线搜索意味着接受对应服务的抓取方式、可用性与隐私边界,这些应写进站点的隐私说明。

Algolia DocSearch

hugo.yml
params:
  search:
    algolia:
      appId: YOUR_APP_ID
      apiKey: YOUR_SEARCH_ONLY_KEY
      indexName: YOUR_INDEX

三个值必须都显式写出,缺一个构建中断:OINK 不会回退到其它项目的公共索引。DocSearch 的 JS 与 CSS 随主题内置,不从 CDN 加载,但每次检索请求都发到 Algolia。需要真实的密钥与索引才能工作,此处不渲染。

Google 可编程搜索

hugo.yml
params:
  gcs_engine_id: YOUR_ENGINE_ID

还需要给结果准备一个落地页:

content/search.md
---
title: 搜索结果
layout: search
---

搜索框把查询提交到 <baseURL>/search/?q=…,结果由 Google 的脚本在那个页面上渲染,需要访问 cse.google.com。同样需要外部服务,此处不渲染。

验证

  1. 构建,确认每种语言各生成了一份索引:

    hugo --printPathWarnings --panicOnWarning
    ls public/offline-search-index.*

    开发构建下文件名是 offline-search-index.zh.json,生产构建加指纹,形如 offline-search-index.zh.7ab….json。一种语言一个文件,缺少某个文件说明那种语言的页面没进索引。

  2. 查看索引内容,这是排查「中文搜不到」的第一步:

    python3 -c "import glob,json; f=sorted(glob.glob('public/offline-search-index.zh*.json'))[0]; \
      d=json.load(open(f)); print(f, len(d)); print(d[0])"

    条目数应接近中文页面数,keywordsboost 字段能看到写进 front matter 的值。

  3. 打开站点,按 /,分别用一个英文词与一个中文词各搜一次。结果按内容根分组,每组的名字是面包屑的第一段。

  4. 子路径部署(站点挂在 https://example.com/docs/ 这类路径下)时,打开浏览器开发者工具的网络面板,确认索引请求带上了子路径。索引请求打到域名根目录并返回 404、页面其余部分正常,是「搜索没结果」最常见的原因。

  • 命令面板 — 搜索的入口,以及面板里的命令与页面动作
  • 键盘导航 — 打开搜索与打开命令的四个单键
  • 多语言 — 分语言索引与缺译回退
  • 配置总览offline_search* 各键的完整定义
  • 页面参数search_keywords / search_boost / search_exclude

5.7 - 命令面板

一个对话框同时承担页面搜索、页面动作与站点命令:如何打开、包含哪些分组、如何添加自定义命令。

命令面板是站点唯一的模态入口:搜索页面、复制本页 Markdown、切换语言、切换版本、跳转到站点自定义链接,都在这一个对话框里完成。它随本地搜索一起装配:params.offline_search 关闭时,面板连同索引与 Lunr 都不进入页面,见全文检索

打开面板

打开方式打开成什么
点顶栏或侧栏的搜索框完整搜索态
/ Ctrl + K完整搜索态;再按一次关闭
/完整搜索态
反斜杠键纯命令态(等于预填了 >
f / c同上两者,由键盘导航提供
在框里输入 > 开头的查询纯命令态

/、反斜杠、fc 都是裸单键,会给输入让行:焦点位于 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 选取,不另写一份清单:

hugo.yml
params:
  ui:
    quick_links: [docs, blog]

值是 menus.main 里条目的 identifier。不写这个键时默认取文档栏目与博客栏目(params.ui.docs_sectionblog_section)。菜单本身怎么配见导航与菜单

自定义命令

站点自己的命令写在 params.ui.command_palette.commands 下,排在内建命令之后,顺序即书写顺序:

hugo.yml
languages:
  zh:
    params:
      ui:
        command_palette:
          commands:
            - id: theme_issues
              title: OINK 问题反馈
              description: 报告或查看主题与文档问题
              url: https://github.com/pgsty/oink/issues
              icon: fa-brands fa-github
              keywords: [缺陷, 支持, 路线图]

上面是本站在用的那一条。字段共七个,写其它键构建失败:

  • id 必填,小写字母开头,只能用小写字母、数字、下划线和短横线;不能与内建动作 ID 重名。
  • title 显示在面板里;description 是它下面那行小字;icon 是一对 Font Awesome class。
  • keywords 是数组,参与匹配但不显示,用于收纳读者可能输入的检索词。
  • urlaction 有且只能有一个url 只接受 http/https 的完整地址、站内路径,或 # 开头的页内锚点;带主机名的地址在新标签打开。action 引用一个内建动作 ID。
不要用 action: 给内建动作起别名

内建动作已经在面板里,再包一层会让同一个功能以两个名字出现两次。

多语言站点把命令写在 languages.<lang>.params.ui.command_palette.commands 下,标题与关键词才能本地化。顺序由默认语言的那份清单决定:其它语言里同 id 的条目只覆盖字段,新增的 id 追加在末尾。各语言的命令顺序因此一致,读者换语言时命令不会换位置。

配置只能给出链接或引用内建动作,不能注入 JavaScript 回调:面板读取的是一份纯数据清单。

页面动作

面板里的「页面操作」与文档标题旁的拆分按钮是同一套实现:同一份动作描述、同一段 URL 生成逻辑、同一个执行器。按钮左半边复制本页 Markdown,右侧箭头展开全部动作。

整组关闭,或只在某些页面关闭:

hugo.yml
params:
  ui:
    page_context_menu:
      enable: true
      # 打开后才会出现「在 ChatGPT / Claude 中打开」
      assistant_links: false
      links: []

enable: false 只移除标题旁的按钮,面板里的对应项保留,面板本身就是命令入口。单页用 front matter 的 page_context_menu: false 覆盖。

assistant_links 默认关闭,原因是读者点击时 当前页面的完整 URL(含查询串与锚点)会被发送到第三方,页面正文不会上传。这是站点级的选择,页面 front matter 里的 assistant_links 只能把它收紧,不能替站点打开。

links 是额外的外部动作,只出现在标题旁的菜单里,不进面板:

hugo.yml
params:
  ui:
    page_context_menu:
      links:
        - name: 在站内讨论区提问
          url: https://github.com/pgsty/oink/discussions/new?title={title}
          icon: fa-solid fa-comments

{url}{title}{markdown_url} 三个占位符会被替换成当前页面的值。

「编辑本页」「查看修改历史」「提 issue」这些动作是否可用,取决于仓库相关的配置,见仓库与页面信息;「复制 Markdown」「查看 Markdown 源码」需要页面开了 markdown 输出,见 Agent 支持

同一个对话框,两条独立的数据来源:

  • 页面结果 来自本地搜索索引。索引未生成或下载失败时,面板照常打开、照常执行命令,页面那部分显示「页面索引暂不可用,操作仍可使用」。
  • 命令与动作 来自页面里内嵌的一段 JSON 清单,不需要网络。

打印态不装配面板,打印输出里没有它;关闭 offline_search 后同样没有面板,此时 fc 静默,不影响正常输入。

验证

  1. 构建后确认命令清单进了页面:

    grep -o 'id="oink-action-manifest"' public/zh/docs/customize/panel/index.html

    没有这一行说明本地搜索没启用,或者这个页面不在外壳布局里。

  2. 打开站点按下 /Ctrl + K,什么都不输入:应该看到快速链接、页面操作、偏好设置、命令四组,顺序如上。

  3. 输入 >:只剩命令与动作。新加的命令应该排在「打开 GitHub 仓库」之后。

  4. 切到另一种语言重复第 3 步,确认命令的标题变了、顺序没变。

  5. 打印预览(/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 或反斜杠键打开命令面板的纯命令态
KCtrlK打开面板;再按一次关闭

/ 和反斜杠属于搜索功能本身,关闭键盘导航后仍然可用;F C 是键盘导航提供的别名,指向同一个面板实例。部分非美式键盘布局上反斜杠不易按到,在面板里输入 > 前缀同样进入纯命令态。面板里有什么见命令面板

保留不占用的键

? 保留不绑定。速查卡挂在页脚最底层栏的问号按钮上,鼠标悬停、键盘聚焦或触摸都能打开,列出当前页面实际可用的按键:单语言站点看不到切换语言那一行。

G GShiftG 和数字键同样保留,可能用作将来的跳转序列。

快捷键的让行规则

所有绑定都是裸单键,凡是可能和输入或弹层冲突的场合一律禁用:

  • 焦点在 input、textarea、select 或 contenteditable 区域里;
  • 正在用输入法组字(中文站的硬约束);
  • 按住修饰键时:C 仍是复制,Shift 仍归浏览器;
  • 命令面板或别的对话框开着,键盘归那个弹层。

评论区在 iframe 中,键事件不冒泡到页面,无需额外隔离。

焦点顺序与无障碍

  • 跳转链接:进入页面后第一次按 Tab 出现的就是「跳转到主要内容」,一步跳过顶栏和侧栏。
  • 真实焦点:树内导航移动的是真正的 DOM 焦点,不是虚拟光标。屏幕阅读器因此读出链接名与「当前页」标记,Enter 是链接的原生行为,Tab 顺序没有被改写。
  • 高对比度:焦点行的底色在 forced-colors 模式下失效,退化为系统高亮色描边。
  • 减弱动效prefers-reduced-motion 打开时,逐节跳转与翻页滚动改为瞬时定位,不做滑行。
  • 速查卡里的键帽与正文里的按键组件是同一套样式。

关闭

全站关闭:

hugo.yml
params:
  ui:
    keyboard_nav: false

单页关闭(交互密集的演示页常常需要),或者用 cascade 按整节关闭:

content/docs/playground.zh.md
---
title: 交互演练场
keyboard_nav: false
---

这个键只接受布尔值,写成 "false" 或其它值时构建失败,报 params.ui.keyboard_nav must be a boolean。完整定义见配置总览

关闭后运行时不进入 JavaScript bundle,而不是加载后再判断。/、反斜杠和 K 属于搜索,仍然可用;页脚折叠链接栅格的箭头不受影响。

验证

  1. 构建后确认速查卡按钮在页面里:

    grep -c 'td-shell-keyboard__trigger' public/zh/docs/customize/keyboard/index.html

    关闭键盘导航且没开本地搜索时,这个按钮整个不生成。

  2. 打开一篇文档,光标停在正文里连按 S:侧栏里应该从当前页那一项开始逐项下移,正文不动。

  3. E 若干次,核对翻页顺序与侧栏从上到下的顺序一致;折叠一个分组再翻,被折叠的页面应该被跳过。

  4. 点进搜索框,按 J:页面 不应该 滚动,字符正常输入。使用中文输入法输入时同理。

  5. 系统里打开「减弱动态效果」,再按 J:应该瞬间定位,没有滑行。

5.9 - 多语言

增加一种语言、并排放置译文、按语言配置菜单与界面文案,并对齐中英标题锚点。

OINK 使用 Hugo 的多语言模型,不额外引入目录约定:配置一个 languages 块,译文与原文并排放在同一个目录里,用文件名后缀区分。以下内容覆盖单语言站点扩展为双语站点需要改动的位置,以及双语站点的两处易错点:资源归属与标题锚点。

启用第二种语言

hugo.yml
defaultContentLanguage: en

languages:
  en:
    label: English
    locale: en-US
    weight: 1
    title: OINK
    params:
      description: A Hugo theme for engineering docs
  zh:
    label: 简体中文
    locale: zh-CN
    weight: 2
    title: OINK
    params:
      description: 为工程而设计的 Hugo 文档主题
      time_format_default: 2006年1月2日
      time_format_blog: 2006年1月2日

上面是本站在用的配置。四个字段的作用:

  • 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.mdindex.zh.md 放在同一个目录里。

页面包里的资源遵循一条规则:文件名不带语言后缀的资源由所有语言共享,带语言后缀的资源只属于那种语言。

  • content/docs/install/
    • index.md英文页
    • index.zh.md中文页
    • topology.webp两种语言都能用
    • screenshot.zh.webp只有中文页能用

正文里引用带后缀的资源时 写不带后缀的名字![截图](screenshot.webp),Hugo 会按当前语言解析。

这条规则有一个推论:页面包里只有 index.zh.md、没有英文对等页时,不带后缀的资源不会分给中文页,它们归属默认语言,而默认语言在这个包里没有页面。此时所有资源都必须带 .zh. 后缀,本站 docs/ 下的中文页面包即是如此。

哪些内容需要翻译:

  • 翻译titledescription、摘要、菜单标签、标签名、图片 alt、提示块正文、shortcode 里面向读者的参数。
  • 保持一致:日期、weight、别名,以及任何影响路由的元数据。两边不一致会导致侧栏顺序在两种语言下不同。
  • 不翻译:命令、配置键、文件名、URL、版本号、产品名、shortcode 名。

按语言分开的配置

三处内容不在 content/ 里,需要各语言各写一份。

菜单 写在各自语言下:

hugo.yml
languages:
  zh:
    menus:
      main:
        - identifier: docs
          name: 文档
          pageRef: /docs
          weight: 20

identifier 两种语言必须一致:命令面板的快速链接与搜索结果分组顺序都按它匹配。菜单的完整写法见导航与菜单

首页数据 按语言取文件:data/home/en.yamldata/home/zh.yaml。当前语言没有对应文件时回退到 en.yaml;单语言站点用一个 data/home.yaml 即可。见首页与落地页

界面文案:主题自带 32 个 locale 的界面字符串。英文、简体中文(zhzh-cn)和繁体中文(zh-tw)经过审校,其余语言保留继承自 Docsy 的翻译,OINK 新增的标签用英文兜底。要改某一条,在站点自己的 i18n/ 下建同名文件,只写要覆盖的键:

i18n/zh.yaml
ui_search: 搜索文档

缺译回退与语言选择器

语言选择器的图标本身是一个链接:点击它按 weight 顺序切到下一种语言(在末尾回到第一种),悬停或键盘聚焦才展开列出全部语言的菜单,触摸屏上菜单不展开,点按即切换。双语站点因此一次点击即可来回切换。

菜单始终列出全部配置的语言,不论当前页有没有译文:

  • 目标语言有译文 → 跳到那一页;
  • 目标语言没有译文 → 跳到那种语言的 首页

回退到首页优于把读者送进 404。代价是读者不一定察觉自己被送到了首页,双语站点应当把「每个页面都有对等译文」作为约束来检查,而不是依赖回退。

缺译不会用原文填充

中文页面不存在时,中文站里就没有这一页:侧栏、搜索索引、翻页顺序都不包含它。

搜索索引也按语言分开:读者在中文页面搜索只命中中文内容。中文查询采用 CJK 子串匹配,细节见全文检索

标题锚点要对齐

Hugo 从标题文本生成 ID,中文标题生成中文 ID:/docs/install/#prerequisites/zh/docs/install/#前置条件 指向同一个位置,却是两个互不相通的锚点,跨语言的深链、目录与页内跳转都会失效。

做法是在译文标题里显式写出原文的 ID:

install.zh.md
## 前置条件 {#prerequisites}

两条纪律:

  1. ID 从 英文页渲染出来的 HTML 里取,不要凭标题文本推断。标题里含行内代码、徽章或 shortcode 时,生成的 ID 与标题文本不一致。
  2. 中英对应页面的标题数量、顺序、ID 必须一致。确实需要在中文里加一节时,给它一个独立、稳定、不与英文冲突的 ID。

本站用一个脚本把这条约束变成 CI 检查,比对的是渲染后的 HTML 而不是源码:

node scripts/check-doc-translations.mjs --public public

新页面从建立时就写显式英文 {#id},成本低于事后回补。

从右向左的语言

在语言下声明书写方向:

hugo.yml
languages:
  ar:
    label: العربية
    locale: ar
    languageDirection: rtl
    weight: 3

<html dir> 随之改变,主题额外加载 Bootstrap 的 RTL 样式表。主题自身的 CSS 全部使用逻辑属性(margin-inline-start 而不是 margin-left),镜像布局自动完成。站点自己写的 CSS 同样要用逻辑属性,否则 RTL 下会错位。

验证

  1. 构建,确认两种语言的产物和索引都在:

    hugo --printPathWarnings --panicOnWarning
    ls public/index.html public/zh/index.html
    ls public/offline-search-index.*
  2. 检查 hreflang:每个页面的 <head> 里,每种语言各一条 rel="alternate",外加一条指向自己的 rel="canonical"

    grep -o 'rel="alternate" hreflang="[^"]*"' public/zh/docs/index.html
  3. 在有译文的页面上展开语言选择器并选择另一种语言,确认停在同一篇文档;在没有译文的页面上重复一次,确认落到目标语言的首页而不是 404。

  4. 两种语言各搜一次同一个概念,确认都有结果。

  5. 双语站点把标题对齐检查接进 CI,见上一节的脚本。

5.10 - 多版本

配置版本切换菜单与归档横幅,并选择多个版本在域名上的部署布局。

产品有多个受支持版本时,文档通常也要分版本。主题提供两项功能:顶栏的版本切换菜单,与旧版本站点上的归档提示横幅。部署布局由站点决定:主题不做跨版本的单次构建,每个版本是一次独立的 Hugo 构建。

版本切换菜单

params.versions 里列出要出现在菜单里的版本。这个列表非空时,顶栏工具区出现一个分支图标的菜单,页脚最底层栏出现同样内容的纯图标向上菜单。

hugo.yml
params:
  # 当前站点是哪个版本
  version: v2.1
  # 菜单的无障碍名称;底栏触发器仍只显示图标
  version_menu: v2.1
  versions:
    - version: v2.1
      url: https://docs.example.com
    - version: v2.0
      url: https://v2-0.docs.example.com
    - version: v1.9
      url: https://v1-9.docs.example.com

菜单项默认显示 version 的值,写了 name 就显示 name。当前项标成选中态,判定方式是条目的 version 等于 params.version,或者条目的 url 等于站点的 baseURL,两者满足其一即可。

没写 url 的条目显示为不可点击的灰项,可用作分节标题;name: '---' 是一条分隔线(分隔线上写 url 会告警)。name 支持行内 Markdown:

hugo.yml
params:
  versions:
    - name: '**当前版本**'
    - version: v2.1
      url: https://docs.example.com
    - name: '---'
    - name: '**历史版本**'
    - version: v1.9
      url: https://v1-9.docs.example.com

同一份列表也是命令面板里「切换版本」的数据来源,菜单与面板不会不一致。

version_menu_pagelinks: true 会把当前页面的路径拼到目标版本的 URL 后面,读者切换版本时 停在同一篇文档

代价是目标版本不一定有这个页面:文档结构在版本间会演进,旧版本没有新增的页面,读者切换过去就是 404。本站关闭这个选项。

单个条目可以覆盖全局设置:

hugo.yml
params:
  version_menu_pagelinks: true
  versions:
    - version: v2.1
      url: https://docs.example.com
    - version: v1.9
      url: https://v1-9.docs.example.com
      pagelinks: false # 这一版结构差异大,只跳首页
判断依据是文档结构的稳定程度,不是版本号的距离

结构稳定时开启,结构变动大时关闭。跳到版本首页多一步操作,仍优于 404。

归档横幅

不再维护的旧版本站点上,向读者说明这是一份快照:

hugo.yml
params:
  archived_version: true
  version: v1.9
  url_latest_version: https://docs.example.com

archived_version: true 时,每个文档页与书籍页正文顶部出现一条横幅,写明当前版本已不再积极维护,并给出指向 url_latest_version 的链接。文案随站点语言本地化,无需自行编写;version 是横幅里显示的版本号。

横幅只出现在文档与书籍页面上,博客和落地页没有。

params.versionparams.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 必须包含那段路径

否则搜索索引、页面动作与资源链接都指向域名根目录:页面看上去正常,搜索却没有结果。这是子路径部署最常见的故障,部署细节见发布上线

验证

  1. 构建后确认版本菜单进了页面:

    grep -c 'nav-version-menu' public/zh/docs/customize/versions/index.html

    params.versions 为空或未配置时,菜单整个不生成。

  2. 看当前版本有没有被标成选中:

    grep -o 'nav-hover-menu__option is-active[^>]*' public/index.html

    一条都没有,说明 params.versionversions 里的 version 字段对不上,或者 baseURL 与该条目的 url 不一致(注意结尾斜杠)。

  3. 逐个访问菜单里的链接。开启 version_menu_pagelinks 时,在一篇旧版本不存在的文档上试一次,确认落点可以接受。

  4. 归档站点上打开任意文档页,横幅应该在正文最上方,语言与站点一致,链接指向最新版本。

  5. /Ctrl + K 打开命令面板,「切换版本」列出的应该是同一份清单。

5.11 - 分类体系

用 tags / categories 给页面加一条横跨目录的索引:术语页、筛选芯片、右栏分类云与顶栏分类面板都是自动的。

目录树只有一条路径,分类体系(taxonomy)给页面加第二条:同一篇 PostgreSQL 备份文档既在「运维」目录下,又能从「备份」标签页找到。启用它只需要 Hugo 的 taxonomies: 配置,术语页、筛选芯片、右栏分类云与顶栏分类面板都由主题自动生成,无需编写模板。

本页带着一个分类:标题下面的「分类: 定制站点」一行,以及右栏目录下面那组带计数的芯片,都不需要在页面上写配置。

启用分类法

分类法由 Hugo 决定,主题不额外提供开关。在 hugo.yml 顶层taxonomies:,键是单数名、值是复数名:

hugo.yml
taxonomies:
  tag: tags
  category: categories

这是本站的配置。三点需要注意:

  • 写了 taxonomies: 之后它就是 完整列表,不是追加。想在自定义分类法之外保留 tags / categories,必须把它们一起列出来。
  • 复数名同时是 URL 段:/zh/tags//zh/categories/
  • 全部关闭:disableKinds: [taxonomy, term]

加一个自己的分类法,例如按产品模块归类:

hugo.yml
taxonomies:
  tag: tags
  category: categories
  module: modules

分类法的显示名:tag tags category categories module modules 这六个键在主题的每个语言文件里都有本地化标题(中文分别是「标签」「分类」「模块」)。其它分类法用复数名的 humanize 结果(productsProducts)。要自己定名字,在 content/<复数名>/_index.md_index.zh.md 里写 title / linkTitle,主题会优先用它:

content/modules/_index.zh.md
---
title: 产品模块
linkTitle: 模块
---

为页面添加标签

front matter 里的键名用 复数名taxonomies 的值那一列),值始终是列表,只有一项也要写成列表:

content/docs/ha/patroni.zh.md
---
title: Patroni 高可用
description: 用 Patroni 管理 PostgreSQL 主从切换。
categories: [高可用]
tags: [PostgreSQL, Patroni, 故障切换]
---

整个栏目共用一个分类时,写在栏目首页的 cascade 里,无需每页重复:

content/docs/customize/_index.zh.md
---
title: 定制站点
linkTitle: 定制站点
icon: fa-solid fa-sliders
cascade:
  categories: [定制站点]
---

本站 docs 的六个栏目都是这样配置的。页面自己写 categories: 会覆盖 cascade,不合并:要在栏目分类之外再加一个,两个都要写出来。

页面上的术语行

文档页与博客页在标题、摘要下面渲染一行已分配的术语,链接指向对应的术语页,本页顶部的「分类: 定制站点」即是。这一行的容器是 .taxonomy-terms-article,按分类法另带一个 .taxo-<复数名> 类,单独调样式时用这两个选择器。

默认列出该页的 全部 分类法,只有 authorsseries 这两个保留复数除外——它们各自有专门的呈现面(署名行与系列横幅),再列一遍标签等于把同一件事说两遍。在 page_header 里点名,就能把它放回去。

只想显示其中几种、并固定顺序:

hugo.yml
params:
  taxonomy:
    page_header: [categories]

这一项由配置总览收录。它不能用来隐藏这一行,见限制

主题认识名字的两个分类法

authorsseries 就是普通的 Hugo taxonomy,按普通方式声明——主题不为它们增加任何参数。主题增加的是各自的一套呈现,所以「声明」本身就是全部开关:

hugo.yml
taxonomies:
  category: categories
  tag: tags
  author: authors
  series: series
复数名声明之后打开了什么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/定制站点/标题是「分类: 定制站点」,下面按日期倒序列出该术语的全部页面,样式与博客列表一致

中文术语的 URL 使用中文字符(浏览器地址栏显示 定制站点,HTML 里是百分号编码),Hugo 不做拼音转写。需要 ASCII URL 时改用英文术语,再在 content/categories/<术语>/_index.zh.md 里用 title 给它一个中文显示名,这是 Hugo 的术语页内容文件机制。

术语页在内容树里没有固定位置,它借用一个:某术语的成员全部位于同一个顶层栏目下时,术语页用那个栏目渲染侧栏树与根链接,读者从文档里点进标签仍留在文档导航中;成员跨栏目时回退到站点级的树。筛选芯片里的「全部」按同一规则处理:只有一个栏目时指向该栏目首页,跨栏目时指向分类法列表页。

筛选芯片只出现在分类法列表页;术语页上换成右栏的分类云。

右栏的分类云

文档页、博客页与术语页的右栏(目录下面)每种分类法一组,芯片带计数,可折叠。这一组是自动的,没有开关:定义了分类法且当前范围内有术语时就会出现。

计数 不是全站计数,而是按顶层栏目统计:先看页面的 type 有没有同名栏目(type: docs 的页面用 /docs/ 这棵树),没有就用页面所在的顶层栏目。博客页上的「标签: release 4」说的是博客里有 4 篇,不是全站有 4 篇。

图标按复数名配置:

hugo.yml
params:
  ui:
    taxonomy_icons:
      categories: fa-solid fa-folder
      tags: fa-solid fa-tags
      modules: fa-solid fa-cubes

categoriestags 的默认值就是上面那两个,其它分类法默认 fa-solid fa-shapes。图标是一对 Font Awesome class,与站点其它地方的图标写法一致。

顶栏菜单里的分类面板

主菜单里指向分类法列表页的条目,会自动变成一块术语芯片面板(按用量降序,带计数),无需手写下拉项:

hugo.yml
languages:
  zh:
    menus:
      main:
        - identifier: tags
          name: 标签
          pageRef: /tags
          weight: 60

pageRef: /tags 与旧式的 url: /zh/tags/ 都能识别:URL 形式的菜单先解析成本站页面再判断类型,从旧配置迁移时不必改写法。菜单本身的其它写法见导航与菜单

双语标签

Hugo 的分类按语言分开统计、分开链接:/categories//zh/categories/ 是两棵互不相干的树,中文页只进中文那棵。术语要在各自语言的 front matter 里各写一遍:

content/docs/ha/patroni.md
categories: [High availability]
tags: [PostgreSQL, Patroni, failover]
content/docs/ha/patroni.zh.md
categories: [高可用]
tags: [PostgreSQL, Patroni, 故障切换]

两条要注意:

  • 同一个词在两种语言里写成同样的字符串(例如 release),得到的仍然是 /categories/release//zh/categories/release/ 两个术语页,各自只统计本语言的页面。不要为了统一而在中文页里写英文词:右栏芯片会显示英文。
  • 分类法的显示名会跟着语言走(上面那六个内置键),但 术语名不会:术语就是你在 front matter 里写的那个字符串,主题不翻译它。英文页里写 高可用,英文站的芯片上显示的就是 高可用

多语言站点的其余部分见多语言

按内容类型开关

主题没有「文档显示、博客不显示」这类开关,控制点是给哪些页面打标签。本站的做法:

内容categoriestags效果
content/docs/**栏目级 cascade(「定制站点」等六个)不打术语行只有一行「分类」
content/blog/**每篇写(releaseoink每篇写(OinkRelease术语行两行,右栏两组芯片

让整个栏目从分类里消失:删掉栏目首页 cascade 里的 categories,不需要别的配置。让某一页不进分类:在它自己的 front matter 里写 categories: [],空列表覆盖 cascade。

验证

页面上看三处:

  • 本页标题下面有一行「分类: 定制站点」;
  • 右栏目录下面有按分类法分组的芯片,每枚带计数;
  • 打开 /zh/categories/ 能看到全部术语的筛选芯片,点任一枚进入术语页。

命令行上查产物:

hugo -d public
ls public/zh/categories/          # 每个术语一个目录
grep -c 'taxonomy-term' public/zh/docs/customize/index.html

主题仓库自带一个针对性检查,验证「不写 taxonomies: 就不生成分类页」与「术语页在中英文下标题正确」两件事:

cd ~/pgsty/oink && python3 bin/check-taxonomy.py

限制

  • page_header: [] 不会 隐藏术语行:空列表被当作未设置,回落到「列出全部分类法」。要去掉这行,就不要给这些页面打标签,或在 assets/scss/_styles_project.scss 里隐藏 .taxonomy-terms-article
  • 右栏分类云没有开关,也没有条数上限;术语数量很多的站点应当减少分类法,配置层面没有裁剪手段。
  • 术语页没有跨语言对等关系:语言切换在术语页上不保证落到「同一个术语的另一种语言」。

5.12 - 仓库与页面信息

把「编辑当前页面」「提交文档议题」「查阅编辑历史」接到你的仓库,并在页尾显示最后修改时间、贡献者与反馈组件。

面包屑行右侧的 操作菜单 里与仓库有关的条目,由几个 github_* 参数推导;页尾的「最后修改」信息行来自 git 历史。前提是内容存放在一个 GitHub 风格的仓库里。

操作菜单里所有跟仓库有关的条目,都由这几个键推导出来:

hugo.yml
params:
  github_repo: https://github.com/pgsty/oink.pgsty.com # 文档源码仓库
  github_project_repo: https://github.com/pgsty/oink # 产品仓库(可选)
  github_branch: main # 默认 main
  github_subdir: '' # 仓库根到 Hugo 站点根的路径

上面是本站的真实配置。填好之后,本页的操作菜单里这几条指向:

菜单条目目标
编辑当前页面…/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/_index.zh.md
---
title: 上游参考
cascade:
  github_repo: https://github.com/OWNER/UPSTREAM
  github_project_repo: https://github.com/OWNER/UPSTREAM
  github_subdir: docs
  path_base_for_github_subdir: content/reference
---

content/reference/api/client.md 因此映射到上游的 docs/api/client.md

path_base_for_github_subdir 的值是正则;源文件名与本地不同名时改用 from / to 映射,例如把每个栏目的 _index.md 对到上游的 README.md

content/reference/_index.zh.md
path_base_for_github_subdir:
  from: content/reference/(.*?)/_index.md
  to: $1/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 隐藏:

assets/scss/_styles_project.scss
.td-page-actions__item[data-oink-action='create_child_page'] {
  display: none;
}

命令面板用的是同一批 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 支持:

hugo.yml
enableGitInfo: true
params:
  github_repo: https://github.com/pgsty/oink.pgsty.com
  ui:
    lastmod_commit: subject # subject | hash | none

页尾出现「最后修改 2026年8月17日 · <commit 主题> (a1b2c3d)」,commit 部分链到 …/commit/<hash>lastmod_commit 三个取值:

取值显示
subject(默认)commit 主题 + 缩写 hash
hashcommit a1b2c3d
none只有日期,不链 commit

写别的值会让构建失败,报 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_nameupstream_copyrightupstream_licenseupstream_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反馈 Feedbackfeedback: true / false
3页面信息 Annotationannotation: false
4翻页器 Pagerdocs / book / blog 开pager: false
5评论 Comments配置完整时开comments: false

顺序对应读者读完最后一段之后依次会做的事:把这页递出去、说一句有没有帮上忙、看看它从哪来、翻到下一页、加入讨论。分享排在最前,因为它是唯一朝外的一块,而且一个决定要把文章转给别人的读者,在被问「这页怎么样」之前就已经决定了。分享栏的配置见写博客

评论的配置在启用评论

反馈组件

一行问题、两个按钮:「这篇文档解决了你的问题吗?」→ 是 / 否。选「否」再展开四个可选原因。默认关闭:

hugo.yml
params:
  ui:
    feedback:
      enable: true
      reasons: true # 选「否」后是否追问原因

只给文档栏目开,用 cascade(博客通常只留评论):

content/docs/_index.md
---
title: 文档
cascade:
  feedback: true
---

行为边界:

  • 点击即完成,没有输入框、没有提交按钮、没有登录。
  • 选择按「页面 + 语言」写进浏览器 localStorage,读者回访时还能看到并修改自己的选择。
  • 站点已有 Google Analytics(gtag)时,发送 docs_feedback 事件,字段 resultsolved / not_solved)、page_pathlanguage;选原因时再发一次,多带 reasonrefinement: true,便于和首次计数区分。没有 analytics 时组件照常工作,只是不上报,它不需要任何后端。
  • 本页启用了评论时,反馈结果下面会多一条「在评论区补充详情」的锚点链接。反馈与 giscus 是两条独立的数据流,主题不会代替读者写评论。

本页在 front matter 里写了 feedback: true(docs 栏目默认关闭),页尾可以看到真实的组件。

贡献者墙

contributors shortcode 渲染一面 GitHub 头像墙,数据来自站点 data/ 目录下的一个文件,不在构建期访问 GitHub

data/contributors.yaml
items:
  - github: Vonng
    name: Ruohang Feng
    role: 主题作者
  - github: pgsty
    name: Pigsty
    role: 项目组织
  - github: gohugoio
    role: 静态站点生成器
    avatar: /icons/logo.svg
源码
{{</* contributors */>}}

字段:github 必填(校验成合法的 GitHub 用户名,重复会让构建失败);name 缺省等于 githubrole 可选;url 缺省是 https://github.com/<github>avatar 可选,不填时渲染成首字母占位块,不发任何网络请求,填写时必须是 http(s):// 或站内根相对路径。

多套名单写多个数据文件,用 data= 指定:

源码
{{</* contributors data="maintainers" */>}}

在 Markdown 与 RSS 输出里,头像墙降级成一串 - [@handle](url) — role 的列表。

本站没有 data/contributors.yaml

上面的例子因此不在本页渲染。放一个数据文件进 data/ 就能看到效果。

验证

  • 点开本页面包屑行右侧的操作菜单,「编辑当前页面」应该指向 github.com/<你的仓库>/edit/<分支>/<源文件路径>,路径要与仓库里的实际路径逐段对应。
  • 从栏目首页(_index.md)再点一次:栏目首页最容易被 path_base_for_github_subdir 的正则改错。
  • 页尾应有「最后修改」行;本地新建、尚未 git commit 的页面没有这一行是正常的。
  • 命令行核对生成的链接:
hugo -d public
grep -o 'data-oink-action="edit_page" href="[^"]*"' \
  public/zh/docs/customize/repository/index.html

5.13 - 打印支持

单页交给浏览器的 Cmd/Ctrl+P,整个栏目用 print 输出格式合成一份连续文档。

单页打印不需要配置:外壳(侧栏、目录、顶栏、按钮)都带 d-print-none,浏览器的 Cmd/Ctrl+P 得到的是一份干净的正文。主题因此没有页面级的「打印本页」按钮。

需要配置的是另一件事:把一整个栏目(或一整本书)连同全部子页面合成一份带目录的连续文档。以下内容覆盖启用方式、打印视图的结构,以及排除页面的做法。

启用整章打印

print 是主题声明的自定义输出格式,主题不替站点打开它。在站点自己的 hugo.yml 里给 section 加上:

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

这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 print 时要把该类型原本有的格式(HTMLRSSmarkdown)一起写全,漏一个就丢一种输出。

开启后,每个栏目多出一个 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/ 这页点它,得到的是整个「定制站点」栏目,不是这一页。

打印视图的结构

打开上面任意一个链接,从上到下是:

  1. 一条提示条:「这是本节的多页打印视图。点击此处打印。返回本页常规视图。」它带 d-print-none,只在屏幕上出现,不进纸。
  2. 栏目标题与摘要。
  3. 全栏目目录,条目编号是 1:2:2.1: 这样的层级号,链接指向文档内的锚点。
  4. 每个页面依次排列,标题变成 1 - 配置总览 这种「编号 - 标题」,描述作为导语,正文原样渲染。

页面顺序是侧栏顺序(weight),子栏目递归展开。第二页起每页都另起一页;第一页是否另起一页,取决于栏目首页自己的正文是否超过 50 个词:首页只有一句话时不单独占一张纸。阈值可以调整:

hugo.yml
params:
  print:
    section_break_wordcount: 120

不需要那份目录:

hugo.yml
params:
  print:
    toc: false

也可以只对某个栏目关闭,写在栏目首页 front matter 里:

content/docs/components/_index.zh.md
---
title: 组件
print:
  toc: false
---

把某些页面排除在外

纯链接页、只有一段跳转说明的页、体积巨大的截图页进纸意义不大。给它们写 no_print

content/docs/about/showcase.zh.md
---
title: 示例站点
no_print: true
---

它只影响整章打印视图,页面自己的 HTML 与浏览器 Cmd/Ctrl+P 不受影响。侧栏分隔项(sidebar_divider)也自动排除。

组件在打印态的形态

打印是四态输出之一,每个组件都有确定的打印形态。整章打印视图与浏览器打印单个页面,规则一致:能交互的降级成静态,可折叠的一律展开

组件打印形态
提示块静态块,折叠型(- / + / DETAILS)全部展开;边框转灰、去底色
标签页标签条消失,所有面板依次展开,每个面板带自己的标题
代码块去掉复制与展开按钮,取消最大高度与滚动,长行改为自动折行
表格满宽静态表,取消横向滚动;表头在跨页时重复
图片图与图注保留,缩放相关的属性被剥掉,宽度收进版心
画廊网格改为竖排堆叠
文件树静态面板,目录全部展开,分栏停在构建期宽度
参数表完整定义列表,两种形态一致
公式静态渲染的 KaTeX / MathML
Mermaid · Markmap · PlantUML照常渲染成图:打印视图仍是一张 HTML 页,这几个运行时照常加载
ECharts · Infographic降级成围栏源码块,不渲染图表
卡片 / 步骤 / 徽章 / 按键静态呈现,内容不变

页面外壳不进纸:侧栏、目录、顶栏、页面操作菜单、反馈组件、标题旁的锚点链接、行内复制按钮。

上表里靠浏览器端运行时绘制的那三种图(Mermaid、Markmap、PlantUML),触发打印前要确认它们已经绘制完成。

浏览器打印样式

主题自带一层 @media print 规则,单页打印与整章打印共用:

  • 纸张 A4,页边距 18mm 16mm 20mm;正文 10.5pt,强制浅色配色。
  • 字体切到 --td-print-font-family 这个排印令牌,见品牌外观
  • 标题不与正文分家(break-after: avoid-page),段落与列表项保留 3 行孤行 / 寡行控制。
  • 表格、图片、块引用、提示块、卡片、标签页尽量不跨页断开;代码块允许跨页,但会自动折行而不是截断。
  • 链接加下划线、转深蓝色,不会在链接后面打印出 URL 文本。需要这个行为的站点自己加:
assets/scss/_styles_project.scss
@media print {
  .td-content a[href^='http']::after {
    content: ' (' attr(href) ')';
    font-size: 0.85em;
    word-break: break-all;
  }
}
  • 收起的 <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.htmlprint/content-book.html,主题会优先用带类型后缀的那个。

整本书的打印(type: book)走另一条路径:章节编号、图表编号与交叉引用都保持全书连续,见书籍出版

验证

hugo -d public
ls public/zh/_print/docs/          # 每个栏目一个目录

再看页面:

  • 浏览器打开 /zh/_print/docs/customize/,确认目录条数等于栏目页数(减去 no_print: true 的页)。
  • 在这个视图里按 Cmd/Ctrl+P,打印预览里应当看不到提示条、顶栏与任何按钮。
  • 找一页含标签页与折叠提示块的(例如标签页),确认预览里所有面板都展开。
  • 打印一份 PDF 通读分页情况,阈值不合适时调整 section_break_wordcount

5.14 - Agent 支持

每一页多产出一份 .md,站点根目录多一份 llms.txt,读者可以把当前页交给 ChatGPT 或 Claude。

HTML 页面里有侧栏、脚本与样式,模型读它要先剥掉这层外壳。OINK 让同一份内容再产出一份纯 Markdown:每页一个 .md,站点根目录一份 llms.txt 索引,页面上一个「复制 Markdown 文本」按钮。三者都是构建期产物,没有运行时服务,也不需要内容协商。

这三件事都要站点自己在 outputs 里声明,主题不替站点打开。

每页一份 .md

markdown 是 Hugo 的内置输出格式。把它加进需要的页面类型:

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

这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 markdown 时要把该类型原本有的格式(RSSprint)一起写全,漏一个就丢一种输出。

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:

<link rel="alternate" type="text/markdown" href="https://oink.pgsty.com/zh/docs/customize/agents/index.md">

.md 的内容

不是把渲染好的 HTML 转回 Markdown,而是 你写的源码:front matter 换成一个 H1 标题加一段引用式摘要,其后是正文原文,shortcode 就地展开成各自的 Markdown 形态。

/zh/docs/customize/print/index.md 的开头
# 打印支持

> 单页交给浏览器的 Cmd/Ctrl+P,整个栏目用 print 输出格式合成一份连续文档。

---

LLMS index: [llms.txt](/zh/llms.txt)

---

单页打印不需要配置:外壳(侧栏、目录、顶栏、按钮)都带 `d-print-none`,浏览器的 `Cmd/Ctrl+P` 得到的是一份干净的正文。

原生 Markdown 形态的组件(提示块、表格、参数表、图片属性行、代码围栏、数据围栏)在 .md 里原样保留源码,模型读到的与你写下的是同一份内容。栏目首页在正文之后还会附一份 Section pages: 子页链接清单。

shortcode 形态各有确定的降级:徽章变成强调文本或链接,按键变成 Ctrl + K标签页变成一段段 **标签名** 小节,参数表变成条目列表。每个组件页的「输出形态」小节写了它自己那一行。

站点没有开 LLMS 输出时,上面那条 LLMS index: 不会出现:主题不指向未发布的文件。

llms.txt

llms.txt 是站点根目录的一份纯文本清单,告诉模型「这个站有什么、机器可读版本在哪」。给 首页 加上 LLMS 输出格式即可生成:

hugo.yml
outputs:
  home: [HTML, markdown, LLMS]

多语言站点每种语言各一份:/llms.txt/zh/llms.txt。内容是自动生成的站点索引:

/zh/llms.txt(节选)
# OINK

> 本地优先、仅依赖 Hugo 的技术文档主题

## Site index

- [Home page](https://oink.pgsty.com/zh/index.md)
- [文档](https://oink.pgsty.com/zh/docs/index.md): OINK 是一款只需 Hugo Extended 的技术文档主题……
- [博客](https://oink.pgsty.com/zh/blog/index.md): Docsy 文章、OINK 工程实践与 OINK 发布注记

## Documentation index

- [简介](https://oink.pgsty.com/zh/docs/about/index.md): 一款只需 Hugo Extended 的技术文档主题……
  - [亮点特性](https://oink.pgsty.com/zh/docs/about/features/index.md): 逐条列出 OINK 与普通 Hugo 主题的差别……
  - [示例站点](https://oink.pgsty.com/zh/docs/about/showcase/index.md): 十四个生产站点在用 OINK……
- [快速上手](https://oink.pgsty.com/zh/docs/start/index.md): 克隆 OINK 文档站,本地预览,替换站点信息,部署到 GitHub Pages。

## Site locales

- [English](https://oink.pgsty.com/index.md)
- [简体中文](https://oink.pgsty.com/zh/index.md)

三段的来源:Site index 是本语言首页加站点主菜单(menus.main,条目有 Markdown 版就链 Markdown 版,带 description 的顺带写上);Documentation indexdocs 栏目的子栏目及其下一层页面,缩进表示层级,每行附上该页的 descriptionSite locales 是站点配置里的全部语言。指向站外的菜单条目(GitHub、issue 跟踪器)会被剔除:它们属于导航外壳,不是本站内容。

改进 llms.txt 的入手处是主菜单与各栏目首页的 description,不是这个模板。

页面上的 Agent 动作

面包屑行右侧的操作菜单里,跟 Agent 有关的是四条:

条目做什么出现条件
复制 Markdown 文本抓取本页 .md 写进剪贴板(悬停时预取,点击后无明显等待)本页有 markdown 输出
查阅 Markdown 源码新标签页打开 .md本页有 markdown 输出
在 ChatGPT 中打开带一句提示词跳转到 ChatGPTassistant_links: true
在 Claude 中打开同上,跳转到 Claudeassistant_links: true

前两条只要开了 markdown 输出就存在。「复制」是拆分按钮的左半边(剪贴板图标),复制成功后短暂显示一个对勾。

后两条默认关闭,要显式打开:

hugo.yml
params:
  ui:
    page_context_menu:
      enable: true
      assistant_links: true

打开之后的边界:读者点击时,运行时用浏览器地址栏里的完整 URL(含真实域名、查询串与锚点)拼一句提示词,中文站是「请阅读 的内容,以便我就此向你提问。」,随后跳转到对方站点。离开本站的只有这个 URL,页面正文不会被上传,后续内容由对方自行抓取。URL 里不要放机密信息,站点也应当在隐私说明里披露这条第三方边界。

页面可以收紧站点策略,不能反向打开:front matter 里 page_context_menu: { assistant_links: false } 关掉本页的助手链接;站点没开时页面写 true 不会生效。整个菜单按页关闭用 page_context_menu: false,见页面参数

命令面板里也能搜到这两条助手动作(用的是同一份动作清单),见命令面板

按页面退出 .md 输出

在页面 front matter 里重写 outputs。它同样是整体替换,只写要保留的格式:

content/legal/terms.zh.md
---
title: 服务条款
outputs: [HTML]
---

要保留 RSS、只去掉 Markdown,就把其它格式列全:

content/blog/_index.zh.md
---
title: 博客
outputs: [HTML, RSS, print]
---

自定义输出

主题用 layouts/all.md 渲染 Markdown 输出,用 layouts/index.llms.txt 生成 llms.txt。站点在自己的 layouts/ 下放同名文件即可整体替换,但 先考虑更窄的做法

  • 按内容类型layouts/blog/single.mdlayouts/docs/list.md 这样带类型的路径只影响那一类内容,主题的打印模板即按此分化(layouts/blog/single.print.html)。查模板查找顺序确认你的组合。
  • 按 shortcode:站点自己的 shortcode 可以加输出格式专属模板,让它在 Markdown 输出里给出更适合机器读的形式。
  • 按页面:少数高价值页面手写内容,成本低于改模板。

llms.txt 的内容由站点结构决定,改模板之前先确认问题不在主菜单或 description

验证

hugo -d public
ls public/zh/llms.txt public/zh/docs/customize/agents/index.md

线上或本地预览用 curl

$ curl -s http://localhost:1313/zh/docs/customize/agents/index.md | head -5
# Agent 支持

> 每一页多产出一份 .md,站点根目录多一份 llms.txt,读者可以把当前页交给 ChatGPT 或 Claude。

$ curl -sI http://localhost:1313/zh/llms.txt | head -3

再检查三处:

  • 任一页 HTML 的 <head> 里有 rel="alternate" type="text/markdown"
  • 面包屑行右侧的复制按钮点击后粘贴,得到的是 Markdown 而不是 HTML;
  • llms.txt 里没有指向站外的链接。

限制

  • 主题产出的机器可读表面只有两样:每页 .mdllms.txt。没有 nav.json,也没有别的结构化目录接口;站点地图仍是 Hugo 自己的 sitemap.xml
  • LLMS 输出格式声明为非替代格式,所以 llms.txt 不会出现在 <head>alternate 链接里,也没有对应的页面操作;它靠约定俗成的根路径被发现。
  • 服务端内容协商(同一个 URL 按 Accept: text/markdown 返回 Markdown)不属于主题范围,要做在托管层。
  • Markdown 输出走 源码 路径:只在浏览器端由 JavaScript 生成的内容(运行时绘制的图表)在 .md 里是围栏源码,不是图。

6 - 维护管理

站点从本机到线上的运维事项:本地预览、发布上线、评论、分析与 SEO、版本升级与排错。

本栏目覆盖内容写完之后的运维事项:在本机预览、构建并部署产物、接入评论与分析、跟随主题版本升级、故障定位。前面五个栏目决定站点的外观与内容,这一栏决定站点能否构建、部署在哪、出问题如何排查。

按任务导航

你要做的事去哪
在本机看到改动本地预览
构建出能部署的 public/本地预览
部署到 GitHub Pages / Cloudflare / Netlify发布上线
部署到 example.com/docs/ 这样的子路径发布上线
让读者在页面底部留言启用评论
接 Google Analytics 或自建统计分析与 SEO
让搜索引擎正确收录分析与 SEO
升到新版主题 / 从 Docsy 或 0.4 迁移版本升级
构建报错、搜不到、页面 404排错与检查

6.1 - 本地预览

用 hugo server 在本机预览改动,用 hugo –panicOnWarning 构建可部署的 public/,不需要 Node 与 CDN。

两条命令覆盖日常工作:hugo server 在本机预览改动,hugo 产出可以部署到任何静态托管的 public/。前提是本机安装了 Hugo Extended(不低于 0.160.1);用 Hugo Module 引入主题时还需要 Go。构建不依赖 Node.js、npm 与 PostCSS,它们只服务于本仓库自身的回归检查。

预览服务器

在站点根目录(hugo.yml 所在的目录)执行:

终端
hugo server

打开 http://localhost:1313/。保存文件后 Hugo 重新构建并刷新浏览器,切换 Git 分支同样触发重建。首次启动较慢:用 Hugo Module 引入主题时,Hugo 要先通过 Go 把模块下载到缓存,之后的启动都走缓存。

常用开关

-D / --buildDrafts , default
draft: true 的页面也构建出来
-F / --buildFuture , default
date / publishDate 在未来的页面也构建出来
-E / --buildExpired , default
expiryDate 已过的页面也构建出来
--disableFastRender , default
每次改动都整站重渲染,不用增量
-M / --renderToMemory , default关(写磁盘)
只在内存里渲染,不落 public/
-N / --navigateToChanged , default
保存哪个页面,浏览器就跳到哪个页面
--bind , default127.0.0.1
监听地址;要让局域网或容器外访问就设 0.0.0.0
-p / --port , default1313
监听端口
--minify , default
预览也压缩输出,用来复现生产环境下的渲染
--printPathWarnings , default
有两个页面写到同一个目标路径时告警

本站开发时用的组合是:

终端
hugo server -DFE \
  --disableFastRender --renderToMemory --minify \
  --printPathWarnings --logLevel info

-DFE-D -F -E 的合写,草稿、未来与过期页面一并构建,写作时新建的页面才可见。

改动没有生效

Hugo 默认开启快速渲染(fast render),只重建它判定受影响的部分。修改布局、配置、data/ 或被 include 引用的文件时,增量判定可能不准,页面看起来没有变化。三步排查:

  1. --disableFastRender 重启,看是否恢复。
  2. 硬刷新浏览器(Cmd/Ctrl + Shift + R),排除浏览器缓存。
  3. 仍未恢复则清缓存后重启。

从其它设备访问

hugo server 默认只监听 127.0.0.1,其它设备访问不到。要在手机或另一台机器上预览:

终端
hugo server --bind 0.0.0.0 --port 1313 --baseURL http://192.168.1.10:1313/

--baseURL 必须写成对方可访问的地址,否则页面能打开,但 CSS 与搜索索引这类走绝对路径的资源会指向 localhost

生产构建

部署产物用 hugo 构建,不用 hugo server

终端
hugo --gc --minify --printPathWarnings --panicOnWarning

产物写入 public/,该目录可以脱离源码树独立部署。四个开关各管一件事:

--gc
构建后清掉 resources/_gen 里不再被引用的缓存资源
--minify
压缩 HTML、CSS、JS 与 XML 输出
--printPathWarnings
两个页面撞到同一个输出路径时告警,多语言站点最常见的静默错误
--panicOnWarning
遇到第一条 WARNING 就让构建失败

--panicOnWarning 需要单独说明。OINK 的多数降级路径是告警而不是报错:giscus 必填键缺失、params.comments.type 取了不支持的值、Hugo 弃用的配置键,都只打一条 WARNING 然后跳过。CI 日志通常无人逐行阅读,这些问题会带到线上。把这个开关写进构建命令,等于要求零告警才算构建通过。

本站 CI 的构建步骤(.github/workflows/pages.yml)是 hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --panicOnWarning,任何一条告警都会让部署停在构建阶段。

baseURL 与构建环境

baseURL 写在 hugo.yml 里,也可以在命令行覆盖:

hugo.yml
baseURL: https://oink.pgsty.com
终端
hugo --minify --baseURL "https://example.com/docs/"

部署到子路径时 --baseURL 必须带上那段路径,细节见发布上线

构建环境用 -e / --environment 选择,hugo 默认 productionhugo server 默认 development。这个选择在 OINK 里有三处可见后果:

  • production 下才输出 <meta name="robots" content="index, follow">,其它环境输出 noindex, nofollow
  • productionrobots.txtAllow: /,其它环境是 Disallow: /
  • production 下才渲染 Hugo 的 Google Analytics 模板,静态资源也才做指纹与 SRI。

预览部署(PR preview、staging)用非 production 环境构建,产物自带不被搜索引擎收录、不上报分析的行为:

终端
hugo --minify --environment staging --baseURL "$PREVIEW_URL"

容器内预览

容器不是必需的。两种情况适合用容器:团队需要固定工具链版本,或不希望在每台开发机上安装 Hugo。

Dockerfile
FROM debian:bookworm-slim

ARG HUGO_VERSION=0.164.0
ARG GO_VERSION=1.26.6
ARG TARGETARCH

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl git \
    && curl -L -o /tmp/hugo.deb \
      "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.deb" \
    && apt-get install -y /tmp/hugo.deb \
    && curl -L -o /tmp/go.tgz \
      "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" \
    && tar -C /usr/local -xzf /tmp/go.tgz \
    && rm -rf /var/lib/apt/lists/* /tmp/hugo.deb /tmp/go.tgz

ENV PATH="/usr/local/go/bin:${PATH}"
WORKDIR /src
EXPOSE 1313
ENTRYPOINT ["hugo"]
CMD ["server", "--bind", "0.0.0.0", "--disableFastRender"]
终端
docker build -t oink-hugo .

# 预览:把站点源码挂进去,顺带挂上 Go 模块缓存
docker run --rm -it -p 1313:1313 \
  -v "$PWD:/src" \
  -v "$HOME/go/pkg/mod:/root/go/pkg/mod" \
  oink-hugo

# 生产构建:覆盖默认的 server 命令
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/src" \
  oink-hugo --gc --minify

镜像里装 Go 的原因:用 Hugo Module 引入主题时,Hugo 需要 Go 解析并下载模块。用 submodule、离线归档或直接克隆的站点可以去掉 Go,镜像会小很多。

不要让 root 写 public/

容器里的进程默认是 root,生成的 public/ 属于 root,宿主机上删不掉。共享环境里用 --user "$(id -u):$(id -g)" 映射用户 ID(上面的生产构建命令已经带了)。

镜像不需要 Node.js、npm 与 PostCSS,也不应出现拉取远程浏览器资源的步骤。网络隔离环境需要预先镜像基础镜像与这两个软件包。

清缓存

Hugo 的中间产物分三处,从轻到重依次清:

public/ , 内容上一次的构建产物
删了页面但线上还在;或用 hugo --cleanDestinationDir 让构建自己清
resources/_gen/ , 内容处理过的图片与编译出的 CSS
换了图片处理参数、换了字体或主色,页面还是旧样子
hugo mod clean , 内容Hugo Module 缓存
换了主题版本但解析出来还是旧的;加 --all 清整个模块缓存
终端
rm -rf public resources/_gen
hugo mod clean          # 只清当前项目用到的模块
hugo mod clean --all    # 清整个模块缓存,下次构建重新下载

public/resources/ 都应该写进 .gitignore,不要提交生成产物。

与主题一起改

同时修改主题与站点时才需要这一节。用 HUGO_MODULE_REPLACEMENTS 把模块临时指向本地 checkout,go.mod 保持不变:

终端
HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> /absolute/path/to/oink' hugo server

本站的 Makefile 封装了这几条命令,要求主题 checkout 在同级目录 ../oink

Makefile 目标
make dev     # 替换为 ../oink 的开发服务器
make check   # 替换为 ../oink 跑完整回归套件(npm test)
make build   # 用 go.mod 里的版本构建
make serve   # 按生产配置起预览服务器
替换只作用于本机

无论用环境变量还是 Go workspace(go work init + HUGO_MODULE_WORKSPACE=go.work),CI 与生产构建都只看 go.modgo.work 记录的是开发机的路径,不能提交。判定一个发布标签是否可用时,去掉替换、用 go.mod 里的版本单独构建一次。

断网构建验证

网络隔离环境的验收要同时覆盖构建阶段与浏览器阶段。六步:

  1. 从一份已校验的主题归档与空的模块缓存开始(hugo mod clean --all)。
  2. 阻断出站 HTTP、HTTPS 与 Go module proxy。
  3. 运行生产构建 hugo --gc --minify --printPathWarnings --panicOnWarning
  4. 浏览产物里两种语言的页面:文档页、博客页、首页、404。
  5. 操作搜索、深浅色切换、图表与内容组件。
  6. 检查子资源来源,确认没有意外的远程主机。

最后一步用主题仓库里的脚本,它不依赖站点的测试框架:

终端
python3 bin/check-output-security.py \
  --public public --base-url https://docs.internal.example.com/

脚本扫描四种输出里的每个 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 追加首方主机。

一次通过只证明当次提交与当次环境。每个主题候选版本、每次随附依赖更新之后都要重跑一遍。

验证

一次干净的生产构建应该是这样:

终端
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --panicOnWarning

看到 Total in … 且没有 ERROR / WARNING 才算通过。然后确认:

  • 日志里没有 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤。出现了说明配置里混进了上游 Docsy 的流程。
  • public/ 下有 sitemap.xmlrobots.txtrobots.txtAllow: /
  • 开了本地搜索的站点,public/ 根下有 offline-search-index.<语言>.json
  • hugo server 打开代表性页面:一个文档页、一个博客页、首页、404,两种语言、两种配色都看一遍。

构建失败或结果不对,去排错与检查

6.2 - 发布上线

把 public/ 部署到 GitHub Pages、Cloudflare Pages 或任何静态托管:baseURL 配对、内容安全策略、验收清单与回滚。

OINK 站点的产物是一个纯静态目录,任何能托管静态文件的地方都能部署,不需要 Node 运行时、服务端渲染或构建插件。托管商一侧只有三件事:用正确的 Hugo 版本执行一条命令、发布 public/、让 baseURL 与最终访问地址一致。

前提是本机已经能完成零告警的生产构建

确定 baseURL

baseURL 是最常见的故障源,失败方式也隐蔽:页面能打开,但搜索索引 404、页面操作链接指向错误位置、部分资源加载失败。

部署到域名根目录:

hugo.yml
baseURL: https://oink.pgsty.com

部署到子路径(https://example.com/docs/)时,路径必须写进 baseURL

hugo.yml
baseURL: https://example.com/docs/

也可以在构建时覆盖,让同一份源码部署到不同位置:

终端
hugo --gc --minify --baseURL "https://example.com/docs/"
不要用 canonifyURLs 修子路径

Hugo 的 canonifyURLs 默认 false,保持这个默认值。OINK 的模板与内容链接都基于 baseURL 解析:路径不对是 baseURL 不对,打开 canonifyURLs 会把本来正确的相对链接一起改写,让问题更难定位。

判断是否配对,看构建后搜索索引的请求路径:浏览器应当去 <baseURL>/offline-search-index.zh.json 取索引,取到别处就是 baseURL 不对。

选一个托管商

源码托管在 GitHub 时,一份 Actions 工作流就够:构建在 Actions 里执行,产物通过 Pages 部署 API 发布,不需要维护 gh-pages 分支。

把下面的文件提交到仓库:

.github/workflows/pages.yml
 1name: Deploy Oink site to GitHub Pages
 2
 3on:
 4  push:
 5    branches: [main]
 6  workflow_dispatch:
 7
 8permissions:
 9  contents: read
10  pages: write
11  id-token: write
12
13concurrency:
14  group: pages
15  cancel-in-progress: false
16
17env:
18  GO_VERSION: 1.26.6
19  HUGO_VERSION: 0.164.0
20  # 同级 checkout 的 workspace 绝不能参与 CI 构建
21  GOWORK: off
22  HUGO_MODULE_WORKSPACE: off
23  HUGO_CACHEDIR: ${{ github.workspace }}/.hugo_cache
24  GOMODCACHE:
25    ${{ github.workspace }}/.hugo_cache/modules/filecache/modules/pkg/mod
26
27jobs:
28  build:
29    name: Build Pages artifact
30    runs-on: ubuntu-latest
31    steps:
32      - name: Checkout
33        uses: actions/checkout@v7
34        with:
35          fetch-depth: 0
36
37      - name: Set up Go
38        uses: actions/setup-go@v6
39        with:
40          go-version: ${{ env.GO_VERSION }}
41
42      - name: Set up Pages
43        id: pages
44        uses: actions/configure-pages@v6
45
46      - name: Install Hugo Extended
47        run: |
48          curl --fail --location --silent --show-error \
49            --output "${RUNNER_TEMP}/hugo.deb" \
50            "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
51          sudo dpkg -i "${RUNNER_TEMP}/hugo.deb"
52
53      - name: Download Hugo module
54        run: go mod download github.com/pgsty/oink
55
56      - name: Build site
57        run: |
58          hugo --cleanDestinationDir --gc --minify --environment production \
59            --printPathWarnings --panicOnWarning \
60            --baseURL "${{ steps.pages.outputs.base_url }}/"
61
62      - name: Upload Pages artifact
63        uses: actions/upload-pages-artifact@v5
64        with:
65          path: public
66
67  deploy:
68    name: Deploy to GitHub Pages
69    environment:
70      name: github-pages
71      url: ${{ steps.deployment.outputs.page_url }}
72    runs-on: ubuntu-latest
73    needs: build
74    steps:
75      - name: Deploy
76        id: deployment
77        uses: actions/deploy-pages@v5

这是本站正在使用的工作流。几处不能删:

  • fetch-depth: 0 — 站点开了 enableGitInfo 时,「最后修改时间」和贡献者信息要读完整 Git 历史,浅克隆会让它们为空。
  • setup-go + go mod download — Hugo Module 方式引入主题时,Hugo 需要 Go 才能解析模块。用 submodule 安装主题的站点改成 submodules: recursive,用离线归档的站点把 themes/oink/ 提交进仓库,这两步都可以去掉。
  • GOWORK: offHUGO_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.yml 里的 baseURL 换成这个域名。发布流程需要产物里带 CNAME 文件时,把它放进 static/CNAME,Hugo 会原样复制到 public/

Cloudflare Pages 从关联的 GitHub / GitLab 仓库构建,并为每个评审分支创建预览部署。构建在平台侧完成,仓库里不用放工作流。

在 Workers & Pages 里导入仓库,选定生产分支:

构建命令
hugo --gc --minify --printPathWarnings --panicOnWarning
构建输出目录
public
HUGO_VERSION
0.164.0(或主题验证过的其它版本)
GO_VERSION
仅 Hugo Module 方式需要;固定一个构建镜像支持的版本
SKIP_DEPENDENCY_INSTALL
1

四点说明:

  1. HUGO_VERSION 必须显式设置,Production 与 Preview 两个环境都要设。Cloudflare v3 构建镜像的默认 Hugo 版本低于 OINK 要求的 0.160.1,不固定版本会在构建镜像更新时静默改变工具链。
  2. SKIP_DEPENDENCY_INSTALL=1 关掉通用依赖安装步骤。OINK 消费端不需要 Node.js,仓库里只给维护工具用的 package.json 不应由平台安装。
  3. Hugo 站点不在仓库根目录时,把 Root directory 设成站点目录,输出目录相对它解析。
  4. 预览部署不要当成生产发布。预览需要用自动生成的 Pages URL 作 base URL 时,构建命令改成 hugo --gc --minify --baseURL "$CF_PAGES_URL",生产发布用规范域名重新构建一次。

检查第一次构建日志:正常的 OINK 消费端构建只有一条 Hugo 命令,不会执行 npm、PostCSS、Autoprefixer,也不会下载主题自有的浏览器资源。

Netlify — 构建命令 hugo --gc --minify,发布目录 public,环境变量 HUGO_VERSION。同样的设置可以写进仓库:

netlify.toml
[build]
command = "hugo --gc --minify --printPathWarnings --panicOnWarning"
publish = "public"

[build.environment]
HUGO_VERSION = "0.164.0"

用 submodule 安装主题就打开递归 submodule 检出;用 Hugo Module 就要求构建环境有 Git 和 Go。生产与预览应使用同一个 Hugo 版本,除非预览环境本来就是用来测升级的。

Vercel — 同样的三件事:构建命令 hugo --gc --minify、输出目录 public、环境变量 HUGO_VERSION。它同样不需要安装 npm 依赖。

任何静态服务器(Nginx / Caddy) — 把 public/ 的内容整个铺上去:

/etc/nginx/conf.d/docs.conf
server {
    listen 80;
    server_name docs.example.com;
    root /var/www/oink;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    error_page 404 /404.html;
}

站点是纯静态的,没有需要转发给应用服务器的路径。

对象存储 — Hugo 自带 deploy 命令,把目标写进配置即可:

hugo.yml
deployment:
  targets:
    - name: aws
      URL: 's3://www.your-domain.tld'
      cloudFrontDistributionID: E9RZ8T1EXAMPLEID

构建之后执行 hugo deploy:它比对远端与 public/ 的差异,只上传变化的文件,并在给了 cloudFrontDistributionID 时使 CDN 缓存失效。不带 --target 时用第一个目标,--dryRun 先看要改什么。两个前提:Hugo 二进制带 withdeployhugo version 的输出里能看到),云厂商凭据由标准环境变量或配置文件提供(AWS 上先用 aws s3 ls 确认)。

离线打包 — 网络隔离环境里,在能联网的机器上构建,把产物打成一个包带过去:

终端
hugo --gc --minify --baseURL "https://docs.internal.example.com/"
tar -czf oink-site-$(date +%Y%m%d).tar.gz -C public .

# 目标机器上
tar -xzf oink-site-20260817.tar.gz -C /var/www/oink

构建时就要用目标环境的 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

终端
hugo --gc --minify --environment staging --baseURL "$PREVIEW_URL"

出来的产物自带 noindex, nofollowDisallow: /,也不会向分析服务上报数据。

内容安全策略

主题自带的运行时、字体与图标都是同源资源,严格的内容安全策略(CSP)因此可行。主题不提供一份通用策略:需要哪些指令由站点启用了什么决定。

改变所需指令的地方有五处:

  • 作者写的行内 HTML 与行内脚本,renderer.unsafe: true 之下由作者负责。
  • ECharts 的 $fn: 回调:回调函数由站点注册到 window.OinkEchartsFunctions,注册脚本的来源要进 script-src
  • 分析脚本:站点自己插入的那段脚本与它上报的目标。
  • 远程 API 规范自建图表服务:落在 connect-srcimg-src
  • giscusscript-srcframe-src 要一起放行。

从只覆盖已审查功能的最小策略起步,逐项放行:不需要回调时让 ECharts 选项保持纯数据,审查作者写的行内脚本,只为站点主动启用的集成添加远程来源。产物里的子资源来源可以先用断网构建验证里的脚本扫一遍。

验收清单

部署完成后按这张表走一遍。前四项是构建期的,后面几项要在真实 URL 上查。

零告警构建
构建命令带 --printPathWarnings --panicOnWarning,日志里有 Total in …
baseURL 正确
页面源码里 <link rel="canonical"> 指向真实生产地址(含子路径)
站点地图
<baseURL>/sitemap.xml 可访问;多语言站点是一个索引,指向 /en/sitemap.xml/zh/sitemap.xml
robots
<baseURL>/robots.txtAllow: / 并带 Sitemap: 行;预览部署应该是 Disallow: /
搜索索引
浏览器能取到 <baseURL>/offline-search-index.<语言>.json,站内搜索有结果
Markdown 输出
任一页面 URL 后面加 index.md 能取到纯文本(站点在 outputs.page 里开了 markdown 时)
llms.txt
<baseURL>/llms.txt<baseURL>/zh/llms.txt 可访问(站点在 outputs.home 里开了 LLMS 时)
两种语言
两边的文档页、博客页、首页都能打开,语言切换落到对应页面而不是首页
外观与交互
深浅色切换、打印视图、代表性组件(提示块、标签页、代码块复制)正常
404
访问一个不存在的路径,看到站点自己的 404 页

sitemap.xmlrobots.txt.mdllms.txt 这几项的开关在配置总览,Agent 输出的细节见 Agent 支持

回滚

静态站点的回滚就是重新发布上一个已知可用的 commit,不要在生产上手工改文件。

  • GitHub Pages:在 Actions 里找到上一次成功的 Deploy Oink site to GitHub Pages 运行,点 Re-run all jobs;或者 git revert 出问题的提交再推一次。
  • Cloudflare Pages / Netlify / Vercel:在部署列表里选上一个成功的部署,用平台的 Rollback / Publish deploy 把它重新设为生产版本。
  • 自建静态服务器:保留上一份 tar.gz,解压覆盖。离线打包里给产物加日期后缀就是为了这一步。

问题出在主题升级而不是内容时,回滚的是 go.mod 里固定的版本,见版本升级

6.3 - 启用评论

用 giscus 把 GitHub Discussions 接成页面底部的评论区,全站开、按页关、跟随深浅色。

OINK 的评论走 giscus:每个页面对应一条 GitHub Discussion,读者用 GitHub 账号登录后发言,维护者在 GitHub Discussions 里审核与管理。主题不提供自建评论后端,也不内置 giscus 以外的服务商。

前提是一个公开的 GitHub 仓库,访客读不到私有仓库的 Discussions。

这是主题里少数对外发请求的功能

启用评论的页面会从 https://giscus.app 加载脚本和 iframe,网络隔离环境里用不了。它默认关闭,只在显式打开时才加载。站点有隐私政策时,这条外部数据边界应当写进去。

准备 GitHub 仓库

  1. 选一个公开仓库存放评论线程,可以就是站点源码仓库。

  2. 在仓库 Settings → General → Features 里勾选 Discussions。

  3. 为该仓库安装 giscus GitHub App。未安装 App 时访客无法评论或表态。

  4. 选一个 Discussion 分类。giscus 推荐 Announcements 类型:只有维护者与 giscus bot 能在该类型下新建 Discussion,读者不会误开话题。

仓库 ID 与分类 ID 是公开标识符,不是凭据。不要往 Hugo 配置里放 personal access token、OAuth secret 或密码。

生成配置

打开 giscus.app,按表单填仓库、映射方式和分类,页面下方会生成一段 <script>。把里面四个属性抄进 OINK 配置:

data-repo
repo
data-repo-id
repoId
data-category
category
data-category-id
categoryId

映射方式(mapping)决定哪个页面对应哪条 Discussion。OINK 默认 pathname,适合发布路径稳定、同一个仓库要服务多个域名或预览环境的站点。开始收集评论之后再改 mapping 或移动页面,giscus 会去找另一条 Discussion:已有评论不会被删除,但页面上再也找不到它们。映射方式要在上线前定好;确实要改 URL 时,同时保留重定向或重命名 Discussion。

全站启用

把生成的标识符写进站点配置:

hugo.yml
params:
  comments:
    enable: true
    type: giscus
    giscus:
      repo: pgsty/oink.pgsty.com
      repoId: R_kgDOTzFZAg
      category: Announcements
      categoryId: DIC_kwDOTzFZAs4DDCm-
      mapping: pathname
      inputPosition: bottom
      theme: auto
      loading: lazy

上面是本站正在使用的配置。reporepoIdcategorycategoryId 四个键缺一不可:任何一个缺失或只有空白字符,Hugo 打一条 WARNING 并跳过 giscus,构建不会失败,因此生产构建要带 --panicOnWarningtype 目前只接受 giscus,写别的值同样是告警加跳过。params.comments 的键名与 Hextra 同形,从 Hextra 迁来的配置可以照搬。

其余的键(strictreactionsEnabledemitMetadatatermlanglightThemedarkThemeariaLabelerrorMessage)都有默认值,完整定义见配置总览。功能开关既可以写 YAML 布尔值,也可以写 giscus 风格的 0 / 1

按页开关

front matter 里的 comments 可以从任一方向覆盖全站开关,离页面最近的值优先。

只给某些页面开评论。全站关掉但保留完整仓库配置,再让选中的页面显式打开:

content/blog/2026-roadmap.md
---
title: 2026 路线图
comments: true
---

只关掉某些页面。全站开着,让不适合讨论的页面退出:

content/about/security.md
---
title: 安全政策
comments: false
---

整个栏目统一设置用 cascade。本站在 content/docs/_index.zh.md 的 cascade 里写了 comments: true,本页底部因此有一个真实的 giscus 评论区。

content/docs/_index.zh.md
---
title: OINK 文档
cascade:
  type: docs
  comments: true
---

站点同时配了 services.disqus.shortname 时,giscus 优先:giscus 生效即抑制 Disqus,comments: false 同时关掉两者,giscus 必填键不全则告警跳过、由 Disqus 兜底。

多语言文案

giscus 的界面语言自动跟随当前 Hugo 语言:简体、繁体、香港繁体分别映射到对应的 giscus locale,不支持的语言回退英文。只有自动选择不合适时才显式设 lang

需要翻译的是 OINK 一侧的两句文案:评论区的无障碍标签与加载失败提示。它们按语言配置,与全局仓库配置合并:

hugo.yml
languages:
  en:
    params:
      comments:
        giscus:
          ariaLabel: Comments
          errorMessage: Comments could not be loaded. Please try again later.
  zh:
    params:
      comments:
        giscus:
          ariaLabel: 评论
          errorMessage: 评论加载失败,请稍后重试。

语言层只需要写差异部分,repo / repoId / category / categoryId 留在 params.comments 里就够了。

跟随深浅色

theme: auto 时,giscus iframe 跟随 OINK 的深浅色切换按钮和浏览器的 prefers-color-scheme,读者切换主题时评论区一起变。

需要更贴合站点配色时,用 lightTheme / darkTheme 分别指定两套 giscus 主题,取值是 giscus 内置主题名或站点自己托管的 CSS。本站用的是后者:

hugo.yml
params:
  comments:
    giscus:
      theme: auto
      lightTheme: /css/giscus-oink-light.css?v=0.4.0
      darkTheme: /css/giscus-oink-dark.css?v=0.4.0

theme 写成固定主题名时不再跟随切换。

自定义 giscus 主题需要跨域可读

giscus 的 iframe 从 giscus.app 加载,要读站点上的这个 CSS 文件需要 CORS 允许。本站在 hugo.ymlserver.headers 里给本地预览加了 Access-Control-Allow-Origin: '*';线上由托管商的响应头配置决定。

隐私与 CSP

  • OINK 不会索取或保存读者的 GitHub 密码与访问令牌,登录与发帖全程在 giscus / GitHub 一侧完成。
  • 评论初始化脚本是主题自带的同源资源,只加入启用了评论的页面,未开评论的页面没有这段脚本。
  • loading: lazy 时,读者滚动到评论区附近才加载 iframe。
  • 站点有严格的内容安全策略时,script-srcframe-src 都要放行 giscus,合并进现有策略而不是替换其它指令(总则见内容安全策略):
CSP 片段
script-src 'self' https://giscus.app;
frame-src 'self' https://giscus.app;

外部脚本加载失败或没能创建 iframe 时,OINK 结束加载状态并在实时状态区域显示 errorMessage,不会让页面停在「加载中」。

验证

终端
hugo --minify --panicOnWarning     # 必填键缺失会在这里失败
hugo server --disableFastRender

然后逐项确认:

  1. 打开一个应该有评论的页面,页面底部出现 giscus,显示「使用 GitHub 登录」,界面语言是当前页面的语言。
  2. 切换 OINK 的深浅色,评论区跟着变(theme: auto 时)。
  3. 打开设置了 comments: false 的页面,确认那里既没有 giscus 也没有其它评论组件。
  4. 发一条测试评论,回到 GitHub 看指定分类下是否出现了对应的 Discussion,并且能在 GitHub 上管理。

首次评论或表态创建 Discussion 之前,浏览器控制台提示「找不到 Discussion」是正常现象。

出问题时按这个顺序查:构建日志里的 WARNING(四个必填键)→ params.comments.enabletype → 页面 front matter 的 comments → 仓库是否公开、Discussions 是否开启、giscus App 是否安装 → 浏览器控制台与响应头(CSP 是否拦了 giscus.app)。找不到已有评论线程,先恢复原来的 mapping 和页面路径。

6.4 - 分析与 SEO

接入一个分析服务(或者不接),并把主题已经生成的 canonical、hreflang、社交卡片、站点地图与 robots 配对。

主题默认不加载任何分析、表单或广告脚本,不配置就没有对外请求。接入需要显式配置,并把这条外部数据边界写进站点的隐私说明。SEO 一侧相反:canonical、hreflang、robots meta、Open Graph 与 Twitter 卡片由主题逐页生成,需要你做的是把 baseURL 与每页的 description 写对。

接 Google Analytics

用 Hugo 内置的服务配置,填 GA4 的 measurement ID:

hugo.yml
services:
  googleAnalytics:
    id: G-6JLQEHYFQG

主题只在 production 环境渲染这段脚本(hugo 构建默认 production,hugo server 默认 development)。本地预览与预览部署因此不上报数据,不需要另加开关。

不要同时设置已经弃用的顶层 googleAnalytics 键。不需要分析时删掉整段配置,不要填一个假 ID。

这与网络隔离环境不兼容

配上之后,页面浏览量与事件会发给 Google。严格的同源内容安全策略也需要为它放行,见内容安全策略。这是站点决策,不是主题默认。

接其它分析服务

Plausible、Umami、Matomo 这类服务只要求插入一段脚本。主题提供两个注入点,在站点仓库里建同名文件即可,不用改主题:

layouts/_partials/hooks/head-end.html , 插入位置</head> 之前,在 Google Analytics 模板之前
分析脚本、cookie 同意脚本、主题没提供的 meta 标签
layouts/_partials/hooks/body-end.html , 插入位置页面脚本的最后
只影响交互、不影响首屏的第三方代码
layouts/_partials/hooks/head-end.html
{{ if hugo.IsProduction }}
<script defer data-domain="oink.pgsty.com"
        src="https://plausible.io/js/script.js"></script>
{{ end }}

hugo.IsProduction 这一层不要省:没有它,每个人的本地预览都会向你的统计上报数据。

head-end 在 Google Analytics 之前执行

这是有意的:cookie 同意脚本必须先于分析脚本运行,才能真正拦住它。

「这篇文档解决了你的问题吗」反馈组件是另一件事:默认关闭,不发网络请求,配置见仓库与页面信息

页面描述

<meta name="description"> 按这个顺序取值,取到第一个非空的就停:

  1. 页面 front matter 的 description
  2. Hugo 计算出的页面摘要(.Summary
  3. 站点配置里的 params.description

每页写一句 description 是唯一需要作者做的 SEO 动作。它同时用于三处:搜索引擎的摘要、栏目首页的卡片副标题、站内搜索的结果预览。

content/docs/admin/analytics.zh.md(本页)
---
title: 分析与 SEO
description: 接入一个分析服务(或者不接),并把主题已经生成的 canonical、hreflang、社交卡片、站点地图与 robots 配对。
---

多语言站点要给每种语言各写一句,不要把英文描述抄到中文页上。站点级默认值也是分语言的:

hugo.yml
languages:
  en:
    params:
      description: A Hugo theme for engineering docs
  zh:
    params:
      description: 为工程而设计的 Hugo 文档主题

canonical 与 hreflang

主题为每个页面输出一条 canonical 和一组 hreflang 备用链接,不需要配置:

渲染结果(本页)
<link rel="canonical" href="https://oink.pgsty.com/zh/docs/admin/analytics/">
<link rel="alternate" hreflang="zh-CN" href="https://oink.pgsty.com/zh/docs/admin/analytics/">
<link rel="alternate" hreflang="en-US" href="https://oink.pgsty.com/">

hreflang 的语言代码来自各语言的 locale(本站是 en-US / zh-CN),链接来自 Hugo 的译文关系。上面英文那一条指向站点首页而不是对应的英文页:本页没有英文对等文件,Hugo 找不到译文时回退到目标语言首页。这是预期行为,也可以用来判断译文关系有没有被 Hugo 认出来。

canonical 由 baseURL 拼出。baseURL 配错时 canonical 会把搜索引擎指向不存在的地址,比构建失败更难发现。上线前照发布上线的验收清单查一遍。

多语言的完整配置在多语言

社交卡片

主题调用 Hugo 内置的 Open Graph 与 Twitter 卡片模板,标题、描述、URL、语言、站名都是自动的:

渲染结果(本页)
<meta property="og:title" content="分析与 SEO">
<meta property="og:type" content="article">
<meta property="og:url" content="https://oink.pgsty.com/zh/docs/admin/analytics/">
<meta property="og:locale" content="zh_CN">
<meta property="og:locale:alternate" content="en_US">
<meta name="twitter:card" content="summary">

要让分享出去的链接带图,在 front matter 里给 images

任意页面
---
title: OINK v0.6.0 发布
images: [/images/releasenote.webp]
---

给全站一张兜底图就把同样的键写进 params

hugo.yml
params:
  images: [/images/oink.webp]

有图时 twitter:cardsummary 变成 summary_large_image,并多出 og:imagetwitter:image 两条。本站两处都没有设置,上面的渲染结果里因此看不到图片相关的标签。

站点地图

Hugo 自动生成,多语言站点生成的是一个索引:

public/ 下的结构
sitemap.xml        ← 索引,指向下面两个
en/sitemap.xml
zh/sitemap.xml

站点级默认值和页面级覆盖都是 Hugo 原生的:

hugo.yml
sitemap:
  changefreq: monthly
  filename: sitemap.xml
  priority: 0.5
某个页面
---
title: 发布说明
sitemap:
  priority: 0.8
---

changefreqpriority 是提示不是承诺,搜索引擎可以忽略。值得做的是发布前确认草稿、私有内容与非规范副本没有进入站点地图,并且每种语言的那份都生成了。

robots.txt 与不收录

Hugo 只在站点配置里打开开关时才生成 robots.txt

hugo.yml
enableRobotsTXT: true

主题提供的模板按构建环境给出两种结果,不需要你写内容:

production 构建
User-agent: *
Allow: /

Sitemap: https://oink.pgsty.com/sitemap.xml
非 production 构建
User-agent: *
Disallow: /

页面里的 robots meta 跟着同一个开关走:production 且不是打印输出时是 index, follow,否则是 noindex, nofollow。预览部署不要用 --environment production 构建,非 production 自带不收录的行为。

主题没有按页 noindex 的开关。某一页不该被收录时,可靠的做法是不发布它(draft: true,或用 Hugo 的 _build 选项)。既要发布又不想被收录,就用 head-end.html 钩子自己输出;主题已经输出了一条 robots meta,两条同时存在时如何合并由搜索引擎决定。

收录检查

上线一两周后,按这个顺序确认搜索引擎看到的东西和你以为的一致:

  1. 抓取权限:访问 <baseURL>/robots.txt,确认是 Allow: / 而不是 Disallow: /
  2. 页面清单:访问 <baseURL>/sitemap.xml,点进语言子地图,看页面数量对不对。
  3. 收录数量:在搜索引擎里查 site:你的域名,数量级对得上就行,不必逐页核对。
  4. 规范地址:搜索结果应当落在 canonical 指向的 URL 上,而不是带 ? 参数或旧域名的版本。
  5. 主动提交:在 Google Search Console / Bing Webmaster Tools 里加上站点并提交 sitemap.xml 的地址,比等着被爬快。

搜索元数据补不了内容本身的问题:单薄、重复、过时的页面,写再好的 description 也一样。

验证

终端
hugo --gc --minify --printPathWarnings --panicOnWarning

在产物里查这几项:

终端
# canonical 指向真实生产地址
grep -o '<link rel="canonical"[^>]*>' public/zh/docs/admin/analytics/index.html

# production 构建才有 index, follow
grep -o '<meta name="robots"[^>]*>' public/zh/docs/admin/analytics/index.html

# robots.txt 与站点地图
cat public/robots.txt
head -5 public/sitemap.xml

# 没接分析时,产物里不应该有任何 gtag / analytics 请求
grep -rl 'googletagmanager\|gtag(' public/ | head

浏览器里再确认一次:打开一个代表性页面,看开发者工具的网络面板,没接分析的站点不应有指向第三方域名的请求。

  • 发布上线baseURL、验收清单与预览部署不被收录
  • 仓库与页面信息 — 页面反馈组件、编辑本页与最后修改时间
  • 多语言 — 语言配置决定 hreflang 与译文关系
  • Agent 支持 — 给大模型看的 .md 输出与 llms.txt
  • 配置总览servicessitemapenableRobotsTXT 等键的定义

6.5 - 版本升级

升到新版主题、用迁移工具把 0.4 的 shortcode 改成 v5 语法、从 Docsy 迁过来,以及出问题怎么退回去。

升级 OINK 是换一个固定的模块版本,再确认站点仍能零告警构建。内容多数不用改;需要改的场景(0.4 的 shortcode 换成 v5 的 Markdown 原生形态)有一个可以干跑的迁移工具,不必手改几百个文件。

升级会改变渲染结果。先建一个升级分支再动手,回退的代价就是丢弃一个分支。

先看发布注记

每个版本的变更、破坏性改动与升级要点都写在发布注记里,升级前先读一遍目标版本那篇:

注记说明这次要不要改内容、有没有配置键被移除、默认行为有没有变化。跳过这一步的代价是升级后对着一个变了样的页面猜原因。

升级 Hugo Module

生产站点固定发布标签或不可变 commit,不跟随分支,也不用 @latest

终端
hugo mod get github.com/pgsty/oink@v0.6.0   # 换成发布注记里的标签
hugo mod tidy
hugo mod graph | grep github.com/pgsty/oink

最后一条要能看到解析结果是那个标签本身,而不是伪版本(v0.0.0-2026...-abcdef)或 main。固定的版本落在 go.mod 里,跟着代码一起提交:

go.mod
module github.com/pgsty/oink.pgsty.com

go 1.26.6

require github.com/pgsty/oink v0.6.0
本地模块替换会盖掉这个固定版本

make devmake check 会仅对当前命令设置 HUGO_MODULE_REPLACEMENTS,使用同级的主题 checkout。判定某个发布标签是否可用时使用不带替换的 make build,否则验证的是本地那份代码。

其它安装方式各一句。Git submodule:用 git submodule update --remote themes/oink 拉到新 ref,再提交 submodule 指针。离线归档与克隆:把 themes/oink/ 整个换成新版本的解压结果,确认 theme: 的值仍与目录名一致。三种方式的取舍见从零建站与其它安装方式

升级后必做

终端
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --panicOnWarning --logLevel info

三件事一起做了:清掉可能过期的缓存、用新版本重新构建、把任何告警变成失败。

--logLevel info 是为了看见 Hugo 的弃用提示。Hugo 的弃用分两级:先是 WARN 级提示(仍可使用),下一个版本变成 ERROR(构建失败)。带上 --panicOnWarning 相当于提前一个版本发现它们,把修复的时间留给自己。

构建通过之后,人眼再过一遍:首页、一个文档页、一个博客页、404、两种语言、两种配色、打印视图,以及站点自己定制过的地方。

内容迁移工具

0.4 的一批 shortcode 在 v5 里换成了 Markdown 原生形态。主题仓库带了一个只依赖 Python 标准库的工具做这件事:

终端
git clone https://github.com/pgsty/oink
cd oink

# 1. 只读盘点:一次看多个站点要改什么,可导出 Markdown / JSON 报告
python3 bin/migrations/oink06.py report --sites ~/pgsty/oink.pgsty.com ~/www/ddia --md report.md

# 2. 干跑:打印每个文件的 diff 与计数,不写任何东西
python3 bin/migrations/oink06.py migrate --site ~/pgsty/oink.pgsty.com

# 3. 真改:原子写入
python3 bin/migrations/oink06.py migrate --site ~/pgsty/oink.pgsty.com --write

# 4. 查残留:还有旧语法就退出码 1
python3 bin/migrations/oink06.py check --site ~/pgsty/oink.pgsty.com

用它的时候记住四条:

  • 干跑是默认行为,只有 --write 才落盘。先干跑,读 diff,再写。
  • 重跑一次应该零改动。第二次 --write 还报改动,说明有转换不收敛,停下来看那几个文件。
  • 围栏里的文字不动,文档站里示范旧写法的代码块不会被误伤。
  • 表达不了的构造原样保留,并附 file:line 与原因列出,作为手工处理清单,不是失败。

只想先转某一类时用 --only,键名见下表最后一列:

终端
python3 bin/migrations/oink06.py migrate --site ~/www/ddia --only callout,tabs --write

改完重新构建一次(带 --panicOnWarning),并逐页看渲染结果:工具保证语法正确,不保证语义符合预期。

0.4 → v5 语法映射

{{%/* alert color= title= */%}}{{%/* details */%}}{{%/* pageinfo */%}}、手写 <details><summary> , v5 的写法> [!TYPE] 标题 / > [!DETAILS]-
callout
{{</* tabpane */>}} + {{%/* tab header= */%}}{{</* code-group */>}} + {{</* code-tab */>}} , v5 的写法相邻围栏加 {tab= group= value=};正文型标签页用 {{</* tabs */>}} + {{</* tab */>}}
tabs
{{</* filetree */>}}filetree/folderfiletree/file , v5 的写法filetree 数据围栏
filetree
{{</* gallery */>}}gallery/image , v5 的写法gallery 数据围栏
gallery
{{</* echarts */>}}{{</* infographic */>}} , v5 的写法同名数据围栏($fn: 不变,js 子围栏要挪到 window.OinkEchartsFunctions
datafence
doc-cards / doc-cardnav-cards / nav-cardcard / cardpanedoc-carousel , v5 的写法{{</* cards */>}} + {{</* card */>}},或链接列表加 {.cards}
cards
{{</* imgproc */>}}{{</* image */>}} , v5 的写法![alt](src) 加属性行 {command= options= caption=}
image
{{</* readfile file= */>}} , v5 的写法{{</* include file= */>}}
include
围栏属性 {filename="x"} , v5 的写法{title="x"}
fencetitle
{{</* badge outline= */>}} , v5 的写法去掉 outline 参数
badge
{{</* example */>}} + 围栏、{{</* book-figures kind="tbl" */>}} , v5 的写法{{</* eg */>}}…{{</* /eg */>}}{{</* book-tables */>}}
eg
{{%/* _param x */%}}iframeconditional-textblocks/*netlify、不带 kind 的 xref , v5 的写法工具只报告,需要手工处理
reportonly

每个新写法长什么样、有哪些参数,去组件里对应的那一页。

从 Docsy 迁移

OINK 是 Docsy 的硬分支:内容模型、td- 命名、Sass 变量、大部分 front matter 都还在。迁移的核心动作是删掉站点里复制的公共外壳,让主题的实现接管,而不是重写正文。

  1. 固定目标版本。在 go.mod 里换成 OINK 的发布标签,或者用完整的版本化归档。评估期可以用不提交的 go.work 指向本地 checkout。

  2. 清点覆盖项。把 layouts/assets/static/ 下每个站点级文件归成四类:公共外壳的副本(验证后删)、OINK 已提供的组件(删或机械重命名)、品牌定制(保留,缩到最小 hook)、业务专属数据与交互(留在站点)。按引用关系删,不要清空 layouts/:首页、下载页这些地方可能还在调用你要删的 partial。

  3. 搬配置。titlelanguages.*github_repogithub_branchpage_widthparams.ui.* 全部留在原来的语义位置,OINK 没有另起一套命名空间。搜索与 Logo 这类只要打开对应的键:

    hugo.yml
    params:
      logo: img/product.svg
      offline_search: true

    Docsy 的驼峰式检索键在 OINK 中已改名:offlineSearchofflineSearchIndexofflineSearchMaxResultsofflineSearchOnServeofflineSearchSummaryLength 一律改为下划线形式。这一步要自己盯着改——那份「中断构建并报出新键名」的迁移登记表已经删除,旧键现在只是一个没人读的键,检索会一声不响地保持关闭。

  4. 字体与样式的兼容点。站点的 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 层,见品牌外观

  5. 换 shortcode。Docsy 的 alertpageinfotabpanecard 系列在 v5 里都有对应形态,用上面的迁移工具批量转,--only 一类一类来。

  6. 一次删一组,每组构建一次。在临时副本里演练,记下主题 commit、Hugo 版本、删了哪些文件、产出多少个 HTML;确认等价之后再在生产分支上重做一遍。

第二步里「验证后删」的那一类,通常是这些文件:

  • layouts/baseof.html 与公共的 docs / blog baseof*.html
  • navbar、footer、sidebar、TOC、search、head CSS 的 partial 及其对应 hook;
  • 旧的品牌文档外壳 partial;
  • asciinemaechartsinfographicdoc-carouseldetailstab / tabpane、card 与 param 的 shortcode 副本;
  • 只服务于上述实现的 JavaScript、Lunr 副本、轮播代码与 SCSS;
  • 不再被任何站点资源需要的 PostCSS 与 Autoprefixer 步骤。

删完之后有两类问题会浮出来。

站点自己的脚本报 $ is not defined:主题不带 jQuery,它以前由 Docsy 在每个页面的 <head> 里加载。主题的功能都不需要它,仍然需要的站点自己引入:

layouts/_partials/hooks/head-end.html
<script src="{{ (resources.Get "js/jquery.min.js").RelPermalink }}"></script>

用 Docsy blocks/* 搭的首页在 v5 构建失败,报 template for shortcode "blocks/cover" not found:主题没有这一组 shortcode。改用 data/home/<语言>.yaml 的首页分区,或给页面写 layout: landing,见首页与落地页

从 0.4 升级的要点

0.4 改了几个默认行为。升级后发现页面多了或少了东西,先看这几条:

  • 顺序翻页默认开启。docsbookblog 页尾都有上一页 / 下一页;文档沿侧栏树走,博客沿时间走。刻意不属于任何序列的页面用 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_icpfooter_icp_url 换成一个支持行内 Markdown 的字符串。

    hugo.yml
    params:
      footer_center_info: '[京ICP备00000000号](https://beian.miit.gov.cn/)'
  • 数学公式要站点自己开 passthrough。Hugo 不会合并主题的 markup 配置,用 \(…\)\[…\]$$…$$ 的站点必须在自己的 hugo.yml 里启用 goldmark passthrough 扩展,见公式

这几项的完整配置都在配置总览布局与页面类型

验证

升级不是「构建通过」就算完,按表面分别看:

文档 / Book
侧栏顺序、翻页、标题、页面操作、编号与交叉引用
博客
时间顺序翻页、RSS 归属、顶栏与页脚
首页 / Landing
无 JS 时的内容、紧凑菜单、打印
发布页
推导出的下载 URL、校验和、发布状态
组件
站点用得最多的那几个组件各找一页看渲染结果
无障碍
纯键盘走一遍、焦点顺序、两种配色、强制颜色模式
部署
站内链接与资源都保留了 base path 前缀

本站的完整门禁是:

终端
npm test           # 构建断言、Markdown 与 favicon goldens、翻译对等、渲染后链接
npm run test:browser   # Playwright:无障碍、响应式外壳、键盘导航、内容组件、代码块、场景组件

其它站点跑等价的构建、链接、输出与浏览器检查即可,细节见排错与检查

本地构建成功不等于发布完成

源码可构建、标签已签名并能通过 Go proxy 解析、站点已固定该标签、线上已部署,这是四件事,要分别记录。别用一次绿色的本地构建代替它们。

最后一步在真实环境上做:先部署一份预览,在真实 URL 上验证页面与浏览器的网络请求,评审通过再合并,合并后在生产上做一次冒烟测试。

回滚

回滚的是版本固定,不是工作树:

终端
hugo mod get github.com/pgsty/oink@v0.4.0   # 上一个已知可用的标签
hugo mod tidy
rm -rf public resources/_gen
hugo --gc --minify --panicOnWarning

三条原则:

  • 保留升级前的模块固定、站点 commit 与已知可用的部署产物,回滚时三者一起恢复。
  • 不要只回滚一部分。给新主题塞回几个旧布局副本,会得到一个比任何完整版本都更难诊断的混合状态。
  • 升级分支与验收证据都留着。回滚是为了先恢复线上,不是丢掉已经做完的工作。

线上产物本身的回滚(重新发布上一个部署)见发布上线

6.6 - 排错与检查

构建、语言、搜索、平台四类故障的症状 → 原因 → 修法,以及站点可以自己跑的那几项检查。

出问题时先做一次干净的生产构建,从第一条错误开始看,后面的多半是级联结果:

终端
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --panicOnWarning --logLevel info

日志里出现 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤,说明配置里混进了上游 Docsy 的流程。OINK 消费端的构建只有一条 Hugo 命令。

下面四张表按「症状 → 原因 → 修法」组织,找到症状那一行即可,不必从头读。

构建

症状原因修法
构建报要求更高的 Hugo 版本装的是标准版而不是 Extended,或版本低于 0.160.1hugo version 输出里必须有 extended。多个 Hugo 共存时先查 PATH 与版本固定配置,而不是再装一份
module "github.com/pgsty/oink" not found主题没解析出来Hugo Module:看 hugo mod graphgo.modgo.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 ...属性行里的键拼错或不被允许属性行只接受该组件的允许键、classdata-*aria-*styleon* 一律构建失败。允许的键就写在报错括号里
shortcode "field": unsupported parameter "colour" at ...shortcode 参数名不对组件参数——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 配置:

hugo.yml
markup:
  goldmark:
    parser:
      wrapStandAloneImageWithinParagraph: false
      attribute:
        block: true
    renderer:
      unsafe: true

两个最常见的 shortcode 报错长这样,注意结尾的 文件:行:列

构建输出
ERROR error building site: assemble: failed to create page from pageMetaSource /a:
  "…/content/docs/x.md:4:1": failed to extract shortcode:
  shortcode "tabs" must be closed or self-closed

ERROR error building site: assemble: failed to create page from pageMetaSource /a:
  "…/content/docs/x.md:4:5": failed to extract shortcode:
  template for shortcode "tabs" not found

语言

症状原因修法
译文页面不出现四种可能,按顺序查hugo.yml 里有 languages.zh 且设了 weight;② 文件名是 page.zh.mdzh 必须小写;③ 译文 front matter 没有 draft: truedate 不在未来;④ 影响路由的元数据与源文件一致
语言切换跳到了首页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,每种语言一份。没有就是没开
索引文件请求 404baseURL 不对子路径部署下 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 valuesAlgolia 三个键没配全三个键必须显式给全,主题不会替你用别的项目的 DocSearch 凭据。不用 Algolia 就把这段配置删掉
命令面板搜不到内容它与全文检索是两件事索引不可用时命令面板仍然能打开,只是提示索引不可用,页面操作与命令照常,见命令面板

平台

症状原因修法
GitHub Pages 上页面 404 或样式全丢项目站点的 URL 带仓库路径,baseURL 没带用工作流里的 --baseURL "${{ steps.pages.outputs.base_url }}/",别手写。完整工作流见发布上线
GitHub Pages 上「最后修改时间」「贡献者」全空checkout 是浅克隆actions/checkoutfetch-depth: 0enableGitInfo 要读完整历史
Cloudflare Pages 构建报 Hugo 版本太低构建镜像的默认 Hugo 低于主题要求在 Production 和 Preview 两个环境都设 HUGO_VERSION,并设 SKIP_DEPENDENCY_INSTALL=1
托管商构建时拉不到主题构建环境没有 GoHugo Module 需要 Go。平台不提供就改用 submodule 或把 themes/oink/ 提交进仓库
CI 上构建结果和本地不一样go.work 参与了 CI 构建CI 里设 GOWORK: offHUGO_MODULE_WORKSPACE: off,让它只认 go.mod 里固定的版本
预览部署被搜索引擎收录了预览也用了 production 环境构建预览构建不要带 --environment production,非 production 自带 noindexDisallow: /,见分析与 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 脚本,其它站点跑等价的检查即可。

零告警构建 , 命令hugo --printPathWarnings --panicOnWarning
重复输出路径、参数非法、外部集成配置不全
输出信任检查 , 命令python3 bin/check-output-security.py --public public --base-url https://oink.pgsty.com/
四种输出里的每个 href / src 都是站内相对或 http(s) / mailto / tel;没有 javascript: URL、没有行内 on* 事件处理器;跨站的 <iframe> <script> <img> 等要显式加 --third-party 才放行
翻译对等 , 命令node scripts/check-doc-translations.mjs --public public
每个英文页有没有中文对等页,以及渲染后的标题 ID 是否逐一对齐;锚点链接错位在这里暴露
完整门禁 , 命令npm test
下面六项串起来跑

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)、第一条完整错误、能复现的最小页面或最小站点。

7 - 设计与开发

在唯一的双语专栏中管理 OINK 维护者契约、已接受决策、定期研究与候选提案。
OINK 0.6.0 契约

本专栏公开随 OINK 0.6.0 正式发布的维护者契约,兼容性下限为 Hugo Extended 0.160.1。唯一的中英文契约源文件位于本站仓库的 content/docs/design/

本专栏是 OINK 可长期维护的设计记录。站内其它专栏按任务讲解如何搭建站点; 这里集中说明现行不变量、这些选择背后的理由、用于比较方案的证据,以及仍处于 候选阶段的工作。

如何阅读本专栏

层次含义
契约兼容实现必须保留的规范性行为
决策用于解释现行行为的已接受理由与边界
研究带日期且不具规范性的证据,必要时应重新验证
提案PRD 与 RFC 草案;公开在这里不代表已经实现

契约目录

契约权威范围
架构契约构建、配置、诊断、特色图片、输出、安全、无障碍与性能
组件契约组件 API、Book 与发布原语、校验和输出降级
外壳与导航契约导航、搜索、博客展示、操作、分类法与页尾组合
落地页契约落地页数据、22 种区块注册表、运行时、无障碍与输出
迁移边界从 0.4 到当前版本所支持的内容与配置迁移

设计记录

集合内容
设计决策已接受的诊断、配置与创作模型设计理由
设计研究Goldmark 探针与真实 OINK 消费站点证据
候选提案知识图谱、媒体收敛与机器可读索引等活跃 PRD

以后所有 OINK PRD 或 RFC 都必须以中英文页面对的形式放入 content/docs/design/proposals/,不得再在仓库中创建 plan/plans/proposal/ 目录。提案被接受后,应同步更新实现、对应检查器与相关契约,把稳定 理由沉淀到“设计决策”,并通过 Git 历史与变更日志退出草案。

权威来源与维护

本目录同时管理英文与中文维护者设计文档。主题仓库管理可执行事实:hugo.yaml 管理公开默认值;对应的解析器与检查器定义可选结构;layouts/assets/ 管理渲染行为;检查脚本与 tests/goldens/ 管理验收;VENDOR.json 管理内置 依赖的版本、许可证、文件与校验和。

公共行为发生变化时,必须在同一次交付中更新实现、对应检查器以及本目录下相关 契约的中英文版本。测试应验证行为和输出,不应只固定某段文字。

7.1 - 架构契约

仓库装配、配置、诊断、输出、性能、安全、CSS、无障碍与发布状态的边界。
OINK 0.6.0 契约

这是随 OINK 0.6.0 正式发布的架构契约。本页是权威中文源文件,与英文版本 一同维护在 content/docs/design/

仓库与装配

仓库根目录是一个完整的 Hugo 模块与主题,不是站点,也不是 npm workspace。 Hugo Extended 负责编译 SCSS 与模板。浏览器运行时与第三方资源都已提交到仓库, 因此普通构建不会访问网络。公开的双语文档、示例与浏览器测试位于同级的 oink.pgsty.com 仓库;主题仓库只在 tests/site/ 中保留范围明确的内部回归 夹具,不再维护独立的公开示例面。

生成的 public/resources/ 目录绝不是源文件。随主题内置的运行时、字体 家族与 Font Awesome 字形定义属于受支持的发行内容,并非待清理的死代码; VENDOR.jsonbin/check-vendor.py 固定其完整性。OINK 发布完整的受支持 Font Awesome 发行包,因为用户编写的内容可能使用主题模板本身没有引用的图标。

Hugo 类型 docsbookblogswagger 选择阅读外壳; 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.giscusplantumldrawio 等包含多项 设置的集成保留在顶层。布尔功能直接使用布尔值,除非它还包含多项设置。页面级 覆盖会去掉 ui. 前缀:params.ui.image_zoom 对应 image_zoom,front matter 中绝不嵌套 ui map。hugo.yaml 声明公开默认值;对应的解析器与检查器定义 任何可选配置的结构或范围。

无效输入遵循同一条规则:警告中写明输入值、允许的结构与安全回退,然后使用该 回退,或省略不安全的功能。普通 hugo server 因而仍可使用,而所有发布门禁都 使用 --panicOnWarning。主题绝不调用 errorfcheck-params.py 会强制守住 这条边界。不要为无法到达的状态增加臆测式校验。

OINK 没有通用的键名重命名注册表。仍需给出迁移诊断的过渡,应在所属解析器中 添加针对性警告,并配严格的反向测试;已经移除的键绝不能作为兼容路径继续读取。

可能联网的功能必须显式启用,并以关闭方式降级。PlantUML 需要 plantuml.svg_image_url,Draw.io 需要 drawio.drawio_server,Algolia 需要 appIdapiKeyindexName;配置不完整时发出警告,而且不产生网络请求。 Draw.io 只在渲染内容含 PNG 或 SVG 候选图片时加载,并且每个不同的图片 URL 只检查一次。

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完整的语义内容;只为实际用到的能力加载本地运行时
Print展开的内容;不含外壳导航、搜索或图片缩放运行时;共享操作层仍支持明确的打印控制
Markdown / LLMS保持源 Markdown 形态,不含 td- 组件标记
RSS安全的静态摘要,或明确省略

站点自行选择是否启用自定义输出;OINK 不会强制生成昂贵的整书聚合。HTML 加载 共享操作层、核心层,以及按页面实际能力和语言生成的功能 bundle。Print 保留 操作层,并且只加载渲染打印功能所需的运行时。大型第三方 UMD 文件保持独立; 未使用的功能运行时不会出现。

性能规则如下:

  • 若站点级资源或 partialCached 结果可以承担工作,不要为每一页遍历 .Site.Pages
  • .Content 只渲染一次,完成后再读取 Page Store 标志;
  • 直接输出正确标记,不要扫描 DOM 后再修复;
  • 浏览器工作按资源 URL 分组,而不是按 DOM 实例重复;
  • 成本显著的普通输出应保持选择启用;
  • 校验确实可达的作者输入,不校验假想的内部状态。

bin/measure-baseline.py 测量构建时间、输出体积、bundle 数量与 shortcode 密度。bin/sites/build-all.py 在隔离快照中构建维护范围内的消费站点。

信任边界、CSS 与无障碍

作者可以启用 Goldmark unsafe,但配置与组件参数不能视作原始 HTML。共享属性 策略使用允许清单、校验 class token、放行 data-*aria-*,并在丢弃 stylesrcdocon*、保留属性与未知属性时发出警告。需要本地 URL 或明确 绝对 URL 时,URL 帮助模板会拒绝危险协议与协议相对 URL。公开 API 承诺支持的 远程 URL 仍然可用,但构建时绝不抓取它们。

主题输出使用 td- class、data-td-* 属性与 --td-* 自定义属性;.steps.cards.full-width 等作者标记保持无前缀。CSS 支持 RTL、打印、强制颜色、 减少动画、超长 token 与窄视口。主题拥有的装饰图标带 aria-hidden;只有包含 任务列表或原始 Font Awesome 元素的页面才加载作者内容无障碍修复。

字体角色为 uibodyheadingcodedisplaymetadataprint, 通过 --td-*-font-family 暴露。params.ui.typography 可取 technicalsystem;两者编译到同一份样式表,不加载运行时。旧 Bootstrap/Docsy Sass 变量继续为这些角色提供初值。

发布状态

源码完成、本地验证、提交、打标签、推送、消费站点固定版本、部署与生产一致是彼此 独立的状态。一次本地 Hugo 构建只能证明本地验证通过。

7.2 - 组件契约

OINK 创作原语、校验、Book、发布行为与输出降级的维护者契约。
OINK 0.6.0 契约

这是随 OINK 0.6.0 正式发布的组件契约。本页是权威中文源文件,与英文版本 一同维护在 content/docs/design/

教程与完整示例位于面向读者的组件专栏。本页定义这些 指南所依赖的 API 与行为。

创作模型

一个区块加属性便能表达组件时,使用普通 Markdown;需要复合正文或 Markdown 无法携带的事实时,使用 shortcode。OINK 没有并行的组件注册表。原生形态要求:

markup:
  goldmark:
    renderer: { unsafe: true }
    parser:
      wrapStandAloneImageWithinParagraph: false
      attribute: { block: true }

只有 {{%/* steps */%}} 使用百分号分隔符,因为它的正文属于页面大纲;其它 shortcode 一律使用尖括号分隔符。复合正文通过 content/render-block.html 处理,并使用唯一的 ID 作用域。Shortcode 与组件参数中的 caption、label、title 和 name 是纯文本,Markdown 应放在正文里。落地页叙述字段遵循自己的契约。图标 由一对 Font Awesome class 表示。组件暴露安全的 class 与属性,不接受任意颜色 或内联样式。

公共 API

OINK 有 29 个 shortcode:

  • 核心:tabstabstepscardscardfieldsfieldincludekbdbadgeparamcommentcontributorsasciinema
  • Book:figtbleqegxrefbook-tocbook-figuresbook-tablesbook-equationsbook-examples
  • 发布:release-cardrelease-assetsdownload
  • OpenAPI:swaggerredoc
组件原生形态Shortcode 形态HTML 运行时
提示块> [!TYPE]、折叠、{icon=}
标签页相邻围栏或表格加 {tab= group= value=}tabs / tab只在使用页加载 tabs
步骤有序列表加 {.steps}steps
卡片链接列表加 {.cards}cards / card
参数表表格加 {.fields}fields / field
FileTreefiletree 数据围栏只有注释存在时加载分隔条运行时
画廊gallery 数据围栏符合条件时共享图片缩放
图片Markdown 图片加块属性符合条件时加载图片缩放
表格属性、caption、编号或标签页复合 Book 表格使用 tbl只有标签页表格加载 tabs
Book 目标图片、表格、passthrough、围栏加 {num=}figtbleqeg
发布资产checksums 数据围栏release-assetsHTML 中加载复制功能
图表与数据mermaidplantumlmarkmapmathchemechartsinfographic 围栏只加载选中的本地运行时

校验

无效的作者输入遵循架构契约:发出警告,使用文档 规定的安全回退或省略组件,再由 --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 图片操作。

组件行为

提示块与标签页

提示块类型包括 notetipimportantwarningcautionsuccessdangerquestionexamplequotedetails- 表示初始折叠,+ 表示初始展开。未知类型会以中性提示块保持可见,不依赖 JavaScript。

只有连续且区块类型相同的相邻标签页才会分组。group 启用 #<group>-<value> hash 与 td-tabs:v1:<group> 存储键;未分组标签页两者都不用。 HTML 在 JavaScript 运行前暴露所有面板,打印输出展开面板,Markdown 保留作者 源文,RSS 接收渲染后的文本摘要。完整形态支持任意 Markdown;tab.label 必填, 父级存在 groupvalue 才严格必填,孤立的 tab 会警告且不渲染。

步骤、卡片、参数表与表格

原生步骤接受普通区块内容。只有某一步必须包含百分号容器时才使用 shortcode。 原生卡片是链接列表;完整形态增加正文、徽章、图标与图片。原生参数表把第一列 映射为名称、最后一列映射为描述,中间列由 meta= 或表头映射;完整形态允许 区块描述。cardfield 只能放在各自的父容器中。

参数锚点为 field-<name>,名称转小写,连续标点折叠为连字符,因此 params.ui.typography 变成 field-params-ui-typography。重复锚点追加位置后缀。

表格渲染钩子负责响应式包装与 caption。.matrix 把第一列变为行表头; .full-width 加宽普通表格或矩阵表格。.fields 不能与 matrix、full-width、 编号或标签页组合;编号与标签页也互斥。

Markdown 图片钩子是普通图片 API。行内图片保持行内;块图片带 captionnum 时变为 figure。允许的图片属性包括 idnumcaptionwidthheightlinkcommandoptions,以及共享安全属性。commandoptions 必须同时 出现,并对可处理的本地资源调用 Hugo FitResizeFillCrop。普通 链接图片使用 Markdown 语法,因此 link 属性要求同时有 caption 或编号。链接 图片与装饰图片不加载缩放。

画廊每行接受一张 Markdown 图片,可带描述、链接与 class。FileTree 接受缩进、 - name、可选 /、注释,以及经过校验的 icon、tone、open、type 属性。Markdown 保留作者源文;打印输出渲染展开的静态图片与文件树。

所有代码高亮都使用 Chroma。通用围栏属性包括 titlecopywrapcollapselabelid、行选项、标签页,以及 Book 的 num/caption。复制 操作返回作者源文。ECharts 输入是声明式 JSON/YAML;回调使用 window.OinkEchartsFunctions 中的 $fn:<name>,绝不执行嵌入脚本。

Book

book 类型扩展 docs 外壳,并遵循内容树或 data/docs_nav.jsonbook_numberbook_partbook_kindbook_status 是展示元数据,不改变 Hugo 发布状态。

带编号的类型为 figtbleqeg,默认 ID 是 <kind>-<num>eg 需要 caption;不带 numeq 是无编号展示公式。xref 要么准确指定一种类型 并可附带 page/anchor,要么指定一个 anchor 和显式文字。带编号的示例是一个 完整的边框正文与 caption。

脚注属于页面文档。原生编号表格与围栏会让脚注留在页面里。Shortcode 正文是独立 的 Goldmark 文档,因此 tblegfigcardtabfieldinclude 中的脚注引用会警告并保持字面形式;该检查忽略代码形态的文本。

book-toc 按 1–3 层导航顺序生成目录;四个 book-* 索引各自收集一种目标。 整书打印会改写跨页链接,并给普通标题与脚注增加命名空间,同时保留显式目标 ID。 消费站点自行选择是否启用这种潜在成本较高的输出。

发布与下载

发布 front matter 使用一个 https://github.com/<owner>/<repo>/releases/tag/<tag> 形态的 release_url;owner、 项目与 tag 来自 URL,日期来自页面。构建不会抓取远程发布状态。已经移除的 release map、release_productsrelease_group_by_product 会警告并给出 替代项,它们不是兼容路径。分区索引列出所有页面;能解析时使用 project tag, 否则使用页面标题。

校验和可以接受规范行,也可以接受一个源资源,两者不能同时提供;文件名不能是 路径。HTML 增加本地复制功能,静态输出暴露完整 hash。

下载使用 data/download/<key>.yaml。channel 可取 rollingpinned;只有 pinned URL 与命令会插值 ${version}${tag}。发布前,rolling channel 保持 可用,pinned channel 显示 pending。Markdown 渲染完整 channel 列表;RSS 省略 该组件。

验证

共享输出规则见架构契约,例外随各组件定义。 Markdown 与 RSS 不设置浏览器运行时标志;Print 只保留渲染打印功能需要的标志。 源码检查覆盖参数、渲染钩子策略、运行时隔离与迁移;输出检查比较 HTML、Print、 Markdown、RSS 与 LLMS golden;浏览器测试覆盖交互界面。迁移行为见 迁移边界

7.3 - 外壳与导航契约

导航权威、沉浸式博客、搜索、操作、分类法、索引与页尾组合契约。
OINK 0.6.0 契约

这是随 OINK 0.6.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_linkbuild.render: link、分隔行、 隐藏节点与占位节点保留各自已定义的语义。sidebar_icon_policy 可取默认的 allgroupsnone;图标是一对 Font Awesome class。无效策略遵循共享的警告与 回退契约。

沉浸式博客展示

OINK 没有 article 类型或第二套外壳。沉浸式阅读由普通博客外壳上的四个独立键 组成,可设在页面或分区 cascade 上;分区索引会重复它自己也需要的值:

featured_image: hero
toc_style: flow
toc_taxonomies: false
sidebar_enabled: false

博客外壳默认不渲染面包屑导航——文章应作为独立作品阅读——所以这份配置不需要 相应的键。breadcrumb 仍是普通键,页面或 cascade 可以在任何外壳上明确打开 或关闭它。

hero 在单页与分区索引上把共享特色图片用作装饰性的全出血背景。没有图片时 渲染普通开场;bannerwash 仍只用于单页。顶部导航栏以对比遮罩叠在 hero 上,并随页面一起滚动。

toc_style 可取 fixedflow;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_markdowncopy_linkopen_chatgptopen_claudeview_markdownview_historyedit_pagecreate_child_pagecreate_issuecreate_project_issueprint_sectionprintswitch_themeswitch_languageswitch_versionopen_github。分享栏之外的 copy_link 只出现在命令面板中。站点通过 languages.<lang>.params.ui.command_palette.commands 配置的命令可以打开安全 URL,或调用内置 ID,绝不能注入 JavaScript。

命令面板有空状态、文本搜索状态与 > 命令状态;快捷链接来自导航。它没有历史、 语义搜索、个性化或远程回退。搜索查询留在浏览器内,默认不发送遥测。

OinkSurfaceCoordinator 协调命令面板、抽屉、根栏目、语言与版本菜单。各界面自行 管理焦点恢复与 Escape。键盘导航会忽略可编辑控件与模态框:/\fc 打开搜索或命令;j/k 移动标题;q/e 翻页;h 改变展示方式;l/ytr 分别打开语言、主题与根栏目选项。侧栏 WASD/方向键导航使用真实焦点, 不会改写 Tab 顺序。

页面大纲从同一套标题模型与滚动容器计算后的 scroll-padding-top 推导光标和可见 标题范围;SVG 线条与圆点共享同一组动画值,不会漂移。禁止增加臆测性的 DOM 修复遍历。

分享

params.ui.share 默认为空,可接受 16 个目标的任意有序子集:xblueskymastodonfacebooklinkedinreddithackernewstelegramwhatsapplinepinterestweibochatgptclaudeemailcopy。 页面列表会替换继承列表;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_nameupstream_copyrightupstream_licenseupstream_notice, 以及可选的 upstream_refupstream_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 图标。图标词汇表由 taxonomy-icon.html 独家拥有——每个 分类法配一对图标:整体分类法一枚、单个术语一枚(folder-open/foldertags/tagcubes/cubeusers/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_weightseries-pages.html 先按 weight 排有权重成员,再按日期升序排 无权重成员,并用 Path 打破平局;系列条与 term 页面共享该顺序。第一个命名系列 得到一条 HTML/Print 系列条:默认收起的一行显示系列名和第 M/N 篇,展开后按阅读 顺序一行一篇,打印时默认展开。单篇系列与非 HTML 输出省略它。编号、交叉引用与 聚合输出仍属于 Book。

默认文章分类法徽章会排除保留的 authorsseries,因为专属界面已经展示 它们。显式设置 params.taxonomy.page_header 可以恢复任意一项。

博客索引与页面组合

博客分区索引使用 params.ui.blog_index:默认的 listcards 都是按最新优先 排列的一段扁平结果,共享 blog_index_size 分页;元数据行已经显示日期,所以不再 需要年份标题。table 把整个分区显示为日期、标题、标签行,不分页。卡片使用共享 首图、本地化日期/作者/分区元数据、标签与三行摘要。Term 与 taxonomy 页面保持 行列表。

params.ui.blog_index_toggle 为当前分页切片渲染三种形态,并允许读者循环切换。 配置值控制首次绘制,本地存储可以覆盖它,隐藏形态不加载图片。Front matter 或 cascade 可为每个分区覆盖站点模式。没有切换器的 table 仍是完整且不分页的归档。

params.logo 始终是品牌标志;params.wordmark 或站点标题是紧凑宽度下隐藏的 文字部分。Docs、Book、Blog 与 Swagger 共享一个外壳模型。页尾顺序为分享、反馈、 注记、翻页、评论。Docs/Book 翻页遵循侧栏前序遍历;Blog 按 weight 后接日期倒序; pager: false 退出。静态输出省略翻页 UI。

每一种实际渲染的页脚形态,都会在最底层栏右侧保留纯图标工具组,顺序为版本、 语言、主题、快捷键帮助。各菜单向上展开;版本触发器不直接显示当前分支或版本名。 胖页脚的折叠箭头排在工具组之后。低于 lg 时,底层栏放弃版权/居中/工具组的 三列布局,改为三行全宽居中堆叠,工具组在最后一行。这些全局控件不再出现在 侧栏底部;footer_style: none 会移除整条底栏。

OINK 没有归档外壳、任意深度飞出菜单、第二个导航权威、查询上传,也没有针对已 移除配置的浏览器兼容 shim。反馈只通过既有 gtag 发出 docs_feedback,在本地 保存选择,而且不替代 Giscus。

验证

bin/check-navigation-contract.pybin/check-shell.py、JavaScript 测试、输出 golden 与消费站点浏览器套件覆盖导航、语言与子路径链接、博客变体、页尾顺序、 键盘行为、无障碍与响应式布局。

7.4 - 落地页契约

落地页数据、内置区块注册表、语言解析、运行时、无障碍与输出的维护者契约。
OINK 0.6.0 契约

这是随 OINK 0.6.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_starsparams.ui.alt_site 是可选的本地 界面事实。

区块注册表

注册表恰好有 22 种内置区块:

  • herometricscapabilitiesprinciplescardslogo-wallgallerytestimonialscontributorsfaqmarkdowncta
  • pricingpricing-comparecommand-boxstepstimelinecode-platepreviewcase-studydownloadbar-chart

条目可以是类型字符串,也可以是包含 typekeyidenabled、内联 data 或有意指定的本地 partial 的 map。作者提供唯一 ID,OINK 把它规范为 锚点安全值。未知类型遵循共享的警告与安全回退策略,绝不静默消失;发布时 --panicOnWarning 会拒绝它。内置区块由 landing/ partial 负责;已经移除的 home/ partial 名称不是 API。

preview 通过站点渲染钩子,把 Markdown source 放在 RenderString 输出旁, 因此其内容会登记与 docs 内容相同的运行时。源码面板使用 Chroma,并带默认值为 page.mdfile 名称。Markdown 输出使用四个反引号包围的 markdown 围栏; RSS 省略它。面板标签来自主题 i18n。

hero.align 可取 startcenter。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-hiddeninert,本地化复选框无需 JavaScript 也能持久保存暂停状态。减少动画会停用动画,强制颜色保留控件,主题 图片响应共享主题事件。顶部导航栏 mega menu 接受 1–4 列。紧凑菜单使用真实链接 与按钮,不捕获焦点,也不复制桌面导航树。

输出与兼容性

输出契约
HTML完整静态区块加渐进增强
Print静态网格与内容,移除控件
Markdown不带主题 class 的标题、正文、列表、表格与代码
RSS省略落地页区块

非 HTML 输出不设置 Landing 标志或运行时。根相对链接与资源遵循部署子路径;普通 构建不下载图片。

已经移除的 0.4 组件形态属于迁移工具,不是并行的落地页实现。OINK 不增加价格 周期切换、远程事实 API、热点编辑器、可视化构建器或第二套注册表。既有首页数据 与显式自定义区块 partial 继续有效。

7.5 - OINK 迁移边界

从 OINK 0.4 到 OINK 0.6.0 所支持的源码、配置与验证迁移边界。
OINK 0.6.0 契约

这是随 OINK 0.6.0 正式发布的迁移契约。本页是权威中文源文件,与英文版本 一同维护在 content/docs/design/

这是源码与配置指南,不是版本发布流水账。本地源码、提交、标签、推送、消费站点 固定版本、部署与生产一致仍是彼此独立的状态。面向读者的升级流程见 版本升级

工具范围

bin/migrations/oink06.py 只扫描和自动改写站点内容目录下的 Markdown 文件, 包括受支持的 YAML front matter。它不改写 Hugo 配置、数据文件、布局、资源、 模块或生成输出。TOML/JSON front matter 与有歧义的 Markdown 会连同位置一起报告, 留给人工检查。

默认执行 dry-run;完成后的迁移具有幂等性:

python3 bin/migrations/oink06.py report --sites <dir>... --md report.md --json report.json
python3 bin/migrations/oink06.py migrate --site <dir>
python3 bin/migrations/oink06.py migrate --site <dir> --write
python3 bin/migrations/oink06.py check --site <dir>

代码围栏不会改写。book_figures.py 保留范围明确的 TPME、DDIA v1/v2 与 pg-internal profile;它不是通用解析器。

从 0.4 内容迁移到当前形态

已移除形态当前形态工具键
alertdetailspageinfo、原始 disclosure> [!TYPE] 提示块callout
tabpane、旧 tabcode-groupcode-tab相邻 {tab=} 区块,或 tabs / tabtabs
FileTree shortcode 或 {.filetree} 列表filetree 围栏filetree
Gallery shortcode 或 {.gallery} 列表gallery 围栏gallery
ECharts / infographic shortcode同名数据围栏datafence
Docsy 卡片家族.cards 列表或 cards / cardcards
imgprocimageMarkdown 图片加属性image
readfileincludeinclude
围栏 filename=title=fencetitle
badge outline=移除 outlinebadge
叶子 examplebook-figures kind=eg、显式 book-* 索引eg
百分号分隔的 fields尖括号分隔的 fields / fieldfieldsdelim
Docsy _param 占位符与 card header= 高亮Font Awesome / badge / param 或提示块param_placeholders
不支持的旧 shortcode报告源码位置,人工检查reportonly

配置与 front matter

以下配置改动需要手工处理;工具可以报告匹配的 front matter 键,但绝不编辑站点 配置。

旧配置当前配置
offlineSearch*offline_search*
disable_click2copy_chromaui.code_copy,取反
content_width`reading_width: slim
github_urlgithub_repo
ui.no_left_sidebarui.sidebar_enabled,取反
breadcrumb 别名ui.breadcrumb
ui.scrollSpyui.scroll_spy,取反
ui.showLightDarkModeMenuui.dark_mode.show_menu
ui.readingtimeui.reading_time
ui.ul_showui.sidebar_expand_levels
ui.docs_rootui.docs_sidebar_root
ui.pagerui.pager_types
annotation/zoom/keyboard/reading 的 { enable: bool } map裸布尔值
ui.typography.presetui.typography
print.disable_tocprint.toc,取反

Prism、rss_sectionsalgolia_docsearch 已移除。Chroma 是唯一高亮器;Algolia 配置为 search.algolia。页面级覆盖会去掉 ui. 前缀。旧 hide_feedbackhide_readingtimeexclude_searchcontent_width、camelCase 手工链接与嵌套 front matter ui map 会连同替代项一起报告。

从 0.5 到 0.6

  • upstream_linkupstream_nameupstream_copyrightupstream_licenseupstream_notice 替代 upstream_attribution;把 downstream_modified 改名为 upstream_modified
  • 用一个 GitHub release_url 替代 release map;从发布索引移除 release_productsrelease_group_by_product
  • 博客与默认日期现在采用 ISO 2006-01-02;面向读者的日期继续显式保留 time_format_blogtime_format_default

已移除名称会警告,并采用文档规定的安全回退或不渲染;普通预览可以继续,严格 门禁通过 --panicOnWarning 拒绝它们。blog_index_togglefeatured_image: herotoc_styletoc_taxonomies 是增量选择启用项,不会 引入内容类型;沉浸式阅读仍使用普通博客外壳。

前置条件与验证

按照组件契约启用 Goldmark unsafe 渲染、块属性与 独立块图片。要使用 \(...\)\[...\]$$...$$,需要显式启用 passthrough; Hugo 不会合并主题的 markup 配置。

针对改动的契约运行范围最小的源码与输出检查,覆盖两个受支持的 Hugo 版本;运行时 变化时执行 JavaScript 测试,并严格构建根路径与子路径。对于维护范围内的站点, 在桌面与窄视口检查有代表性的 EN/ZH Docs 与 Blog 路由,再分别记录固定版本、部署 与线上一致状态。

7.6 - 设计决策

解释 OINK 现行公开契约与实现为何采用当前形态的已接受选择。
已接受的理由

决策记录解释 OINK 为什么在多个兼容方案中选择了当前设计。上方五份契约仍是 现行行为的规范描述;实现与归属检查器仍是可执行事实。

OINK 过去把评审、PRD 与执行记录放在本地 plan/ 目录中。这样既不便发现有价值的 推理,也容易让已经放弃的设计看起来仍有权威。已经接受的理由现在统一进入这座双语、 版本化的文档站,与它所支撑的契约放在一起。

决策地图

决策解决的问题
警告与安全回退为什么普通预览能容忍错误输入,而发布仍保持严格
配置模型配置放在哪里、页面如何覆盖,以及 OINK 为什么不另造配置命名空间
Markdown 优先创作为什么优先使用原生 Markdown,以及 Docs、Blog、Book、Landing 如何延长共享系统

记录格式

一份已接受决策应记录背景、选择、后果,以及证明该选择仍然成立的证据。它不重复参数 参考或教程。每份决策都要链接到归属契约与验证面,中英文页面必须同步修改。

决策发生变化时,应在同一次交付中更新实现、检查器、受影响契约与决策记录。旧答案留在 Git 历史和版本变更记录中,不在导航树里并列保留两套“现行”答案。

7.6.1 - 警告与安全回退

作者输入无效时,预览阶段发出警告并安全降级;–panicOnWarning 在发布阶段恢复硬门禁。
决策

OINK 不调用 Hugo 的 errorf。作者或站点输入无效时,主题发出警告,并使用文档中 明确的安全回退,或者省略无效片段。版本发布与部署构建使用 --panicOnWarning, 因此同一条警告在发布门禁中仍会导致硬失败。

背景

Hugo 把整座站点作为一次事务构建。编辑一页时触发的 errorf 会让该次重建中的所有 URL 都返回错误,包括无关页面和首页。服务器进程仍然存在,修正输入后也会自动恢复,但多人共享 的预览在此期间完全不可用。

警告的开发成本不同。出错的值可以回退,站点其余部分仍可检查,作者也能看到准确消息。 发布构建则不会放过它,因为 OINK 的 CI 与集成门禁都会加上 --panicOnWarning

决策

校验遵循四条规则:

  1. 点明无效键和值、允许的形状以及实际采用的回退值。
  2. 值来自页面 front matter 时带上页面位置;站点级错误不要在每一页重复刷屏。
  3. 不允许无效值继续参与后续运算。先校验,再用规范化后的值渲染。
  4. 没有诚实回退时,警告并且不渲染。不能为了继续构建而编造内容、发起网络请求或输出 不安全 URL。

枚举、布尔、CSS 长度与数字的共享校验形状位于 layouts/_partials/validate.html。领域 resolver 可以增加更窄的规则,但必须保留同一套 警告与回退契约。

安全边界

继续构建不等于继续输出危险内容。被拒绝的 CSS 长度要在进入 style 属性之前回退;远程服务 配置不完整时,要在浏览器可能发起请求之前省略组件;不安全的操作 URL 直接丢弃。真正的保护是 坏输出没有出现,而不是 Hugo 被终止。

这也把编辑与发布清晰分开:

阶段无效输入的处理
hugo server 或普通本地构建警告、回退或省略,其它页面继续可用
CI、版本验收、部署同一警告在 --panicOnWarning 下让构建以非零状态退出

后果

  • 每个回退值都是公开契约的一部分,必须与主题声明的默认值一致。
  • 从“失败”改成“回退”时,测试也必须改变。负向测试要同时证明普通构建存活、警告文案、 渲染后的回退,以及严格构建失败。
  • 检查器必须直接验证被拒绝的输出。例如 URL 安全测试应断言危险 URL 没有进入产物,不能把 任意构建失败当作充分证据。
  • 参数源码检查器守住主题 layout 中不存在 errorf 调用这一不变量。

验证

本决策的归属参考包括 架构契约bin/check-params.py,以及主题夹具与本站的严格构建。

7.6.2 - 配置模型

OINK 延长 Hugo 与 Docsy 兼容配置,不另造第二套命名空间或全局 resolver。
决策

OINK 保留 Hugo 原生键与仍有价值的 Docsy 兼容键,把主题呈现和行为放在 params.ui.* 下,并用同名的顶层 front matter 键提供页面覆盖。它不增加 params.oink.* 配置树,也不建立一套遮蔽 Hugo 配置模型的注册表。

背景

OINK 继承了成熟的配置面,又增加了阅读外壳、内容输出和本地交互。早期设计曾尝试把所有 主题自有键迁入一个新命名空间,并在每页一次性解析完整配置字典。这样会在 Hugo 原生键旁边 再造一种语言,使 section cascade 更复杂,迁移规模甚至超过它要控制的行为本身。

现行模型直接体现每一层的归属:

层次职责示例
Hugo站点身份、语言、菜单、输出、分类法、markup、模块baseURLlanguagesoutputs
站点事实与集成仓库、版本、作者、本地搜索、评论、外部服务params.github_repoparams.versionparams.comments
OINK 界面外壳、导航、呈现与本地交互params.ui.sidebar_*params.ui.typographyparams.ui.share
页面或栏目对可覆盖站点默认值的局部调整sidebar_enabledfeatured_imageshare
数据文件不是开关的结构化事实与有序内容data/landingdata/downloaddata/docs_nav.json

决策

配置 API 遵循以下规则:

  1. 站点事实保留在既有顶层;界面选择归入 params.ui.*
  2. 页面覆盖去掉 ui. 前缀,其余名称保持一致。section 的 cascade 可以把这个顶层键应用到后代。
  3. 一个布尔值足以表达完整政策时使用标量;只有真正存在下级设置时才使用 map。既有 map 可以接受 布尔速记。
  4. 名称采用正向、snake_case,并按功能分组。密切相关的设置共用前缀,不为此再建一层 resolver。
  5. 主题默认值声明在主题的 hugo.yaml 中。只有静态值会抹掉刻意存在的外壳差异时,模板才可以 推导默认值。
  6. 每个功能族负责自己的规范化与校验。共享 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 优先创作

原生 Markdown 承载常见语义;shortcode 只填补真实能力缺口,各内容模型延长共享外壳而不是分叉。
决策

Goldmark 能保留目标语义时,优先提供原生 Markdown 形态。只有原生形态无法表达真实能力时, 才保留 shortcode。新增内容场景时延长既有外壳和数据模型,不另建一套并行渲染系统。

背景

OINK 同时服务短手册、大型参考文档、发布归档、落地页和书籍。对十一个消费站点、五千多篇 Markdown 的盘点呈现了两个极端:有些页面几乎不用主题语法,有些页面则由大量嵌套 shortcode 与站点自有 layout 拼成。

只为后一类优化的组件 API 会变成私有 DSL;只支持纯 Markdown 又会迫使书籍、富图、标签页和 结构化发布退回站点自有 HTML。真正有用的边界是能力,而不是语法看起来是否新颖。

决策

OINK 按以下顺序设计:

  1. 原生 Markdown 优先。 列表可以成为 Steps、Cards 或 FileTree 标记;表格可以成为 Fields 或矩阵;blockquote 可以成为 callout;代码围栏、图片与 passthrough 块通过渲染钩子携带属性。
  2. shortcode 只补能力。 CommonMark 缩进、嵌套容器、处理选项或跨页登记无法安全表达同一结果时, 才保留全量 shortcode 形态。
  3. 语义实现只有一套。 原生形态与全量形态进入同一组规范化 partial 和输出契约,不能只是两种 外观相似的组件。
  4. 沿一条系统延长。 新 Landing 区块进入 section 注册表;新 Blog 呈现仍是 Blog 变体;Book 编号接入内容原语与导航系统。OINK 不为一个功能再造第二套卡片、落地页、导航或 Article 外壳。
  5. 事实不藏在呈现字符串里。 版本、仓库、日期与有序记录来自 front matter、站点参数或数据文件。 shortcode 参数不能成为第二个事实来源。

输出契约

只有在每种已启用输出中都得到明确语义结果,一种创作形态才算完整:

输出要求
HTML服务器端先输出完整语义内容,JavaScript 只做增强
Print静态、展开,不包含依赖交互的控件
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.7 - 设计研究

用于形成 OINK 设计决策的定期实验与消费站证据,不具备规范效力。
证据,不是契约

研究记录测量了什么、使用了哪些输入与工具版本。它可以解释决策,但不能覆盖当前契约或实现。

只有其他维护者能够检查方法、理解边界并复现相关检查时,研究才适合进入公开 Design 内容树。 原始 Agent 对话、临时构建日志和本机绝对路径不符合这一标准。

研究地图

记录证据
Goldmark 块属性支持的 Hugo 下限版本上,渲染钩子能看到什么,以及 CommonMark 容器的边界
消费站与迁移证据带日期的语料盘点与确定性 Book 迁移结果

发布规则

研究记录必须说明日期、输入、相关版本、方法、结果与已知边界。容易变化的数字明确标为快照。 涉及外部框架的比较,公开前要依据一手资料重新核验,并提炼成与 OINK 有关的结论,不能直接 复制成竞品目录。

研究结果成为稳定产品选择后,从已接受的决策链接它;如果它提出的 行为尚不存在,则把设计问题放入提案

7.7.1 - Goldmark 块属性实测

Hugo 0.160.1 与 0.164.0 上列表、图片、表格、passthrough、围栏、callout 与嵌套容器的可复现实测。
已验证快照

这些探针在 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 - 消费站与迁移证据

塑造 OINK 外壳、创作原语与确定性 Book 迁移策略的定期语料快照。
带日期的语料快照

这些计数描述 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 v2106 张图、3 张表、22 个代码示例,相关 304 条链接全部入账1 条题注链接降级为可见文本,无未解释跳过项
DDIA v190 张编号图与 203 条匹配引用14 张装饰性或无编号图片刻意不处理
TPME31 张图、10 张表、44 条编号引用与 1,018 条通用稳定引用被识别项目零跳过
私有 Book profile119 张图、5 张表与 136 条编号引用3 张歧义图片保留人工复核

每个 profile 都先干跑,只在歧义边界明确后写入;第二次执行变更数为零;随后以警告即失败的模式 构建,并通过渲染后的 kind、编号和锚点检查。公开迁移工具与当前 profile 边界见 创作书籍迁移契约

边界

这些数字不能直接用于产品宣传,也不能当作当前站点清单。重做研究时,需要重新确定仓库清单并生成 新的带日期报告。本公开记录刻意排除了本机路径、未提交内容、私有仓库名称、原始 Agent 对话与生成 构建产物。

7.8 - 设计提案与 PRD

仍在评估中的 OINK PRD 与设计草案的唯一双语归档位置。
非规范性材料

提案描述的行为可能尚不存在。当前行为由契约、已接受决策、实现与归属检查器定义。不能把提案 当作配置参考。

本栏目是 OINK 产品需求文档、RFC 风格设计与未决维护者提案的唯一正本位置。不要在主题仓库或 文档仓库中另建本地 plan/plans/proposal/ 或其它并行设计树。

当前提案

提案当前边界
反向链接与知识图谱草案;当前没有 graph 或 backlink 实现
媒体收敛草案;共享正文 resolver 与 Zoom marker 已落地,只记录剩余跨表面收敛
Agent 批量索引草案;每页 Markdown 与 llms.txt 已存在,全文合集与导航 JSON 尚不存在

新 PRD 放在哪里

创建一份英文主页面及其简体中文对页:

content/docs/design/proposals/<slug>.md
content/docs/design/proposals/<slug>.zh.md

两份文件都使用显式、稳定的英文标题 ID。中文页面中的代码、键、路径、版本与 API 名称保持原样。 提案开头要有可见的草案状态,并包含:

  1. 状态、负责人、日期和受影响契约面;
  2. 背景与证据;
  3. 目标与明确非目标;
  4. 提议行为,以及输出、无障碍、安全边界;
  5. 兼容与迁移影响;
  6. 实现与归属检查器计划;
  7. 验收标准与待决问题;
  8. 记录提案自身变化的决策日志。

大型实验可以在 ../research/ 下增加带日期的页面;临时日志与生成 产物不进入 Hugo 内容,也不进入 Git。

生命周期

草案提案
    ├── 拒绝或被替代 → 从活动树移除,由 Git 历史保存
    └── 接受
          ├── 实现与归属检查器
          ├── 受影响的中英文契约
          ├── 理由具有长期价值时新增已接受 Design 决策
          └── 相关受众需要时更新变更记录、迁移与用户文档

提案被接受后不会自动成为第二份契约。稳定行为进入归属契约,稳定理由进入 Decisions,用户步骤进入 相关指南,然后把提案退出活动导航。本地构建、提交、tag、公开模块、消费站 pin 与部署仍是相互独立 的完成状态。

评审门禁

实施前,评审者确认提案没有重复已有外壳、resolver、组件族或数据权威。实施期间,如果设计改变, 先更新这份双语提案,不能让代码悄悄漂移。验收至少覆盖主题的最窄归属检查、真实文档站、渲染后的 中英文、相关输出、无障碍与响应式检查。

7.8.1 - 反向链接与知识图谱

从普通 Hugo 链接推导反向链接、局部与全站图谱的三阶段设计草案。
PRD 草案,尚未实现

OINK 当前没有 backlink 区块、局部图谱、全站图谱页或 graph 输出格式。提案中的名称和配置在 提案被接受、契约发生变化之前都不是公开 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、邮件、同页锚点与自链接被排除;
  • refrelref 被纳入;
  • 每种语言生成相互独立的图;
  • 无法解析的派生边由警告或专项检查报告,但不会让普通 hugo server 不可用。

扫描原始源码存在已知遗漏。自定义 shortcode 参数或原始 <a href> 中的 URL 可能不会进入图谱。 必须明确记录这种遗漏,不能声称得到完整语义图。

G1 在页尾附近输出一份短小、有序的列表。排序必须确定:先按 section,再按导航 weight、标题,最后 用稳定路径破平。区块使用普通链接与标题,不把内容只藏在 disclosure 中;没有入链时不渲染。

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、严格构建负向用例、浏览器无障碍 与响应式测试,以及真实双语站构建。性能在有代表性的大站上测量,但带日期的原型耗时不能自动成为 永久预算。

待决问题

  1. G1 是 opt-in、opt-out,还是只为选定 shell type 开启?
  2. 局部图只暴露一层,还是允许严格限额的第二层?
  3. 哪些页面元数据值得进入 graph JSON?
  4. 无法解析的启发式边应保持静默并由专项链接检查报告,还是显示去重后的预览警告?
  5. 在 G1、G2 获得生产证据前,G3 是否值得新增输出格式?

7.8.2 - 媒体收敛

正文图片、编号图、Landing 媒体与代表图片选择之间剩余收敛工作的设计草案。
PRD 草案,只记录剩余工作

OINK 已经具备共享正文图片 resolver、单一 Zoom marker、可处理的 Markdown 图片、编号 figure 与安全的 Landing URL 处理。本页只提议尚未解决的收敛问题,不能把它读成“当前缺失功能清单”。

当前基线

正文图片钩子、编号 fig、卡片与 gallery 统一通过 content/image-resolve.html 解析页面资源、 section 资源、全局资产、static 文件与显式远程 URL。栅格资源可以提供固有尺寸与处理后派生图。 HTML Zoom 资格使用 data-td-image-zoom 标记;构建期检测只查找主题自己输出的标记。

独占 Markdown 图片已经可以把题注或 Book 编号与图片处理、链接组合起来。编号图片 figure 共用 td-figuretd-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 能力决策

从两个答案中明确选择一个:

  1. 为全量 fig 的来源形态增加处理参数,并通过同一处理 helper 规范化;或者
  2. 处理能力只属于原生 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 的能力选择明确后, 提案才能被接受。

待决问题

  1. 一份共享结果结构是否足够,还是共享更底层的 URL/资源记录会让 resolver 归属更清晰?
  2. Landing 应消费资源署名,还是只消费尺寸与 URL?
  3. 原生图片已经能组合编号、题注、链接和处理后,全量 fig 处理能力是否仍有真实消费需求?
  4. 哪些输出兼容名称仍被真实消费站使用?

7.8.3 - Agent 批量索引

基于 OINK 既有 Markdown 输出与导航权威,可选生成 llms-full 全文包和稳定导航 JSON 的设计草案。
PRD 草案,部分前提已经存在

OINK 已经支持每页 Markdown、语言内 llms.txt、HTML discovery link 与 Copy Markdown。 当前不发布 llms-full.txt 或导航 JSON。本页只提议这两类剩余输出。

当前基线

站点可以为 page 与 section 启用 Hugo 的 Markdown 输出,并为 home 启用生成 llms.txtLLMS 输出。OINK 把 shortcode 渲染成语义化 Markdown,保留源码 URL 和语言内 LLMS 索引发现信息,Copy Markdown 也读取同一个 alternative output URL。主题声明输出格式,但不强迫站点选择哪些 outputs

导航已经存在权威链:有显式 data/docs_nav.json 树时使用它,否则使用内容树与 weight。侧栏、 pager 与已声明 section index 共用这一权威。机器导航输出必须从同一棵树派生,不能再造排序。

目标与非目标

目标:

  • 为小型站点可选装配语言内全文包,为大型站点可选按顶层 section 分包;
  • 可选发布带版本的导航 JSON,供 Agent 与外部工具使用;
  • 复用人工站点的同一 Markdown 页面渲染器、页面纳入规则与导航权威;
  • 所有输出仍通过 Hugo output 配置 opt-in;
  • 验证链接、语言隔离、media type 与确定性顺序。

非目标:

  • 替换每页 Markdown 或 llms.txt
  • 新建 params.oink.* 配置树;
  • 在 Hugo 构建期间抓取生成好的 public/ 文件;
  • 嵌入私有源码路径、草稿页面或跨语言回退;
  • 承诺一个巨型全文包适合所有模型上下文。

全文包

提议的 llms-full.txt 输出拼接每页输出所用的同一份语义化 Markdown。页面之间使用稳定、可见的 分隔符与来源 URL。站点从两种部署形态中选择:

形态位置适用场景
全站包每种语言在语言根下一个文件小型、聚焦站点
section 分包每个显式启用的顶层 section 一个文件大型参考站与书籍

由 Hugo output 配置决定哪些页面获得该格式,而不是由主题参数决定。主题可以提供检查器,报告意图 与实际输出不一致,但不能修改站点输出集合。

全文包在 Hugo 内部通过共享页面渲染 partial 组装,不读取 public/ 中的兄弟产物,也不依赖输出 构建顺序。文件大小作为证据报告;任意阈值不能通过警告让 --panicOnWarning 拒绝原本合法的发布。

导航 JSON

提议的 JSON 包含 schema 版本、语言、根节点与递归有序节点。页面节点包含稳定 ID、标题、HTML URL、 启用时的 Markdown URL、kind/type、weight 与 children。显式外部导航节点只包含标签、URL 与 external kind。

输出遵循渲染侧栏相同的可见性与排序规则,排除 draft、headless resource、隐藏导航项与当前语言 不可用页面,永不序列化本机文件名。

该格式拥有自己的 JSON Schema 与 golden 夹具,并标记为 notAlternative,避免 Hugo 把它广告为 页面级 alternate。

发现信息与输出边界

llms.txt 可以链接已经启用的全文包与导航 JSON。HTML head 继续发现每页 Markdown 和语言内 LLMS 索引,不把每个批量产物塞进每一页。

shortcode、Landing section、Book 目标与交互组件继续使用当前 Markdown 降级。新输出无权增加组件 HTML、脚本、评论、反馈控件或导航 chrome。

验收标准

  • EN 与 ZH 输出只包含各自语言的页面和 URL。
  • 每个列出的 Markdown URL 都存在;每个导航 URL 都可解析,或明确标记为外部节点。
  • 同一根下的顺序与渲染侧栏、pager 一致。
  • 固定 Hugo 版本与输入时,相同源码重建得到字节稳定输出。
  • 新格式关闭时,HTML、Markdown、Print、RSS 与 LLMS golden 均无回归。
  • 大站夹具能证明按 section 分包,而不是为每个嵌套 section 都生成文件。

待决问题

  1. 两种全文部署形态是否都需要,还是只按 section 分包更安全?
  2. 导航 JSON 应当是 home output,还是由 resource template 支撑的专用内容页?
  3. 哪些节点元数据足够稳定,可以进入 schema version 1?
  4. 导航 JSON 存在时,llms.txt 是否默认列出它?
  5. 检查器应报告哪些体积证据,又不武断执行某个模型上下文上限?