跳转到主要内容

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

返回本页常规视图.

定制站点

站点级配置:品牌、导航、布局、搜索、多语言、多版本、打印与 Agent 输出。

本栏目覆盖站点级配置:hugo.yml 里的参数、data/ 下的数据文件、assets/ 下的样式入口。单个页面的写法与 front matter 见创作内容

按改动目标查找

改动目标对应页面
站名、Logo、favicon品牌外观
配色、深浅色模式、字体品牌外观
顶栏菜单与下拉导航与菜单
侧栏宽度、图标密度、目录深度布局与页面类型
首页与落地页首页与落地页
全文检索与索引范围全文检索
命令面板里的条目命令面板
快捷键键盘导航
新增一门语言多语言
多版本站点与归档横幅多版本
标签与分类分类体系
编辑本页、最后修改、贡献者仓库与页面信息
打印与整章导出打印支持
llms.txt 与每页 .md 输出Agent 支持
某个参数的类型与默认值配置总览

评论、分析与部署需要接入外部服务,见维护管理

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 应与它一致。

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

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. 部署到子路径时,确认站内链接与图片都带上了前缀。

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 - 布局与页面类型

用 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 没有重复输出路径的告警。

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

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)里不应该出现任何面板痕迹。

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:应该瞬间定位,没有滑行。

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,见上一节的脚本。

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 打开命令面板,「切换版本」列出的应该是同一份清单。

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
  • 右栏分类云没有开关,也没有条数上限;术语数量很多的站点应当减少分类法,配置层面没有裁剪手段。
  • 术语页没有跨语言对等关系:语言切换在术语页上不保证落到「同一个术语的另一种语言」。

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

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

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 里是围栏源码,不是图。