这是本节的多页打印视图。 .
定制站点
- 1: 配置总览
- 2: 品牌外观
- 3: 首页与落地页
- 4: 导航与菜单
- 5: 布局与页面类型
- 6: 全文检索
- 7: 命令面板
- 8: 键盘导航
- 9: 多语言
- 10: 多版本
- 11: 分类体系
- 12: 仓库与页面信息
- 13: 打印支持
- 14: 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 module | Hugo 本身,行为见 gohugo.io |
params 顶层 | logo offline_search github_repo version page_width comments | 主题读取的站点级选项 |
params.ui.* | navbar_enabled sidebar_width_min typography pager_types | 外壳、导航与阅读界面 |
params.<运行时> | mermaid plantuml drawio markmap | 各内容运行时自己的开关与端点 |
最小的可用配置只需要前两层:
配置原则
- 主题默认保守,只写要改的键。交互功能(本地搜索、图片缩放、评论、反馈、深浅色菜单)默认关闭,主题不替站点做策略决定。从一份「完整配置」逐条删减,比按需添加更容易留下用不上的键。
- 没有主题总开关。不存在
oink.enabled,也没有params.oink.*命名空间,更没有在「Docsy 外壳」与「OINK 外壳」之间切换的选项。这一页查不到的开关即不存在。 - 非法值告警并回退到文档里写明的默认值。
params.ui.typography: solarized报invalid params.ui.typography "solarized" (allowed: technical | system) -- using "technical",站点照常构建;footer_style: thin、page_width: huge、section_index: grid同理。一个笔误因此只降级一个设置,而不是让hugo server下每个 URL 都返回 HTTP 500。它也不会因此静悄悄上线:所有发布关卡都带--panicOnWarning构建,那条警告在那里仍然是硬失败。 - 仍有少数情况会中断构建,它们都属于「继续构建就会发布出错误内容」而非「发布出朴素内容」。需要外部端点的功能——PlantUML、Draw.io、Algolia——缺少端点时报错,因为主题不会代为连接公共服务;残缺的上游署名报错,因为半条声明读起来和完整的一模一样。
params.offline_search_index、release事实,以及解析不到目标的内容引用同理。
页面级覆盖优先级
Hugo 的 .Param 查找让大部分参数可以逐页覆盖,优先级从高到低:
- 页面自己的 front matter;
- 祖先分区
_index.md里的cascade(离页面越近越优先); - 站点
params。
写进 front matter 时要去掉 ui. 前缀。
站点上的 params.ui.scroll_spy 在页面里就写成 scroll_spy。front matter 里出现 ui:
块的话,里面的键没有人读,也没有人报错——某个设置看着没生效时,先对照页面参数核一遍键名。
分区级用 cascade 一次设定整棵子树:
覆盖用于真实的内容差异。逐页重建一套视觉系统的配置,会在主题升级后失配。
三项 goldmark 前置
Hugo 不会 把主题模块的 markup 配置合并进站点,这三项必须写在站点自己的 hugo.yml 里,否则属性行、组件 HTML 与数学公式都不工作:
缺 attribute.block 时,{.fields} {.steps} {caption=…} 会原样显示成文字;缺 passthrough 时 \(x\) 不会变成公式;缺 unsafe 时步骤与卡片的结构会被转义。
renderer.unsafe: true 同时允许 Markdown 正文里的原始 HTML 通过,面向的是受信任的作者,不是投稿过滤器。内容来自不可信来源时,审查应放在提交流程里。
站点身份与品牌
Hugo 原生顶层键:
title,- 站名,显示在顶栏、
<title>与页脚 baseURL,- 生产域名;子路径部署时带上路径段
copyright,- 版权行的兜底值,
params.copyright未设时按 HTML 原样渲染 enableGitInfo, ,- 打开后才有「最后修改」与 commit 信息
enableRobotsTXT, ,- 生成
robots.txt enableEmoji, ,- 允许
:smile:简码
主题参数:
params.logo, ,- 品牌图标,可指向
assets/资源或static/路径,见品牌外观 params.wordmark,- 横向字标;设置后顶栏用它替代「图标 + 站名」
params.description,- 站点描述,页面没有
description时作为 meta 兜底 params.copyright,- 字符串按 Markdown 渲染;map 接受
authorsfrom_yearto_year(present表示今年) params.footer_center_info, ,- 页脚中间的行内 Markdown,设为空字符串即隐藏
params.author,- RSS 的作者;map 接受
name与email
favicon 没有参数:主题按约定名扫描 static/(favicon.ico favicon.svg favicon-NxN.png apple-touch-icon.png apple-touch-icon-NxN.png),见品牌外观。
外壳类型与栏目根
外壳按 页面 type 生效,不看路径。文档可以放在任意目录,再用 cascade 给它 type: docs。
params.ui.shell_types, ,- 哪些 type 使用带侧栏的阅读外壳,见布局与页面类型
params.ui.docs_section, ,- 文档栏目的根目录名,只用于导航解析
params.ui.blog_section, ,- 博客栏目的根目录名
params.ui.docs_sidebar_root, ,section时 docs 页的侧栏根是文档栏目;home时是站点首页。非法值告警并回退params.ui.quick_links, ,- 命令面板空查询时列出的顶层菜单 identifier,见命令面板
params.ui.sidebar_root_enabled, ,- 允许子分区用
sidebar_root_for: self自成一棵侧栏树 params.ui.sidebar_root_menu, ,- 侧栏顶部显示栏目切换器;只有一个入口时退化为普通链接
params.ui.section_index, ,- 栏目首页子页列表样式:
list或cards,可按分区覆盖 params.ui.section_index_columns, ,section_index: cards时的列数
博客
三个键决定博客栏目的样子。它们作用于 params.ui.blog_section 指定的栏目,每一个都能通过博客根目录的 front matter 或 cascade 按栏目覆盖。
params.ui.featured_image, ,- 文章正文里怎么渲染自己的题图:
none不渲染,banner在标题上方框出一张 16:9 的图,wash把它铺在文章头部背后、只留十分之一的不透明度。用的就是这一页在卡片与og:image里已经在用的那张图,两处不会打架。没有题图的文章在两种模式下都不渲染任何东西 params.ui.blog_index, ,- 博客栏目列表页的形态:
list是行列表,cards是内容卡片网格,卡片带 16:9 题图、日期与栏目行,以及三行摘要。按年分组、分页与manual_link在两种形态下行为一致 params.ui.blog_index_columns, ,blog_index: cards时的列数;md 到 xl 之间恒为两列,md 以下一列,不受此值影响
作者与系列是 taxonomy 而不是参数,见分类法与写博客。
顶栏与页脚
params.ui.navbar_enabled, ,- 是否渲染站点顶栏,可用页面顶层
navbar_enabled覆盖,见导航与菜单 params.ui.navbar_autohide, ,- 顶栏收到视口上方,指针进入唤醒区才出现;小于 768px 或粗指针时不生效
params.ui.footer_style, ,fat多列网格 + 版权行,slim只有版权行,none不渲染。非法值告警并回退params.ui.dark_mode, ,true同时启用深色调色板与主题控件;只要控件写dark_mode: { show_menu: true }params.ui.breadcrumb, ,- 面包屑;设为
false关闭。顶层分区本来就省略只有一级的面包屑 params.ui.page_context_menu.enable, ,- 标题旁的页面操作拆分按钮
params.ui.page_context_menu.assistant_links, ,- 显示「在 ChatGPT / Claude 中打开」;读者点击时完整 URL 会离开本站
params.ui.page_context_menu.links, ,- 自定义外部操作,
url支持{url}{title}{markdown_url}占位符 params.ui.github_stars,- 顶栏 GitHub 徽标上的星数,本地常量,不发请求
params.ui.alt_site,- 单语言站在页脚显示的姊妹站链接,必填
label与绝对http(s)的url
胖页脚的列数据来自 data/footer/<语言>.yaml,不是参数,见导航与菜单。
侧栏
params.ui.sidebar_menu_compact, ,- 只展开当前分支与邻近条目
params.ui.sidebar_menu_foldable, ,- 允许读者展开/折叠分区
params.ui.sidebar_menu_truncate, ,- 一个分区最多渲染的条目数,超出截断
params.ui.sidebar_cache_limit, ,- 站点页数超过它就复用共享导航标记,active 状态改由浏览器还原
params.ui.sidebar_width_min, ,- 桌面端拖拽调宽的下限,像素
params.ui.sidebar_width_max, ,- 拖拽调宽的上限,像素
params.ui.sidebar_item_overflow, ,ellipsis长标题省略,wrap换行params.ui.sidebar_icon_policy, ,- 图标密度:
all全部、groups只有根与有子页的节点、none全不显示。非法值警告并回落all params.ui.sidebar_expand_levels, ,- 默认展开的树层级数
params.ui.sidebar_headings, ,- 只对
type: book生效:在侧栏当前行下展开标题分支;整数取值 2–4,true等于 2 params.ui.sidebar_enabled, ,- 左侧栏;设为
false关掉,通常按页面而不是按站点设置 params.ui.taxonomy_icons,- 按分类复数名指定右栏分组图标,例如
tags: fa-solid fa-tags
侧栏怎么用见布局与页面类型;目录树本身由 content/ 的结构决定,见组织内容。
目录 TOC
右栏大纲的层级由 Hugo 原生配置决定,主题只控制跟踪行为:
markup.tableOfContents.startLevel, ,- Hugo 原生:收录的最高标题级别
markup.tableOfContents.endLevel, ,- Hugo 原生:收录的最低标题级别
params.ui.scroll_spy, ,- 滚动位置跟踪;设为
true打开活动项高亮
单页隐藏大纲用 front matter notoc: true,见页面参数。
翻页与页尾
页尾组件顺序固定为分享 → 反馈 → 页面信息 → 翻页 → 评论,五者独立开关。
params.ui.share, ,- 页尾分享目标,按给定顺序渲染,取值来自
xblueskymastodonfacebooklinkedinreddithackernewstelegramwhatsapplinepinterestweibochatgptclaudeemailcopy。为空则不出现分享栏。每一项都是纯粹的 intent 链接——没有 SDK、没有 iframe、没有第三方脚本、没有分享计数,见写博客。未知目标告警并丢弃 params.ui.pager_types, ,- 哪些 type 显示上一页/下一页;单页用 front matter
pager: false退出。未知 type 告警并丢弃 params.ui.annotation, ,- 正文末尾的「最后修改」与出处区块;上游署名由页面的
upstream_link一族键驱动,见页面参数 params.ui.translation_notice, ,- 权威版本的语言代码,译文页据此显示一条指回原文的说明;页面写
translation_notice: false退出 params.ui.reading_time, ,- 页面标题下显示阅读时长
params.ui.book_draft_banner, ,- Book 草稿页开头额外加一条横幅
搜索与命令面板
本地搜索默认关闭;打开后命令面板才会出现(顶栏放大镜、Cmd/Ctrl 加 K、/、\)。
params.offline_search, ,- 生成每语言一份本地索引并启用命令面板,见全文检索
params.offline_search_on_serve, ,hugo server预览时也构建索引,预览行为与线上一致;站点极大时设false跳过以加快本地重建params.offline_search_index, ,- 索引范围,逐级累加:
titleheadingsummarycontent。非法值构建失败 params.offline_search_summary_length, ,summary档摘录截断的字数params.offline_search_max_results, ,- 结果条数上限,同时约束 Lunr 与中文子串兜底
params.ui.landing_search, ,layout: landing页面是否保留搜索入口params.ui.command_palette.commands, ,- 自定义命令,每条二选一:
url或内置action;见命令面板 params.gcs_engine_id,- Google 可编程搜索引擎 ID,启用后引入外部服务
params.search.algolia,- Algolia DocSearch,必须显式给出
appIdapiKeyindexName,缺一构建失败
自定义命令的每条记录只接受 id title description icon keywords url action 七个键;id 必须匹配 ^[a-z][a-z0-9_-]*$,且不能与内置动作 ID 重名。分语言的标题写在 languages.<lang>.params.ui.command_palette.commands。
键盘
params.ui.keyboard_nav, ,- 单键导航(WASD/方向键走树、j/k 跳标题、q/e 翻页、面板与外壳开关)。设为
false后运行时不进包,见键盘导航
图片缩放
params.ui.image_zoom, ,- 允许正文图片点击放大;页面用 front matter
image_zoom覆盖。非布尔告警并回退
哪些图片会成为缩放候选见图片。
字体排版
params.ui.typography, ,technical用随主题分发的 Inter / Chakra Petch / IBM Plex Mono;system只用平台字体栈,不请求品牌字体。非法值告警并回退params.page_width, ,- 外壳整体宽度:
normalwidefull,可逐页覆盖 params.reading_width, ,- Book 页正文的阅读行宽:
slimnormalwide,不影响外壳
自定义字体与配色走 SCSS 入口而不是 YAML,见品牌外观。
评论与反馈
params.comments.enable, ,- 站点级评论开关,页面用 front matter
comments覆盖,见启用评论 params.comments.type, ,- 目前只有
giscus会真正渲染 params.comments.giscus.repo,- 承载讨论的 GitHub 仓库,必填
params.comments.giscus.repoId,- 仓库 ID,必填
params.comments.giscus.category,- 讨论分类名,必填
params.comments.giscus.categoryId,- 讨论分类 ID,必填
params.comments.giscus.mapping, ,- 页面与讨论的映射方式
params.comments.giscus.term,mapping为specific或number时的讨论标题或编号;不设置时不输出这个属性params.comments.giscus.strict, ,- 严格标题匹配
params.comments.giscus.reactionsEnabled, ,- 显示主贴表情
params.comments.giscus.emitMetadata, ,- 向父页面发送讨论元数据
params.comments.giscus.inputPosition, ,- 输入框在评论列表上方还是下方
params.comments.giscus.theme, ,- giscus 主题,
auto跟随站点深浅色 params.comments.giscus.lightTheme, ,- 浅色模式下使用的 giscus 主题或自定义 CSS URL
params.comments.giscus.darkTheme, ,- 深色模式下使用的 giscus 主题或自定义 CSS URL
params.comments.giscus.loading, ,- iframe 加载策略
params.comments.giscus.lang, ,- giscus 界面语言。不设置时中文站解析为
zh-CN/zh-TW/zh-HK,其它语言取主语言代码,giscus 不支持则回落en params.comments.giscus.ariaLabel, ,- 评论区容器的
aria-label;默认值是英文,多语言站点需按语言各写一份 params.comments.giscus.errorMessage, ,- 加载失败时显示的文字;默认值是英文,多语言站点需按语言各写一份
params.ui.feedback.enable, ,- 页尾「这页有帮助吗」两个按钮;无后端,有
gtag时记录结构化事件 params.ui.feedback.reasons, ,- 选「否」后展开四个可选原因
四个 giscus 必填项缺任意一个,评论区就不渲染:不报错,也不出现。
仓库链接与页面信息
params.github_repo,- 内容仓库 URL,解析「编辑本页」「查看历史」「新建子页」「提文档 issue」,见仓库与页面信息
params.github_project_repo, ,- 产品仓库 URL,用于「提项目 issue」与顶栏 GitHub 入口
params.github_branch, ,- 编辑链接指向的分支
params.github_subdir,- 内容站在 monorepo 里的子目录
params.path_base_for_github_subdir,- 源路径重写;map 形式接受
from与to params.github_url, ,- 已移除,改写
params.github_repo。那份负责提示替代键名的迁移登记表已经删掉,所以旧键现在只是一个没人读的键 params.ui.lastmod_commit, ,- 「最后修改」后面附什么:
subjectcommit 标题、hash短哈希、none不附。非法值告警并回退 params.images, ,- 站点级社交卡片:页面自己没有封面时用它填
og:image;只进元数据,不会渲染成列表缩略图 params.default_featured, ,- 已移除,改写
params.images或栏目cascade里的images。同上,旧键现在只是一个没人读的键
内容运行时
Mermaid、KaTeX、ECharts、Infographic、Asciinema、Swagger UI 与 Redoc 按内容自动检测,页面用到才加载,没有站点开关。需要开关或外部端点的只有这几个:
params.markmap, ,- 站点级启用思维导图围栏,见思维导图
params.mermaid,- 透传给
mermaid.initialize()的配置;键名全小写,深色模式自动覆盖theme params.plantuml.enable, ,- 启用 PlantUML 围栏,见 PlantUML
params.plantuml.svg_image_url,- PlantUML 服务的 SVG 端点,启用时必填,缺失构建失败
params.plantuml.svg,- 用内联 SVG 而不是
<img>渲染 params.drawio.enable, ,- 启用
.drawio.svg图片的编辑按钮,见 Draw.io params.drawio.drawio_server,- Draw.io 编辑器地址,启用时必填,缺失构建失败
params.highlight_classes, ,- 代码高亮输出 Chroma class;设
false回到 Hugo 的行内样式 params.ui.code_copy, ,- 代码块的复制按钮;设为
false全局去掉,围栏上的copy=仍然优先
数学公式不需要参数,只需要 passthrough 前置。
输出格式
主题声明了两种自定义输出格式,但 不替站点打开:要哪种就在 outputs 里写哪种。
| 格式 | 产物 | 说明 |
|---|---|---|
HTML | index.html | 交互形态,必选 |
markdown | index.md | 每页的纯 Markdown 版本,页面操作里的「复制 Markdown」「查看源码」依赖它,见 Agent 支持 |
LLMS | llms.txt | 主题声明的纯文本格式,通常只挂在 home |
print | _print/index.html | 主题声明的整分区打印页,见打印支持 |
RSS | index.xml | Hugo 原生,挂在 section 上让每个栏目都有订阅源 |
打印输出的两个参数:
params.print.toc, ,- 打印页开头生成目录;设为
false不生成 params.print.section_break_wordcount, ,- 打印页中一节多少词以上才另起一页
多语言与版本
语言用 Hugo 原生的 languages 块定义,主题只读它建立的翻译关系:
defaultContentLanguage, ,- 不带路径前缀的首要语言
languages.<lang>.label,- 该语言的自称,显示在语言菜单里
languages.<lang>.locale,- 完整 locale,用于
<html lang>与 SEO languages.<lang>.weight,- 语言顺序,也是点击语言图标时的循环顺序
languages.<lang>.title,- 该语言的站名
languages.<lang>.languageDirection, ,- RTL 语言设为
rtl
写作侧的对等文件、锚点对齐与缺译回退见多语言。
版本相关参数:
params.version,- 当前站点变体的版本标识(不一定是 Git ref),见多版本
params.version_menu, ,- 版本菜单的标题
params.version_menu_pagelinks,- 切版本时先尝试目标站点的同一路径
params.versions,- 版本条目:
versionurlkind,name: '---'是分隔线 params.archived_version,- 顶部显示「这是归档版本」横幅
params.url_latest_version,- 归档横幅里指向最新版的链接
params.time_format_blog, ,- 博客日期格式,按语言覆盖
params.time_format_default, ,- 其它日期格式,按语言覆盖
其它
验证配置变更
改完配置跑一次严格构建:
输出 Total in … 且没有 ERROR / WARN 才算通过。常见报错与原因:
| 报错片段 | 原因 |
|---|---|
invalid params.ui.typography | 预设只有 technical 与 system |
invalid footer_style … (allowed: fat | slim | none) | 页脚形态写错,报错会指出是哪个页面 |
invalid page_width … (allowed: normal | wide | full) | 页宽写错 |
invalid params.ui.section_index … (allowed: list | cards) | 栏目首页样式写错 |
invalid params.offline_search_index | 索引范围只有 title heading summary content |
params.plantuml.enable requires an explicit params.plantuml.svg_image_url | 开了 PlantUML 却没给端点 |
params.drawio.enable requires an explicit params.drawio.drawio_server | 开了 Draw.io 却没给服务地址 |
params.search.algolia requires explicit appId, apiKey, and indexName | Algolia 三项必须齐全 |
params.ui.image_zoom must be a boolean | 写成了字符串 "true" |
command … must define exactly one of url or action | 自定义命令同时给了 url 和 action,或两个都没给 |
invalid params.ui.sidebar_icon_policy …; using all | 只是警告,但取值拼错了 |
配置改动还要至少验证三件事:每种语言各一页、缺译页的回退、生产 baseURL 下的链接(子路径部署容易漏)。
主题声明的 Hugo 下限是 0.160.1,当前验证版本是 0.164.0。改动配置后按这两个版本各构建一次,可以及早发现只在新版本可用的特性:
下限版本写在主题的 hugo.yaml 与 theme.toml 里,站点自己的 module.hugoVersion.min 应与它一致。
相关
2 - 品牌外观
本页覆盖站点外观:站名与 Logo 写在 hugo.yml,配色与字体走 SCSS 入口,页宽与页脚形态是参数。前提是站点已能构建(十分钟上手)。
需要改动的文件有四个:hugo.yml、static/ 下的图标、assets/scss/_variables_project.scss、assets/scss/_styles_project.scss。不要改主题目录里的文件:主题是 Hugo Module,升级时整个目录会被替换。
站名
站名出现在顶栏、浏览器标题与页脚。多语言站每种语言各写一个:
顶层 title 是兜底,languages.<lang>.title 优先。
Logo 与字标
主题默认使用自带的 assets/icons/logo.svg。替换步骤是把图标文件放进站点的 assets/ 或 static/,再在配置里指向它。
params.logo是方形图标,顶栏、侧栏与页脚共用。放在assets/下会经过 Hugo 资源管线(可指纹化),放在static/下按原样发布;两种写法都是相对assets/、static/根的路径。params.wordmark是横向字标。设置后顶栏用它替代「图标 + 站名」,窄屏放不下时回落到params.logo。不设置则保持「图标 + 站名」。
源 SVG 应紧贴图形边缘裁切,否则各处尺寸对不齐。SVG 必须带 viewBox,颜色继承 currentColor,或者在深浅色下都有足够对比度。
本站两个参数都不设:顶栏用主题自带的 assets/icons/logo.svg 搭配以展示字体渲染的站名。
favicon
favicon 没有参数。主题扫描站点 static/ 目录里的约定文件名,发现哪个就在每个页面输出对应的 <link>:
| 文件 | 生成的链接 |
|---|---|
static/favicon.ico | rel="icon" |
static/favicon.svg | rel="icon" type="image/svg+xml" |
static/favicon-32x32.png | rel="icon" 带 sizes,按尺寸升序输出 |
static/apple-touch-icon.png | rel="apple-touch-icon" |
static/apple-touch-icon-180x180.png | rel="apple-touch-icon" 带 sizes |
够用的最小组合是 favicon.ico + favicon.svg + apple-touch-icon.png。带尺寸后缀的文件必须是正方形(NxN),否则不会被识别。
这些文件用任意图形工具生成即可。主题不需要 Node.js,Hugo 只发布 static/ 里已经存在的文件。
Web App Manifest 一类的额外 head 元数据不在扫描范围内,用 layouts/_partials/hooks/head-end.html 钩子自行输出;要改变发现规则本身(换目录、增加文件名),在站点 layouts/ 下覆盖 layouts/_partials/favicons.html。
主色与配色
配色分两层:Bootstrap 的语义色(编译期 Sass 变量)和 OINK 的品牌层(运行期 CSS 自定义属性)。
先改语义色,它决定按钮、链接、提示块的色调:
这个文件在 Bootstrap 与 OINK 默认值 之前 加载,是覆盖 Sass 变量的位置。需要引用 Bootstrap 已定义的变量或 map 时,改用 _variables_project_after_bs.scss。
品牌层是一组 CSS 自定义属性,浅色和深色 必须成对覆盖,否则一种模式下会漏色:
可覆盖的品牌属性有 --td-brand-elev(浮层底色)、--td-brand-silk(次要文字)、--td-brand-copper 与 --td-brand-copper-dim(强调色与它的弱化版)、--td-brand-line-strong(分隔线)、--td-brand-header-bg(顶栏背景)、--td-brand-shadow-sm / --td-brand-shadow-md(阴影)、--td-brand-mark-from / --td-brand-mark-to / --td-brand-mark-gradient(品牌渐变)。
深浅色模式
主题默认 不显示 深浅色控件。开启方式:
开启后顶栏出现一个主题控件:点击在浅色与深色之间切换,悬停或键盘聚焦展开「跟随系统 / 浅色 / 深色」。读者的选择存在浏览器本地,没有选择时跟随 prefers-color-scheme。切换脚本在首屏绘制前设置好 data-bs-theme,不会出现主题闪烁。
只要深色调色板、不要控件时写 dark_mode: { show_menu: false, enable: true };dark_mode: false(默认)两者都不启用。
自定义组件在两种模式下都要给出可读的悬停、聚焦、禁用、选中状态,正文对比度至少 4.5:1、大号文字 3:1。
字体
字体有两档预设,在构建期决定,不涉及 JavaScript:
technical(默认):界面与正文用随主题分发的 Inter(可变字重,拉丁 / 西里尔 / 希腊 / 越南语子集,中文与 emoji 落到平台字体),标题装饰用 Chakra Petch,代码用 IBM Plex Mono。字体文件都是本地的,不请求 Google Fonts。system:界面、展示、元数据、打印与等宽角色全部回到平台字体栈,浏览器不请求品牌字体。字体文件仍随主题分发,只是不被引用。
非法取值让构建失败(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/,在项目样式里声明字面,再改写角色:
角色按 CSS 规则继承,只给某类内容换字体也不必复制组件选择器:
等宽字体要带中文兜底,否则中英混排的代码块会对不齐:
从 Docsy 迁移过来的站点不必改写法。旧的 Sass 变量仍然喂进对应角色,写在 _variables_project.scss 里照样生效,优先级高于预设默认值:
| 旧 Sass 变量 | 喂给的字体角色 | 说明 |
|---|---|---|
$td-fonts-serif | --td-ui-font-family / --td-body-font-family | Docsy 的界面字体栈,赋值给 $font-family-sans-serif |
$font-family-sans-serif | --td-ui-font-family / --td-body-font-family | 项目给出自己的栈时,technical 预设不再把 Inter 放在它前面 |
$font-family-base | --td-ui-font-family / --td-body-font-family | Bootstrap 的正文变量,经 --bs-body-font-family 进入角色 |
$headings-font-family | --td-heading-font-family | 不设置时标题继承正文角色 |
$font-family-code | --td-code-font-family | 代码、终端与 pre / code / kbd |
$td-font-family-monospace | --bs-font-monospace | 赋值给 $font-family-monospace |
$font-family-monospace | --bs-font-monospace | system 预设下,项目的显式取值优先于平台等宽栈 |
Docsy 的三个 Google Fonts 变量 $td-enable-google-fonts、$td-google-font-name 与 $td-web-font-path 主题已不再读取。它们留在 _variables_project.scss 里不影响构建,也不产生任何效果:随主题分发的是 Inter、Chakra Petch 与 IBM Plex Mono,两档预设都不向 Google Fonts 发请求。打印角色 --td-print-font-family 跟随正文角色,主题不为纸张单独提供字体。
YAML 里不接受远程字体 URL,也不接受任意 CSS:字体文件与样式都必须是可审查的本地输入。
页宽
page_width 控制外壳整体宽度,可逐页或按分区 cascade 覆盖。Book 页另有一个 reading_width(slim / normal / wide),改的是正文阅读行宽,不是外壳。两个键取值非法都让构建失败。
页脚
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>。
验证
- 构建输出
Total in …,没有 ERROR / WARN; - 页面源码里
<html>上有data-td-typography="technical"(或所选的预设); - 浏览器中顶栏显示自己的 Logo 与站名,标签页图标是自己的 favicon;
- 切到深色模式再看一遍正文、表格、提示块、代码块与焦点框。配色改动容易只在一种模式下验证过;
- 换一种语言,确认站名随之切换。
字体是否已替换,用浏览器开发者工具查任意一段正文的 font-family:应当是自己声明的字面,而不是 Inter。
相关
3 - 首页与落地页
首页不是模板,是一份数据:data/home/<语言>.yaml 里的 sections 列表决定页面从上到下有哪些分区,每个分区的内容在同一份文件里按名字取。普通页面加 layout: landing 也能用同一套分区。
分区全部在服务端渲染。价格、star 数、截图、头像、下载状态都必须在 Hugo 启动前就存在于仓库中,没有分区会在浏览器里取数据。
从 Docsy 的 blocks/* 首页迁移过来的站点要重写首页:主题没有 blocks/cover、blocks/section、blocks/feature 这些 shortcode,保留它们会让构建报 template for shortcode "blocks/cover" not found。两条出路是本页讲的 data/home/<语言>.yaml,或者给一个普通页面加 layout: landing。
首页的数据来源
首页的内容文件只留标题与描述:
分区数据按语言分文件:
首页数据
- data/
- home/
- en.yaml英文站首页
- zh.yaml中文站首页
- home/
查找顺序是 data/home/<当前语言>.yaml → data/home/en.yaml → 单语言站点的 data/home.yaml。
文件结构只有两层:一个 sections 列表,加上被列表引用的同名键。
这是本站首页的写法,完整文件见仓库的 data/home/zh.yaml。
最小可用首页
粘贴下面这段,替换文字与链接即可发布。链接写成不带前导斜杠的站内路径,主题会补上当前语言前缀(docs/start/ → /zh/docs/start/)。
Hero
Hero 是首屏,唯一一个带大标题与配图的分区。
不写 title_lines 时用 title,两者都没有时用站点标题。配图是 CSS 背景图,alt 有值时容器带 role="img",无值时对辅助技术隐藏。
align: center 是纯文字的居中首屏:文案块加宽居中,标题自动平衡换行,note 挪到按钮下方。它不接受 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 即变成构建失败。
常用分区的最小写法
卡片与能力面板是最常用的两种。cards 用 columns 控制列数:
capabilities 是一屏一条能力,右边配一块结构化的视觉面板,visual.type 只能是 shell、components、code、image、card 五种之一:
这些片段摘自主题仓库的可执行回归夹具
tests/site/data/landing/demo/en.yaml,字段名可照抄。
download 分区消费的就是发布与下载页里那份 data/download/<key>.yaml,不引入第二套版本模型。
任意页面做落地页
普通内容页加两行 front matter 即成为落地页:全宽画布,保留顶栏、命令面板与页脚,去掉侧栏与目录。
数据放在与首页平行的目录下,同样按语言分文件:
落地页数据
- data/
- landing/
- pricing/
- en.yaml
- zh.yaml
- pricing/
- landing/
非首页落地页按这个顺序查找数据,找不到则构建失败,不会渲染空页面:
- 页面 front matter 里的
sections; data/landing/<key>/<精确语言>.yaml;- 单文件
data/landing/<key>.yaml里的精确语言条目; - 英文或无语言后缀的记录。
数据量小时可以写在 front matter 里,但 landing: 与 sections: 互斥:
分区条目写法
sections 的每一项可以是一个类型名字符串,也可以是一个 Map:
| 键 | 作用 |
|---|---|
type | 分区类型;省略时用 key 当类型 |
key | 从哪个键取数据,默认与 type 同名;同一种分区用两次时用它区分 |
data | 内联数据,不再到顶层查找键 |
id | 分区的锚点 ID,默认由 key / type 生成 |
enabled: false | 停用这个分区,保留数据 |
partial | 换成站点自己的 partial。属于本地模板约定,不是可移植的 Landing 数据 |
多语言与本地事实
叙事文字优先分语言文件(zh.yaml / en.yaml)。共享的事实记录也可以在字段级回退:<字段>_<精确语言> → <字段>_<主语言> → <字段>,语言标签里的 - 规范化成 _。中文站解析 title_zh_cn、title_zh、title。不接受 camelCase 后缀。
分区里的显示文字是站点数据,不是主题的 i18n 字符串。只有跑马灯暂停、定价状态这类主题自带控件用翻译键。多语言站点的整体配置见多语言。
落地页外壳上的几个可选事实也是本地的,写在 hugo.yml 里,运行时不会去取它们:
页脚不属于首页数据:它读 data/footer/<语言>.yaml(单语言站点用 data/footer.yaml),本站两种语言各一份。data/home/<语言>.yaml 里残留的 footer 键会让构建失败并提示新位置。写法见导航与菜单。
输出形态
| 输出 | 呈现 |
|---|---|
| HTML | 完整的静态分区内容,再按需加载 landing.js 做渐显、计数、复制与主题图片切换 |
| 打印 | 内容保留,跑马灯之类的动态面变成静态网格,控件移除 |
| Markdown | 标题、正文、列表、表格与代码,不带组件 class |
| RSS | 不输出 Landing 分区 |
禁用 JavaScript 后服务端文档仍然完整。跑马灯的副本轨道不进无障碍树,暂停用的是不依赖 JavaScript 的复选框;读者开启减少动态效果偏好时,移动与渐显关闭。
验证
- 构建零告警:
hugo --printPathWarnings --panicOnWarning。类型写错、数据键不存在、landing与sections同时出现都在这一步暴露。 - 打开首页与落地页,逐个分区对照数据文件,每种语言各看一遍。
- 禁用 JavaScript 后刷新:内容仍在,只是没有动效。
- 深浅色各看一遍,确认
image.light/image.dark都给对。 - 部署到子路径时,确认站内链接与图片都带上了前缀。
相关
4 - 导航与菜单
本页覆盖读者在页面之间移动的入口:顶栏菜单、栏目切换器、面包屑、页面操作、上一页 / 下一页与页脚。侧栏树与目录属于布局与页面类型。
导航没有第二套信息架构:顶栏来自 Hugo 的 menus.main,侧栏来自 content/ 的目录结构。主题不读 docs.json、navigation.yaml 一类的并行导航树。
顶栏菜单
顶层入口写在各语言的 menus.main 里:
weight 越小越靠前。pageRef 指向站内页面,url 指向外链;外链自动加 target="_blank" 与 rel="noopener noreferrer",并带一个外链角标。identifier 是配置里引用这个入口的稳定标识(quick_links、sidebar_root_menu 按它匹配),name 按语言翻译,identifier 不翻译。
菜单项也可以挂在页面 front matter 上,适用于「这一页本身就是一个顶层入口」:
顶栏右侧的 GitHub 入口 不是 菜单项,它来自 params.github_project_repo(未设时回落 params.github_repo)。标识为 github 的菜单项会被菜单区跳过,写了也不显示。改变这个入口的目标要改仓库参数,见仓库与页面信息。
下拉菜单
用 Hugo 的 parent 建立父子关系,只支持一级子项:
- 每个条目都是独占一行的一个图标加一个标题,整个面板是一列宽度适中的
纵向列表。子项的
params.description只是配置数据,面板不会渲染它。 - 父级本身是一个普通链接:悬停或键盘聚焦展开面板,点击或回车进入父级页面。没有单独的展开箭头,触屏读者落到父级页面,该页正文同样列出这些链接。
- 键盘:向下箭头展开并聚焦第一项,Esc 关闭并把焦点还给链接,点击面板外部关闭。
- 0.5 的
params.columns参数已退役:设置它会发出构建警告,面板保持单列。 - 再深一层会发出构建警告并降级成静态分组标题,不会 生成三级悬浮菜单。更深的层级放进侧栏。
菜单图标
小于 lg 时菜单项只剩图标,每个顶层入口都应有一个。图标按这个顺序解析:
- 目标页面 front matter 里的
icon; - 菜单项自己的
params.icon; - 按 identifier / 分区名匹配的内置默认值(
docsblogexamplescommunityaboutdownloadgithub等); - 都没有时用
fa-solid fa-link。
图标写成一对 Font Awesome class,主题本地提供免费版字体:
标签菜单
顶层入口指向 taxonomy 页面(/tags/、/categories/)时不需要手工配置子菜单:面板自动渲染「标签 + 数量」的 chip 网格,按数量降序排列。
分类怎么启用见分类体系。
顶栏控件
顶栏高 50px,从左到右是:品牌(Logo 或字标)、菜单区、搜索、版本、语言、主题、GitHub。首页和 Landing 页面最右侧还固定保留抽屉菜单按钮。顶栏在所有布局上渲染;文档、博客和分类页使用相同控件,但没有这个 Landing 抽屉。
顶栏分为桌面完整形态与紧凑图标形态:
| 视口 | 状态 |
|---|---|
lg 及以上 | 完整:品牌、带文字的菜单项、各工具控件;首页/Landing 最后是抽屉按钮 |
小于 lg | 紧凑:品牌保留,其余全部右对齐成图标 |
小于 md | 顶栏右侧只留搜索与抽屉按钮;版本、语言、主题与快捷键帮助仍在页脚最底层栏中 |
各控件的开关不在这里:搜索图标要 params.offline_search(见全文检索),版本菜单要 params.versions(见多版本),语言菜单在配置了两种及以上语言时自动出现(见多语言),主题控件要 params.ui.dark_mode(见品牌外观)。
自动隐藏
开启后顶栏离开正常流、停在视口上方,指针进入原位置上方 60% 的中间区域(或键盘焦点进入)才滑出,并且覆盖在正文之上,不把正文顶下去。左右各 64px 不属于唤醒区,避免盖住折叠后的侧栏与大纲恢复按钮。
小于 768px、粗指针或纯触屏时自动停用,顶栏始终可见。页面 front matter 顶层的 navbar_autohide 或分区 cascade 可以逐段覆盖。
关闭顶栏
也可以只关闭某一页或某一段:
关闭后主题补回原本由顶栏承担的界面:移动端子导航、侧栏顶部的品牌与搜索行、大纲轨道上的工具按钮。这个开关适用于必须独占视口的页面,不作为常规排版偏好。本站的文档栏目使用它:文档页依靠侧栏导航,顶栏是多余的一行。
栏目切换器
侧栏顶部那一行是栏目切换器,决定当前显示哪棵树。入口集合按顺序去重构造:所有顶级栏目 → 全站所有 sidebar_root_for: self 的分区 → 当前解析出的根。
让一棵大子树自成一个根(带版本的 API 参考、独立手册),在它的 _index.md 里:
self 让这个分区索引与它的后代都用这棵新树;children 把索引留在父树里,只约束后代。让某个顶层分区不出现在切换器里,在它的 front matter 里设 sidebar_root_menu: false。
只有一个入口时切换器退化成一个无边框链接,两个及以上才是下拉菜单。切换器下面的树仍然把栏目首页本身作为第一个链接:切换器选一棵树,根链接选一篇文档。
面包屑与页面操作
普通内容页标题上方是面包屑行,这一行右端同时承载页面操作。顶层分区省略只有一级、与标题重复的面包屑,操作按钮的位置不变。
面包屑标签取本地化的 linkTitle,层级与侧栏一致。
页面操作菜单
页面操作是标题行末尾的拆分按钮:左半边一键复制本页 Markdown(成功后变成绿色对勾),右侧箭头展开完整菜单。菜单分两组,上半组是取走内容,下半组是改动与产出:
| 操作 | 出现条件 |
|---|---|
| 复制 Markdown 文本 | 站点开了 markdown 输出格式 |
| 在 ChatGPT 中打开 | page_context_menu.assistant_links: true |
| 在 Claude 中打开 | 同上 |
| 查看 Markdown 源码 | markdown 输出格式 |
| 查看编辑历史 | params.github_repo 能解析出源文件路径 |
| 编辑本页 | params.github_repo |
| 新建子页面 | params.github_repo |
| 提交文档 issue | params.github_repo |
| 提交项目 issue | params.github_project_repo |
| 打印整个分区 | 分区开了 print 输出格式 |
助手入口默认关闭:读者点击时,完整的当前 URL(含 query 与 fragment)会随本地化提示词发给第三方,页面正文不上传。开启前确认 URL 里没有敏感信息,并在隐私说明里披露这个边界。页面可以用布尔型 front matter assistant_links 收紧站点策略,不能反过来替站点开启。
自定义外部操作排在菜单最后,url 支持三个已 URL 编码的占位符:
可用占位符:{url}(页面完整地址)、{title}(页面标题)、{markdown_url}(Markdown 版地址)。
在博客根分区及其一级子分区上,左半边变成 RSS 订阅链接,菜单里仍保留「复制 Markdown 文本」。没有 Markdown 输出的页面去掉左半边,箭头变成带文字的「操作」按钮。
这些操作同时是命令面板里的条目。
翻页器
正文末尾的上一页 / 下一页是两个文本链接,顺序与侧栏可见树一致:根页 → 第一篇 → 直到最后一篇。根页没有上一页,末页没有下一页。站点提供 data/docs_nav.json 时,这棵显式树同时决定翻页顺序,以及该文件声明过的 docs / book 栏目的栏目索引顺序——侧栏、翻页器与索引不会再把同一批子页排出三种顺序。文件没有声明的栏目,以及没有这个文件的站点,仍然沿内容树走。见布局与页面类型。
pager_types 只接受 docs、book、blog 三个值,其它取值告警并丢弃。单页退出用 front matter:
同一份顺序也写进 <head>:有上一页 / 下一页时输出 <link rel="prev"> 与 <link rel="next">,供浏览器与爬虫识别阅读序列。
翻页只在 HTML 输出中生效。打印、Markdown 与 RSS 既没有翻页链接,也没有这两个 rel 关系。
翻页器是页尾四件套的第三件(反馈 → 页面信息 → 翻页 → 评论),顺序固定,四者独立开关。
页脚
页脚形态由 params.ui.footer_style 决定(fat / slim / none,见品牌外观)。fat 的多列链接网格读 data/footer/<语言>.yaml。它不是菜单,主题没有 menus.footer:
brand.name与brand.logo不写时回落到站点自己的品牌名、Logo 与字标;tagline与slogan渲染 Markdown。- 站内
url相对当前语言根解析;external: true在新标签页打开并带rel="noopener noreferrer"。 - 网格列数等于数据里的列数。
- 单语言站可以使用
data/footer.yaml。 - 配了
fat但没有数据时自动降级成slim,可以先开启再补内容。
fat 页脚的版权行右端有一个折叠箭头,收起或恢复它上方的链接栅格。默认展开,读者的选择存在 localStorage 的 td-footer-collapsed 键里,跨页面保留;slim 与 none 没有这个按钮,它也与专注模式无关。
只要页脚有渲染,最底层栏右侧就固定保留同一组图标:版本、语言、主题、快捷键帮助。各菜单向上展开;版本按钮只显示分支图标,完整版本名仍保留在选项中。fat 页脚的折叠箭头排在这四项之后。侧栏不再重复这组控件,footer_style: none 则连同页脚一起移除底栏。
版权行与中间那句说明由参数控制,见配置总览。
验证
改完导航要检查这几处:
- 构建没有
Navbar menu … supports one interactive child level警告;出现它说明菜单嵌了三层; - 桌面端:父级菜单点击进入父级页面,悬停展开面板,Esc 关闭面板;
- 窗口缩到
lg以下:每个顶层入口仍有图标,没有图标的项在这个宽度下是空白; - 缩到
md以下:首页与 Landing 顶栏右侧只剩搜索和抽屉按钮;版本、语言、主题与快捷键帮助固定在 footer 最底层栏; - 侧栏顶部的切换器列出所有顶级栏目,当前项有选中标记;
- 任意文档页按 E / Q 翻页,顺序与侧栏一致,页面源码里有对应的
rel="prev"/rel="next"; - 打开页面操作菜单,确认该出现的项都在,不该出现的没有(例如未配置
github_project_repo时的「提交项目 issue」)。
相关
5 - 布局与页面类型
本页覆盖页面骨架:有没有侧栏、侧栏多宽、目录收几级、栏目首页是列表还是卡片。内容放在哪个目录见组织内容,这里只讲外壳。
规则是 外壳看 type,不看路径。文档可以放在 content/ 下的任何位置,只要给它 type: docs。
外壳类型
params.ui.shell_types 列出哪些 type 使用带侧栏的阅读外壳:
| type | 外壳 |
|---|---|
docs | 文档外壳:左侧栏(栏目切换器 + 目录树)+ 正文 + 右栏大纲 |
book | 文档外壳,另加编号目标、reading_width 阅读行宽与草稿横幅 |
blog | 文档外壳,侧栏默认展开,标题行左半边是 RSS |
swagger | 文档外壳,正文交给 Swagger UI 或 Redoc,见 API 文档 |
| 其它 type | 普通页面:顶栏 + 单栏正文 + 页脚,没有侧栏 |
分类页与标签页(taxonomy / term)不在这张表里,但也走同一套外壳。
给一棵子树指定 type 用 cascade,这是把文档放在任意路径的做法:
栏目根只是导航起点
这两个键 不决定外壳,只告诉主题文档树与博客树的根在哪,用于解析侧栏根、快捷入口与默认图标。上面 content/handbook/ 的例子照样有文档外壳,docs_section 保持 docs 不影响它。
需要让 docs 页的侧栏根变成站点首页,而不是文档栏目时:
取值只有这两个,其它值构建失败。
文档挂在站点根
以文档为主的站点可以把 docs 分区发布到 URL 根路径,源码仍然放在 content/docs/ 下。这需要三段配置一起给出。
第一段用 Hugo 原生的 permalinks 去掉 URL 里的 docs/ 段:
第二段让物理站点根索引仍可作为链接目标,但不再争抢同一个输出路径。每种语言的站点根索引(content/_index.md、content/_index.zh.md)都要写:
第三段把侧栏根声明为站点首页,让侧栏与翻页共用同一棵树:
docs_sidebar_root: home 之后,站点首页的所有顶层分区都会进入这棵树。博客、社区、下载这类不属于阅读序列的概览分区,在自己的 _index.md 里设 toc_root: true 退出,它们既不出现在树里,也不成为翻页目标:
文档此时与博客、社区等分区共享 URL 根路径。构建加 --printPathWarnings,发布前解决所有重复目标。
落地页
任意页面加 layout: landing 即使用落地页布局:顶栏 + 分区拼装的正文 + 页脚,没有侧栏。数据写法见首页与落地页。
landing_search: false 会把搜索入口从落地页外壳里去掉,其它页面不受影响。
侧栏
侧栏树来自 content/ 的目录结构,按 weight 排序,有 linkTitle 时用它作为标签。可调的是密度与尺寸:
sidebar_menu_compact只展开当前分支及邻近条目;设为false时整棵树全展开。sidebar_menu_foldable允许读者手动展开 / 折叠分区。博客栏目默认展开;某个分区要默认收起,在它的_index.md里写sidebar_expanded: false。sidebar_expand_levels是默认展开的层级数。sidebar_menu_truncate是单个分区最多渲染的条目数,避免上千页的目录把 HTML 撑到不可用。sidebar_width_min/sidebar_width_max是桌面端拖拽调宽的上下限(像素)。读者调整后的宽度存在浏览器本地,双击分隔条恢复默认。sidebar_item_overflow默认ellipsis(长标题省略),中文长标题多的站点可以改wrap换行。
折叠状态、宽度与滚动位置保存在读者本地,按语言隔离。小于 md 时侧栏变成带遮罩的抽屉。
单页去掉侧栏用 front matter:
显式导航树 data/docs_nav.json
侧栏树默认从 content/ 推导。站点也可以给出一份显式导航清单,三个条件同时成立时主题改用它渲染:
- 站点存在
data/docs_nav.json且其中有sections键; - 页面的 type 是
docs或book; - 解析出的侧栏根不是站点首页。
文件是一棵嵌套的节点树。每个节点用 page 指向内容路径,url 是它的链接,children 是子节点;active_path_by_url 记录每个 URL 对应的祖先链,供当前项高亮使用:
URL 在比较前去掉语言前缀,一份文件服务所有语言。
这棵树同时决定翻页顺序,侧栏与上一页 / 下一页不会出现两种排序。sections 为空数组时构建失败(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 会出现在侧栏。叶子页全部带图标会降低可读性,用密度策略控制:
| 取值 | 效果 |
|---|---|
all | 每个有图标的条目都显示(未设置时的兼容默认值) |
groups | 只有根节点和有子页的节点显示图标 |
none | 侧栏不显示条目图标 |
非法取值只发警告并回落到 all,不让构建失败。本站使用 groups。
在侧栏里展开标题
Book 页可以在侧栏当前行下展开 h2–h4 分支,便于在长章节内跳转:
整数指定展开到第几级(2–4),true 等于 2(只展开 h2),false 关闭。取值超出范围构建失败。只对 type: book 的页面生效,且只在侧栏当前行下展开。
目录 TOC
右栏大纲由 Hugo 从 Markdown 标题生成,收录层级是 Hugo 原生配置:
主题只管跟踪行为:
默认 关闭 滚动跟踪。设为 true 开启后,大纲绘制连续轨道、高亮当前区段并标出位置。读者可以整体折叠右栏,状态存在本地。小于 xl 时右栏隐藏,大纲内容移进侧栏抽屉。
单页隐藏大纲用 front matter notoc: true。
只有进入 Hugo 目录的标题才出现在大纲里:Markdown 型 shortcode({{%/* … */%}})输出的标题会进,普通 shortcode({{</* … */>}})输出的通常不会。结构性标题应留在 Markdown 里。
栏目首页样式
带 _index.md 的分区会自动列出子页。两种样式:
list(默认):每个子页一个标题 + 描述段落;cards:网格卡片,读子页的title(或linkTitle)、description与icon。
可以按分区覆盖,非法取值构建失败:
相关的页面级开关:no_list: true 不列子页;simple_list: true 只输出一个无描述的项目符号列表;子页设 hide_summary: true 把自己从列表里去掉。不要手写子页清单:手写的清单会与侧栏失同步。
页宽
normal 是常规阅读宽度,wide 放宽正文栏,full 铺满视口。可以逐页或按分区覆盖;宽表格、大图与 API 参考页常用 wide:
Book 页另有一个 reading_width(slim / normal / wide),改的是正文本身的阅读行宽,不动外壳。两个键取值非法都让构建失败。
顶栏与页脚开关
顶栏与页脚属于逐页的布局决定,写在 front matter 顶层(不在 ui 下),可以用分区 cascade 一次设定:
验证
- 构建输出
Total in …,没有 ERROR / WARN; - 新建的
type: docs页面有左侧栏。没有则检查 cascade 是否覆盖到该页,以及shell_types是否包含这个 type; - 拖动侧栏分隔条,刷新后宽度保留,双击恢复默认;
- 窗口缩到
md以下时侧栏变成抽屉且可关闭,缩到xl以下时大纲移进抽屉; - 栏目首页的卡片数量与侧栏子页数量一致;
page_width: wide的页面比相邻页面宽;- 文档挂在站点根时,
hugo --printPathWarnings没有重复输出路径的告警。
相关
6 - 全文检索
OINK 的搜索是本地搜索:Hugo 在构建时给每种语言生成一份 JSON 索引,读者的浏览器下载它,在本地完成检索。不需要爬虫、账号、CDN,也不需要联网。主题默认不启用,一行配置即可开启。
搜索的入口是命令面板,打开方式与面板的其余内容见命令面板。
打开本地搜索
这一个键决定索引、Lunr 运行时与搜索对话框是否进入页面。三个条件同时成立时页面才带上它们:
params.offline_search为真;- 页面是首页,或者用了外壳布局(
docs/book/blog/swagger,见布局与页面类型),或者是开着params.ui.landing_search的落地页; - 当前输出不是打印。
任何一条不成立,构建就不往这个页面里放对话框、索引引用与 Lunr。这些资源不是被隐藏,而是不生成。
hugo server 下索引默认 也会生成,预览行为与线上一致。站点极大、每次改动都重建全站索引明显拖慢预览时,把它关掉:
控制索引体积
offline_search_index 决定每个页面往索引里写多少内容,因此同时决定两件事:读者能否搜到正文里的词,以及第一次搜索要下载多大的文件。
| 取值 | 索引进去的内容 | 什么时候用 |
|---|---|---|
title | 标题、标签、分类、search_keywords | 只靠标题定位的超大站 |
heading | 上面这些 + 页内各级标题 | 标题写得足够具体时 |
summary | 上面这些 + 描述与摘要 | 千页级站点;本站使用这一档 |
content | 上面这些 + 全文纯文本 | 默认值,几百页以内适用 |
其它取值构建失败,报 invalid params.offline_search_index。
offline_search_summary_length 是结果行里摘要的截断长度(默认 70),offline_search_max_results 是结果条数上限(默认 10)。这几个键的完整定义在配置总览。
读者搜第一个词之前要先下载整份索引。超过这个量级就把 offline_search_index 从 content 降到 summary。
调整排序
页面在 front matter 里影响自己的排名:
search_keywords 是额外的匹配词,可以写一个字符串,也可以写数组。它是这两个键里更有用的一个:读者搜 pg 或 GUC 即可命中标题只写着「PostgreSQL 参数」的页面。检索时关键词的权重仅次于标题,高于正文。
search_boost 是最终得分的正数乘子,默认 1.0,作用在文本匹配得分之上。1.5 不会把页面固定在第一位,只让它在本来就匹配的结果里前移。零、负数与非数字会告警并按 1.0 处理。
整节的默认值用 cascade 一次设定:
页面自己写的值覆盖继承来的值。本站 docs/ 下的页面按这种方式使用 search_keywords:每页列出中文说法、英文原词与配置键名。
把页面挡在索引外
search_exclude 是唯一写法,exclude_search 与 excludeSearch 会让构建失败并提示改名。正文为空的页面不进索引。
不该公开的内容不要放进站点,也不要用 search_exclude 保护它。
中文与 CJK
Lunr 不能可靠地给中文分词。面板在查询里检测到 CJK 字符时整条切到子串匹配:逐篇比对标题、关键词、页内标题、描述、正文,命中哪一层给哪一层的分,最后同样乘上 search_boost。两条路径的排序规则一致。
三点需要知道:
- 中文查询是 子串 匹配。搜「主从复制」只命中连续出现这四个字的位置,搜「复制主从」没有结果。
search_keywords对中文站的收益因此最大:把读者可能使用的同义说法、英文原词、缩写都写进去。- 输入法组字期间面板不重算结果,文字上屏后才检索,中文输入不会逐字母刷新结果。
中文搜不到内容时,先确认中文页面进了中文那份索引(见下面的验证),再考虑分词问题。
可选:在线搜索
本地搜索之外,主题保留了两个在线搜索集成,默认关闭。同一时间只启用一种:配置了多个入口时构建告警 You have more than one site-search option configured。
启用在线搜索意味着接受对应服务的抓取方式、可用性与隐私边界,这些应写进站点的隐私说明。
Algolia DocSearch
三个值必须都显式写出,缺一个构建中断:OINK 不会回退到其它项目的公共索引。DocSearch 的 JS 与 CSS 随主题内置,不从 CDN 加载,但每次检索请求都发到 Algolia。需要真实的密钥与索引才能工作,此处不渲染。
Google 可编程搜索
还需要给结果准备一个落地页:
搜索框把查询提交到 <baseURL>/search/?q=…,结果由 Google 的脚本在那个页面上渲染,需要访问 cse.google.com。同样需要外部服务,此处不渲染。
验证
构建,确认每种语言各生成了一份索引:
开发构建下文件名是
offline-search-index.zh.json,生产构建加指纹,形如offline-search-index.zh.7ab….json。一种语言一个文件,缺少某个文件说明那种语言的页面没进索引。查看索引内容,这是排查「中文搜不到」的第一步:
条目数应接近中文页面数,
keywords与boost字段能看到写进 front matter 的值。打开站点,按 /,分别用一个英文词与一个中文词各搜一次。结果按内容根分组,每组的名字是面包屑的第一段。
子路径部署(站点挂在
https://example.com/docs/这类路径下)时,打开浏览器开发者工具的网络面板,确认索引请求带上了子路径。索引请求打到域名根目录并返回 404、页面其余部分正常,是「搜索没结果」最常见的原因。
相关
7 - 命令面板
命令面板是站点唯一的模态入口:搜索页面、复制本页 Markdown、切换语言、切换版本、跳转到站点自定义链接,都在这一个对话框里完成。它随本地搜索一起装配:params.offline_search 关闭时,面板连同索引与 Lunr 都不进入页面,见全文检索。
打开面板
| 打开方式 | 打开成什么 |
|---|---|
| 点顶栏或侧栏的搜索框 | 完整搜索态 |
| ⌘ / Ctrl + K | 完整搜索态;再按一次关闭 |
| / | 完整搜索态 |
| 反斜杠键 | 纯命令态(等于预填了 >) |
| f / c | 同上两者,由键盘导航提供 |
在框里输入 > 开头的查询 | 纯命令态 |
/、反斜杠、f、c 都是裸单键,会给输入让行:焦点位于 input、textarea、select 或 contenteditable 中,以及正在用输入法组字时,按键作为普通字符输入。带修饰键的 ⌘/Ctrl + K 没有这个限制,在输入框里也能打开面板。
面板内:↑ ↓ 选择,Enter 执行,Esc 关闭并把焦点交还给打开它的控件。
面板内容
不输入任何内容时,面板按固定顺序列出四组:
| 分组 | 内容 | 谁决定 |
|---|---|---|
| 快速链接 | 顶栏一级菜单里选出的几个入口 | params.ui.quick_links |
| 页面操作 | 复制 Markdown、查看 Markdown 源码、编辑本页、查看修改历史、新建子页、提 issue、打印整节 | 仓库配置与本页是否有 Markdown 输出 |
| 偏好设置 | 切换版本 → 切换语言 → 切换主题 | 站点是否配了多版本、多语言、深浅色菜单 |
| 命令 | 打开 GitHub 仓库,之后是站点自定义命令 | params.github_project_repo(缺省回退到 github_repo)与 ui.command_palette.commands |
偏好设置三项的顺序与顶栏控件一致(版本、语言、主题),面板与顶栏是同一个次序。选中「切换语言」这类项后,面板不立即跳转,而是就地展开可选项,再选一次。
输入文字时,先是页面结果,按内容根分组(分组名是面包屑的第一段,组间顺序跟随顶栏一级菜单的顺序),命令与动作合并成一组排在最后。
以 > 开头时只列命令与动作,不查页面。不确定某个功能在哪个菜单里时用它定位。
不可用的项在能说明原因时仍然列出。站点没有配置仓库地址,「编辑本页」会带着「不可用」的说明留在列表里,而不是消失。
快速链接
快速链接从 Hugo 主菜单里按 identifier 选取,不另写一份清单:
值是 menus.main 里条目的 identifier。不写这个键时默认取文档栏目与博客栏目(params.ui.docs_section 和 blog_section)。菜单本身怎么配见导航与菜单。
自定义命令
站点自己的命令写在 params.ui.command_palette.commands 下,排在内建命令之后,顺序即书写顺序:
上面是本站在用的那一条。字段共七个,写其它键构建失败:
id必填,小写字母开头,只能用小写字母、数字、下划线和短横线;不能与内建动作 ID 重名。title显示在面板里;description是它下面那行小字;icon是一对 Font Awesome class。keywords是数组,参与匹配但不显示,用于收纳读者可能输入的检索词。url与action有且只能有一个。url只接受http/https的完整地址、站内路径,或#开头的页内锚点;带主机名的地址在新标签打开。action引用一个内建动作 ID。
action: 给内建动作起别名内建动作已经在面板里,再包一层会让同一个功能以两个名字出现两次。
多语言站点把命令写在 languages.<lang>.params.ui.command_palette.commands 下,标题与关键词才能本地化。顺序由默认语言的那份清单决定:其它语言里同 id 的条目只覆盖字段,新增的 id 追加在末尾。各语言的命令顺序因此一致,读者换语言时命令不会换位置。
配置只能给出链接或引用内建动作,不能注入 JavaScript 回调:面板读取的是一份纯数据清单。
页面动作
面板里的「页面操作」与文档标题旁的拆分按钮是同一套实现:同一份动作描述、同一段 URL 生成逻辑、同一个执行器。按钮左半边复制本页 Markdown,右侧箭头展开全部动作。
整组关闭,或只在某些页面关闭:
enable: false 只移除标题旁的按钮,面板里的对应项保留,面板本身就是命令入口。单页用 front matter 的 page_context_menu: false 覆盖。
assistant_links 默认关闭,原因是读者点击时 当前页面的完整 URL(含查询串与锚点)会被发送到第三方,页面正文不会上传。这是站点级的选择,页面 front matter 里的 assistant_links 只能把它收紧,不能替站点打开。
links 是额外的外部动作,只出现在标题旁的菜单里,不进面板:
{url}、{title}、{markdown_url} 三个占位符会被替换成当前页面的值。
「编辑本页」「查看修改历史」「提 issue」这些动作是否可用,取决于仓库相关的配置,见仓库与页面信息;「复制 Markdown」「查看 Markdown 源码」需要页面开了 markdown 输出,见 Agent 支持。
与全文检索的关系
同一个对话框,两条独立的数据来源:
- 页面结果 来自本地搜索索引。索引未生成或下载失败时,面板照常打开、照常执行命令,页面那部分显示「页面索引暂不可用,操作仍可使用」。
- 命令与动作 来自页面里内嵌的一段 JSON 清单,不需要网络。
打印态不装配面板,打印输出里没有它;关闭 offline_search 后同样没有面板,此时 f 与 c 静默,不影响正常输入。
验证
构建后确认命令清单进了页面:
没有这一行说明本地搜索没启用,或者这个页面不在外壳布局里。
打开站点按下 ⌘/Ctrl + K,什么都不输入:应该看到快速链接、页面操作、偏好设置、命令四组,顺序如上。
输入
>:只剩命令与动作。新加的命令应该排在「打开 GitHub 仓库」之后。切到另一种语言重复第 3 步,确认命令的标题变了、顺序没变。
打印预览(⌘/Ctrl + P)里不应该出现任何面板痕迹。
相关
8 - 键盘导航
OINK 的交互式页面自带一套单键快捷键:WASD 在侧栏树中移动,J K 在标题间跳转,Q E 翻页,另有几个单键切换主题、语言与命令面板。默认开启,所有绑定都给输入让行,可以按站点或按页面关闭。
键盘导航不维护第二套状态:树的展开折叠复用侧栏原有的箭头按钮,逐节跳转读取右栏目录,切换语言与主题复用命令面板的同一批动作。键盘操作的顺序与鼠标操作的顺序因此一致。
侧栏
| 按键 | 行为 |
|---|---|
| W S ↑ ↓ | 焦点移到上一个 / 下一个可见项 |
| A D ← → | 折叠 / 展开分组;叶子节点上 A 跳到父级,D 无动作 |
| Enter Space G | 打开焦点所在的页面 |
| Esc | 退出树,焦点回到正文 |
四个字母键不需要先进入树:焦点还在正文时按 S,以当前页在侧栏里的那一项为起点下移一格并落焦。焦点行整行加深底色,比「当前页」的底色深一档,用于区分当前页与焦点位置。
窄屏侧栏收进抽屉、或桌面侧栏被折叠时,第一次按这四个键先展开侧栏。页面没有侧栏树时静默。
方向键 只在焦点已经进入侧栏后 才作用于树,正文里保持浏览器原生滚动。RTL 语言下 ← → 随阅读方向对调,A D 恒等于「折叠 / 展开」。
阅读
| 按键 | 行为 |
|---|---|
| J K | 沿页面目录跳到下一节 / 上一节 |
| N | 首页专用:跳到下一个顶层分区(首页 J 的助记别名) |
| Q E | 上一篇 / 下一篇 |
| H | 专注阅读模式:隐藏 / 恢复导航外壳 |
J K 的目标序列与右栏目录同源,落点与点击目录一致。跳转是固定 100 ms 的缓动滑行,与距离无关;连续按键不必等上一段动画结束。已经读到某一节内部一段距离后,K 先回到本节起点,再按一次才跳到上一节。页面没有标题时退化为一小段滑动。
Q E 按 侧栏树的可视顺序 翻页,不按日期。栏目入口页本身也是树里的一项,博客的栏目边界因此表现为「上一专栏最后一篇 → 下一专栏入口页 → 下一专栏第一篇」。折叠起来的分支不在这个顺序里:翻页顺序与焦点移动顺序是同一个。页面没有侧栏树时回退到页尾翻页器,没有翻页器时用 <head> 里的 rel=prev/next。
H 在首页只隐藏顶栏与页脚,在文档页同时隐藏左右栏与浮动按钮。状态记录在当前标签页的会话中,首帧之前恢复,用 Q E 连续翻页不丢状态、不闪烁。外壳隐藏时 WASD 不会把焦点送入不可见的侧栏。
外观、语言与路由
| 按键 | 行为 |
|---|---|
| L Y | 循环切换语言(两个键等价) |
| T | 亮 / 暗模式切换 |
| R | 在首页与顶栏的同源一级入口之间循环 |
这三个键在任何交互式页面上都有效,不限于文档外壳。单语言站点的 L、关闭深浅色菜单后的 T、只有一个一级入口时的 R 都静默。R 只在同源的一级菜单项之间循环,外链与顶栏上的工具控件不参与。
搜索与命令
| 按键 | 行为 |
|---|---|
| F 或 / | 打开命令面板的完整搜索态 |
| C 或反斜杠键 | 打开命令面板的纯命令态 |
| ⌘ 加 K 或 Ctrl 加 K | 打开面板;再按一次关闭 |
/ 和反斜杠属于搜索功能本身,关闭键盘导航后仍然可用;F C 是键盘导航提供的别名,指向同一个面板实例。部分非美式键盘布局上反斜杠不易按到,在面板里输入 > 前缀同样进入纯命令态。面板里有什么见命令面板。
保留不占用的键
? 保留不绑定。速查卡挂在页脚最底层栏的问号按钮上,鼠标悬停、键盘聚焦或触摸都能打开,列出当前页面实际可用的按键:单语言站点看不到切换语言那一行。
G G、Shift 加 G 和数字键同样保留,可能用作将来的跳转序列。
快捷键的让行规则
所有绑定都是裸单键,凡是可能和输入或弹层冲突的场合一律禁用:
- 焦点在 input、textarea、select 或
contenteditable区域里; - 正在用输入法组字(中文站的硬约束);
- 按住修饰键时:⌘ 加 C 仍是复制,Shift 加 ↓ 仍归浏览器;
- 命令面板或别的对话框开着,键盘归那个弹层。
评论区在 iframe 中,键事件不冒泡到页面,无需额外隔离。
焦点顺序与无障碍
- 跳转链接:进入页面后第一次按 Tab 出现的就是「跳转到主要内容」,一步跳过顶栏和侧栏。
- 真实焦点:树内导航移动的是真正的 DOM 焦点,不是虚拟光标。屏幕阅读器因此读出链接名与「当前页」标记,Enter 是链接的原生行为,Tab 顺序没有被改写。
- 高对比度:焦点行的底色在
forced-colors模式下失效,退化为系统高亮色描边。 - 减弱动效:
prefers-reduced-motion打开时,逐节跳转与翻页滚动改为瞬时定位,不做滑行。 - 速查卡里的键帽与正文里的按键组件是同一套样式。
关闭
全站关闭:
单页关闭(交互密集的演示页常常需要),或者用 cascade 按整节关闭:
这个键只接受布尔值,写成 "false" 或其它值时构建失败,报 params.ui.keyboard_nav must be a boolean。完整定义见配置总览。
关闭后运行时不进入 JavaScript bundle,而不是加载后再判断。/、反斜杠和 ⌘ 加 K 属于搜索,仍然可用;页脚折叠链接栅格的箭头不受影响。
验证
构建后确认速查卡按钮在页面里:
关闭键盘导航且没开本地搜索时,这个按钮整个不生成。
打开一篇文档,光标停在正文里连按 S:侧栏里应该从当前页那一项开始逐项下移,正文不动。
按 E 若干次,核对翻页顺序与侧栏从上到下的顺序一致;折叠一个分组再翻,被折叠的页面应该被跳过。
点进搜索框,按 J:页面 不应该 滚动,字符正常输入。使用中文输入法输入时同理。
系统里打开「减弱动态效果」,再按 J:应该瞬间定位,没有滑行。
相关
9 - 多语言
OINK 使用 Hugo 的多语言模型,不额外引入目录约定:配置一个 languages 块,译文与原文并排放在同一个目录里,用文件名后缀区分。以下内容覆盖单语言站点扩展为双语站点需要改动的位置,以及双语站点的两处易错点:资源归属与标题锚点。
启用第二种语言
上面是本站在用的配置。四个字段的作用:
label是语言选择器里显示的名字,用该语言自己的文字书写:写简体中文,不是Chinese。locale是标准语言标签,会进<html lang>、hreflang备用链接和 Open Graph 元数据。weight同时决定语言排序和选择器的轮换顺序,小的在前。params是语言级覆盖:这里没写的键继承全局同名值。日期格式通常需要按语言各写一遍。
默认语言不带路径前缀(英文在 /docs/…),其它语言各占一个前缀(中文在 /zh/docs/…)。默认语言也需要前缀时加 defaultContentLanguageInSubdir: true。这会改变全站 URL,已上线的站点要同时配好重定向。
文件命名与资源
译文与原文并排放置,用后缀区分,Hugo 靠相同的基础文件名把它们认成同一页的两个语言版本:
- content/docs/
- install.md英文
- install.zh.md中文
- _index.md
- _index.zh.md
页面包同理:index.md 与 index.zh.md 放在同一个目录里。
页面包里的资源遵循一条规则:文件名不带语言后缀的资源由所有语言共享,带语言后缀的资源只属于那种语言。
- content/docs/install/
- index.md英文页
- index.zh.md中文页
- topology.webp两种语言都能用
- screenshot.zh.webp只有中文页能用
正文里引用带后缀的资源时 写不带后缀的名字:,Hugo 会按当前语言解析。
这条规则有一个推论:页面包里只有 index.zh.md、没有英文对等页时,不带后缀的资源不会分给中文页,它们归属默认语言,而默认语言在这个包里没有页面。此时所有资源都必须带 .zh. 后缀,本站 docs/ 下的中文页面包即是如此。
哪些内容需要翻译:
- 翻译:
title、description、摘要、菜单标签、标签名、图片 alt、提示块正文、shortcode 里面向读者的参数。 - 保持一致:日期、
weight、别名,以及任何影响路由的元数据。两边不一致会导致侧栏顺序在两种语言下不同。 - 不翻译:命令、配置键、文件名、URL、版本号、产品名、shortcode 名。
按语言分开的配置
三处内容不在 content/ 里,需要各语言各写一份。
菜单 写在各自语言下:
identifier 两种语言必须一致:命令面板的快速链接与搜索结果分组顺序都按它匹配。菜单的完整写法见导航与菜单。
首页数据 按语言取文件:data/home/en.yaml、data/home/zh.yaml。当前语言没有对应文件时回退到 en.yaml;单语言站点用一个 data/home.yaml 即可。见首页与落地页。
界面文案:主题自带 32 个 locale 的界面字符串。英文、简体中文(zh 与 zh-cn)和繁体中文(zh-tw)经过审校,其余语言保留继承自 Docsy 的翻译,OINK 新增的标签用英文兜底。要改某一条,在站点自己的 i18n/ 下建同名文件,只写要覆盖的键:
缺译回退与语言选择器
语言选择器的图标本身是一个链接:点击它按 weight 顺序切到下一种语言(在末尾回到第一种),悬停或键盘聚焦才展开列出全部语言的菜单,触摸屏上菜单不展开,点按即切换。双语站点因此一次点击即可来回切换。
菜单始终列出全部配置的语言,不论当前页有没有译文:
- 目标语言有译文 → 跳到那一页;
- 目标语言没有译文 → 跳到那种语言的 首页。
回退到首页优于把读者送进 404。代价是读者不一定察觉自己被送到了首页,双语站点应当把「每个页面都有对等译文」作为约束来检查,而不是依赖回退。
中文页面不存在时,中文站里就没有这一页:侧栏、搜索索引、翻页顺序都不包含它。
搜索索引也按语言分开:读者在中文页面搜索只命中中文内容。中文查询采用 CJK 子串匹配,细节见全文检索。
标题锚点要对齐
Hugo 从标题文本生成 ID,中文标题生成中文 ID:/docs/install/#prerequisites 与 /zh/docs/install/#前置条件 指向同一个位置,却是两个互不相通的锚点,跨语言的深链、目录与页内跳转都会失效。
做法是在译文标题里显式写出原文的 ID:
两条纪律:
- ID 从 英文页渲染出来的 HTML 里取,不要凭标题文本推断。标题里含行内代码、徽章或 shortcode 时,生成的 ID 与标题文本不一致。
- 中英对应页面的标题数量、顺序、ID 必须一致。确实需要在中文里加一节时,给它一个独立、稳定、不与英文冲突的 ID。
本站用一个脚本把这条约束变成 CI 检查,比对的是渲染后的 HTML 而不是源码:
新页面从建立时就写显式英文 {#id},成本低于事后回补。
从右向左的语言
在语言下声明书写方向:
<html dir> 随之改变,主题额外加载 Bootstrap 的 RTL 样式表。主题自身的 CSS 全部使用逻辑属性(margin-inline-start 而不是 margin-left),镜像布局自动完成。站点自己写的 CSS 同样要用逻辑属性,否则 RTL 下会错位。
验证
构建,确认两种语言的产物和索引都在:
检查
hreflang:每个页面的<head>里,每种语言各一条rel="alternate",外加一条指向自己的rel="canonical"。在有译文的页面上展开语言选择器并选择另一种语言,确认停在同一篇文档;在没有译文的页面上重复一次,确认落到目标语言的首页而不是 404。
两种语言各搜一次同一个概念,确认都有结果。
双语站点把标题对齐检查接进 CI,见上一节的脚本。
相关
10 - 多版本
产品有多个受支持版本时,文档通常也要分版本。主题提供两项功能:顶栏的版本切换菜单,与旧版本站点上的归档提示横幅。部署布局由站点决定:主题不做跨版本的单次构建,每个版本是一次独立的 Hugo 构建。
版本切换菜单
在 params.versions 里列出要出现在菜单里的版本。这个列表非空时,顶栏工具区出现一个分支图标的菜单,页脚最底层栏出现同样内容的纯图标向上菜单。
菜单项默认显示 version 的值,写了 name 就显示 name。当前项标成选中态,判定方式是条目的 version 等于 params.version,或者条目的 url 等于站点的 baseURL,两者满足其一即可。
没写 url 的条目显示为不可点击的灰项,可用作分节标题;name: '---' 是一条分隔线(分隔线上写 url 会告警)。name 支持行内 Markdown:
同一份列表也是命令面板里「切换版本」的数据来源,菜单与面板不会不一致。
逐页跳转的取舍
version_menu_pagelinks: true 会把当前页面的路径拼到目标版本的 URL 后面,读者切换版本时 停在同一篇文档。
代价是目标版本不一定有这个页面:文档结构在版本间会演进,旧版本没有新增的页面,读者切换过去就是 404。本站关闭这个选项。
单个条目可以覆盖全局设置:
结构稳定时开启,结构变动大时关闭。跳到版本首页多一步操作,仍优于 404。
归档横幅
不再维护的旧版本站点上,向读者说明这是一份快照:
archived_version: true 时,每个文档页与书籍页正文顶部出现一条横幅,写明当前版本已不再积极维护,并给出指向 url_latest_version 的链接。文案随站点语言本地化,无需自行编写;version 是横幅里显示的版本号。
横幅只出现在文档与书籍页面上,博客和落地页没有。
params.version 与 params.versions 的区别
两个键名字相近,职责不同:
params.versions是 一张跨站点的清单:菜单里能跳到哪些版本,各自的地址是什么。它描述的是其它站点。params.version是当前这次构建自己的版本标识。它决定菜单里哪一项被标成选中、归档横幅里显示什么版本号,data/download/*.yaml没写version时也以它兜底(见发布与下载页)。
它不一定是 Git 引用。需要一个能解析的发布 tag(例如安装命令里引用的那个)时,另设一个自己的参数,不要复用 params.version。这两个键的完整定义在配置总览。
多版本部署布局
| 布局 | baseURL | 特点 |
|---|---|---|
| 子域名 | https://v1-9.docs.example.com/ | 各版本相互独立,互不影响;需要给每个版本配 DNS 与证书 |
| 子路径 | https://docs.example.com/v1.9/ | 单域名,SEO 权重集中;需要托管方支持按路径路由到不同产物 |
每个版本是一次独立构建:从对应的 Git 分支或 tag 检出内容,用那一版自己的 hugo.yml 构建,产物发布到对应地址。当前版本的站点把 versions 列全,旧版本的站点在列全之外再加上归档横幅。
baseURL 必须包含那段路径否则搜索索引、页面动作与资源链接都指向域名根目录:页面看上去正常,搜索却没有结果。这是子路径部署最常见的故障,部署细节见发布上线。
验证
构建后确认版本菜单进了页面:
params.versions为空或未配置时,菜单整个不生成。看当前版本有没有被标成选中:
一条都没有,说明
params.version与versions里的version字段对不上,或者baseURL与该条目的url不一致(注意结尾斜杠)。逐个访问菜单里的链接。开启
version_menu_pagelinks时,在一篇旧版本不存在的文档上试一次,确认落点可以接受。归档站点上打开任意文档页,横幅应该在正文最上方,语言与站点一致,链接指向最新版本。
按 ⌘/Ctrl + K 打开命令面板,「切换版本」列出的应该是同一份清单。
相关
11 - 分类体系
目录树只有一条路径,分类体系(taxonomy)给页面加第二条:同一篇 PostgreSQL 备份文档既在「运维」目录下,又能从「备份」标签页找到。启用它只需要 Hugo 的 taxonomies: 配置,术语页、筛选芯片、右栏分类云与顶栏分类面板都由主题自动生成,无需编写模板。
本页带着一个分类:标题下面的「分类: 定制站点」一行,以及右栏目录下面那组带计数的芯片,都不需要在页面上写配置。
启用分类法
分类法由 Hugo 决定,主题不额外提供开关。在 hugo.yml 顶层 写 taxonomies:,键是单数名、值是复数名:
这是本站的配置。三点需要注意:
- 写了
taxonomies:之后它就是 完整列表,不是追加。想在自定义分类法之外保留tags/categories,必须把它们一起列出来。 - 复数名同时是 URL 段:
/zh/tags/、/zh/categories/。 - 全部关闭:
disableKinds: [taxonomy, term]。
加一个自己的分类法,例如按产品模块归类:
分类法的显示名:tag tags category categories module modules 这六个键在主题的每个语言文件里都有本地化标题(中文分别是「标签」「分类」「模块」)。其它分类法用复数名的 humanize 结果(products → Products)。要自己定名字,在 content/<复数名>/_index.md 与 _index.zh.md 里写 title / linkTitle,主题会优先用它:
为页面添加标签
front matter 里的键名用 复数名(taxonomies 的值那一列),值始终是列表,只有一项也要写成列表:
整个栏目共用一个分类时,写在栏目首页的 cascade 里,无需每页重复:
本站 docs 的六个栏目都是这样配置的。页面自己写 categories: 会覆盖 cascade,不合并:要在栏目分类之外再加一个,两个都要写出来。
页面上的术语行
文档页与博客页在标题、摘要下面渲染一行已分配的术语,链接指向对应的术语页,本页顶部的「分类: 定制站点」即是。这一行的容器是 .taxonomy-terms-article,按分类法另带一个 .taxo-<复数名> 类,单独调样式时用这两个选择器。
默认列出该页的 全部 分类法,只有 authors 与 series 这两个保留复数除外——它们各自有专门的呈现面(署名行与系列横幅),再列一遍标签等于把同一件事说两遍。在 page_header 里点名,就能把它放回去。
只想显示其中几种、并固定顺序:
主题认识名字的两个分类法
authors 与 series 就是普通的 Hugo taxonomy,按普通方式声明——主题不为它们增加任何参数。主题增加的是各自的一套呈现,所以「声明」本身就是全部开关:
| 复数名 | 声明之后打开了什么 | term 页变成什么 |
|---|---|---|
authors | 文章头部的头像与带链接的名字、列表行上的名字、feed 里每位作者一条 <dc:creator> | 作者主页:显示名取 term 页的链接标题(有 linkTitle 用它,否则用 title),description 是一句话介绍,正文是长介绍,头像取题图解析器为这一页选中的那张 |
series | 正文上方一条横幅,写明系列名、本篇位置、下一篇,以及折在 <details> 里的完整列表 | 系列引言,成员按阅读顺序排列,而不是最新在前 |
两者的完整说明与各自需要的 front matter 在写博客。这里只提两件事:
- 主题刻意不设
data/authors文件。作者主页就是 term 页本身,因此不存在第二份权威跟它打架。 - 系列 term 页是唯一不按时间倒序排列的 term 页。写了
series_weight的成员按升序排在前,其余按日期升序跟在后。term 页没法把顺序交给 Hugo,所以主题自己算一次,两处呈现读同一份结果。
标签页与分类页
每种分类法生成两级页面:
| 页面 | URL | 内容 |
|---|---|---|
| 分类法列表页 | /zh/categories/ | 标题是分类法的本地化名(「分类」),下面是全部术语的筛选芯片,每枚带计数,第一枚是「全部」 |
| 术语页 | /zh/categories/定制站点/ | 标题是「分类: 定制站点」,下面按日期倒序列出该术语的全部页面,样式与博客列表一致 |
中文术语的 URL 使用中文字符(浏览器地址栏显示 定制站点,HTML 里是百分号编码),Hugo 不做拼音转写。需要 ASCII URL 时改用英文术语,再在 content/categories/<术语>/_index.zh.md 里用 title 给它一个中文显示名,这是 Hugo 的术语页内容文件机制。
术语页在内容树里没有固定位置,它借用一个:某术语的成员全部位于同一个顶层栏目下时,术语页用那个栏目渲染侧栏树与根链接,读者从文档里点进标签仍留在文档导航中;成员跨栏目时回退到站点级的树。筛选芯片里的「全部」按同一规则处理:只有一个栏目时指向该栏目首页,跨栏目时指向分类法列表页。
筛选芯片只出现在分类法列表页;术语页上换成右栏的分类云。
右栏的分类云
文档页、博客页与术语页的右栏(目录下面)每种分类法一组,芯片带计数,可折叠。这一组是自动的,没有开关:定义了分类法且当前范围内有术语时就会出现。
计数 不是全站计数,而是按顶层栏目统计:先看页面的 type 有没有同名栏目(type: docs 的页面用 /docs/ 这棵树),没有就用页面所在的顶层栏目。博客页上的「标签: release 4」说的是博客里有 4 篇,不是全站有 4 篇。
图标按复数名配置:
categories 与 tags 的默认值就是上面那两个,其它分类法默认 fa-solid fa-shapes。图标是一对 Font Awesome class,与站点其它地方的图标写法一致。
顶栏菜单里的分类面板
主菜单里指向分类法列表页的条目,会自动变成一块术语芯片面板(按用量降序,带计数),无需手写下拉项:
pageRef: /tags 与旧式的 url: /zh/tags/ 都能识别:URL 形式的菜单先解析成本站页面再判断类型,从旧配置迁移时不必改写法。菜单本身的其它写法见导航与菜单。
双语标签
Hugo 的分类按语言分开统计、分开链接:/categories/ 与 /zh/categories/ 是两棵互不相干的树,中文页只进中文那棵。术语要在各自语言的 front matter 里各写一遍:
两条要注意:
- 同一个词在两种语言里写成同样的字符串(例如
release),得到的仍然是/categories/release/与/zh/categories/release/两个术语页,各自只统计本语言的页面。不要为了统一而在中文页里写英文词:右栏芯片会显示英文。 - 分类法的显示名会跟着语言走(上面那六个内置键),但 术语名不会:术语就是你在 front matter 里写的那个字符串,主题不翻译它。英文页里写
高可用,英文站的芯片上显示的就是高可用。
多语言站点的其余部分见多语言。
按内容类型开关
主题没有「文档显示、博客不显示」这类开关,控制点是给哪些页面打标签。本站的做法:
| 内容 | categories | tags | 效果 |
|---|---|---|---|
content/docs/** | 栏目级 cascade(「定制站点」等六个) | 不打 | 术语行只有一行「分类」 |
content/blog/** | 每篇写(release、oink) | 每篇写(Oink、Release) | 术语行两行,右栏两组芯片 |
让整个栏目从分类里消失:删掉栏目首页 cascade 里的 categories,不需要别的配置。让某一页不进分类:在它自己的 front matter 里写 categories: [],空列表覆盖 cascade。
验证
页面上看三处:
- 本页标题下面有一行「分类: 定制站点」;
- 右栏目录下面有按分类法分组的芯片,每枚带计数;
- 打开 /zh/categories/ 能看到全部术语的筛选芯片,点任一枚进入术语页。
命令行上查产物:
主题仓库自带一个针对性检查,验证「不写 taxonomies: 就不生成分类页」与「术语页在中英文下标题正确」两件事:
限制
page_header: []不会 隐藏术语行:空列表被当作未设置,回落到「列出全部分类法」。要去掉这行,就不要给这些页面打标签,或在assets/scss/_styles_project.scss里隐藏.taxonomy-terms-article。- 右栏分类云没有开关,也没有条数上限;术语数量很多的站点应当减少分类法,配置层面没有裁剪手段。
- 术语页没有跨语言对等关系:语言切换在术语页上不保证落到「同一个术语的另一种语言」。
相关
12 - 仓库与页面信息
面包屑行右侧的 操作菜单 里与仓库有关的条目,由几个 github_* 参数推导;页尾的「最后修改」信息行来自 git 历史。前提是内容存放在一个 GitHub 风格的仓库里。
四个键接通全部链接
操作菜单里所有跟仓库有关的条目,都由这几个键推导出来:
上面是本站的真实配置。填好之后,本页的操作菜单里这几条指向:
| 菜单条目 | 目标 |
|---|---|
| 编辑当前页面 | …/edit/main/content/docs/customize/repository.zh.md |
| 查阅编辑历史 | …/commits/main/content/docs/customize/repository.zh.md |
| 添加子页面 | …/new/main/content/docs/customize?filename=change-me.md&value=<模板> |
| 提交文档议题 | …/issues/new?title=仓库与页面信息 |
| 提交项目议题 | https://github.com/pgsty/oink/issues/new |
几点约定:
github_repo指向内容所在的仓库,不是主题仓库。写主题仓库会把读者的改动引到错误的位置。省略它时,上表五条全部消失。github_project_repo是第二个仓库,接收产品缺陷而非文档错误的议题。读者难以区分两者时不要配置它。github_branch默认main,填的是内容分支,不是部署分支,也不是 Pages 自动生成的分支。github_subdir是仓库内路径。站点源码在仓库根目录时留空;放在子目录(例如仓库里同时有代码和website/)时填website。
这几个键都可以在站点、单语言、栏目 cascade 或页面 front matter 上设置,内容来自多个仓库时用得到。键的完整定义在配置总览。
内容来自另一个仓库
把一棵子树从上游仓库挂进来时,用栏目 cascade 覆盖仓库参数,再用 path_base_for_github_subdir 告诉主题:先去掉本地路径前缀,剩下的部分接到 github_subdir 后面。
content/reference/api/client.md 因此映射到上游的 docs/api/client.md。
path_base_for_github_subdir 的值是正则;源文件名与本地不同名时改用 from / to 映射,例如把每个栏目的 _index.md 对到上游的 README.md:
OINK 把 .md 与 .zh.md 并排放在同一个目录里,两种语言共用同一个路径前缀,正则里不需要语言目录。改完从叶子页、栏目首页、两种语言各点一次「编辑当前页面」:正则去掉的部分过多时,生成的 URL 看上去合理,实际是 404。
关闭其中几条
菜单里每个条目都带一个稳定的操作 ID:
| 菜单条目 | 操作 ID |
|---|---|
| 复制 Markdown 文本 | copy_markdown |
| 查阅 Markdown 源码 | view_markdown |
| 在 ChatGPT / Claude 中打开 | open_chatgpt / open_claude |
| 查阅编辑历史 | view_history |
| 编辑当前页面 | edit_page |
| 添加子页面 | create_child_page |
| 提交文档议题 | create_issue |
| 提交项目议题 | create_project_issue |
| 打印完整章节 | print_section |
托管服务不支持某条时,用 CSS 隐藏:
命令面板用的是同一批 ID,隐藏菜单条目不会让它从面板里消失。全站用不上的目标应当从配置里省略对应的键,而不是用 CSS 遮盖:CSS 只能隐藏链接,不能把错误的链接改对。
整个菜单也可以按页面关闭,front matter 写 page_context_menu: false,见页面参数。
「添加子页面」预填的新页面模板来自主题的 assets/stubs/new-page-template.md;站点在自己的 assets/stubs/new-page-template.md 放一份同名文件即可替换成自己的骨架。
最后修改时间
这一行的数据来自 git,不是文件的 mtime。打开 Hugo 的 git 支持:
页尾出现「最后修改 2026年8月17日 · <commit 主题> (a1b2c3d)」,commit 部分链到 …/commit/<hash>。lastmod_commit 三个取值:
| 取值 | 显示 |
|---|---|
subject(默认) | commit 主题 + 缩写 hash |
hash | commit a1b2c3d |
none | 只有日期,不链 commit |
写别的值会让构建失败,报 invalid params.ui.lastmod_commit。
两点注意:
- CI 必须有足够的 git 历史。浅克隆(
fetch-depth: 1)取不到文件的最后一次提交,日期会缺失或错误。GitHub Actions 里设fetch-depth: 0。 - 未提交的文件没有 git 时间。本地预览新写的页面时这一行不出现。
git 历史不可用时不要用构建时间代替「最后修改」,构建时间不是内容的修改时间。
这一行属于 页面信息(Annotation) 组件,默认开启,位置在反馈之后、翻页器之前。整页关闭写 annotation: false。
这一行不是页面信息区块的全部。同一个区块还会渲染两种来源说明,都由页面 front matter 驱动,不需要覆盖模板:
- 上游署名:页面改写自别处时写
upstream_link,配上upstream_name、upstream_copyright、upstream_license、upstream_notice四个必填键,页尾出现一条带作品、版权人、许可证与完整声明链接的署名行;再写upstream_modified: true追加一条「本地已修改」。 - 译文说明:
params.ui.translation_notice写权威版本的语言代码,译文页就显示一条指回原文的说明;以本语言原创的页面写translation_notice: false退出。
这两族键的完整定义见页面参数。
确实需要自定义时,三个覆盖点各管一层:
| 覆盖哪个 partial | 改什么 |
|---|---|
layouts/_partials/annotation-items.html | 增删或重排这些行,保留主题的标记、图标、打印规则与无障碍标签 |
layouts/_partials/page-meta-lastmod.html | 换掉这些行的渲染标记 |
layouts/_partials/page-annotation.html | 换掉整个区块的外层容器 |
页尾的组成
五个组件的顺序是固定的,所有阅读型布局共用一份实现:
| 顺序 | 组件 | 主题默认 | 页面开关 |
|---|---|---|---|
| 1 | 分享 Share | 关(params.ui.share 为空) | share: false,或页面自己的列表 |
| 2 | 反馈 Feedback | 关 | feedback: true / false |
| 3 | 页面信息 Annotation | 开 | annotation: false |
| 4 | 翻页器 Pager | docs / book / blog 开 | pager: false |
| 5 | 评论 Comments | 配置完整时开 | comments: false |
顺序对应读者读完最后一段之后依次会做的事:把这页递出去、说一句有没有帮上忙、看看它从哪来、翻到下一页、加入讨论。分享排在最前,因为它是唯一朝外的一块,而且一个决定要把文章转给别人的读者,在被问「这页怎么样」之前就已经决定了。分享栏的配置见写博客。
评论的配置在启用评论。
反馈组件
一行问题、两个按钮:「这篇文档解决了你的问题吗?」→ 是 / 否。选「否」再展开四个可选原因。默认关闭:
只给文档栏目开,用 cascade(博客通常只留评论):
行为边界:
- 点击即完成,没有输入框、没有提交按钮、没有登录。
- 选择按「页面 + 语言」写进浏览器
localStorage,读者回访时还能看到并修改自己的选择。 - 站点已有 Google Analytics(
gtag)时,发送docs_feedback事件,字段result(solved/not_solved)、page_path、language;选原因时再发一次,多带reason与refinement: true,便于和首次计数区分。没有 analytics 时组件照常工作,只是不上报,它不需要任何后端。 - 本页启用了评论时,反馈结果下面会多一条「在评论区补充详情」的锚点链接。反馈与 giscus 是两条独立的数据流,主题不会代替读者写评论。
本页在 front matter 里写了 feedback: true(docs 栏目默认关闭),页尾可以看到真实的组件。
贡献者墙
contributors shortcode 渲染一面 GitHub 头像墙,数据来自站点 data/ 目录下的一个文件,不在构建期访问 GitHub:
字段:github 必填(校验成合法的 GitHub 用户名,重复会让构建失败);name 缺省等于 github;role 可选;url 缺省是 https://github.com/<github>;avatar 可选,不填时渲染成首字母占位块,不发任何网络请求,填写时必须是 http(s):// 或站内根相对路径。
多套名单写多个数据文件,用 data= 指定:
在 Markdown 与 RSS 输出里,头像墙降级成一串 - [@handle](url) — role 的列表。
data/contributors.yaml上面的例子因此不在本页渲染。放一个数据文件进 data/ 就能看到效果。
验证
- 点开本页面包屑行右侧的操作菜单,「编辑当前页面」应该指向
github.com/<你的仓库>/edit/<分支>/<源文件路径>,路径要与仓库里的实际路径逐段对应。 - 从栏目首页(
_index.md)再点一次:栏目首页最容易被path_base_for_github_subdir的正则改错。 - 页尾应有「最后修改」行;本地新建、尚未
git commit的页面没有这一行是正常的。 - 命令行核对生成的链接:
相关
13 - 打印支持
单页打印不需要配置:外壳(侧栏、目录、顶栏、按钮)都带 d-print-none,浏览器的 Cmd/Ctrl+P 得到的是一份干净的正文。主题因此没有页面级的「打印本页」按钮。
需要配置的是另一件事:把一整个栏目(或一整本书)连同全部子页面合成一份带目录的连续文档。以下内容覆盖启用方式、打印视图的结构,以及排除页面的做法。
启用整章打印
print 是主题声明的自定义输出格式,主题不替站点打开它。在站点自己的 hugo.yml 里给 section 加上:
这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 print 时要把该类型原本有的格式(HTML、RSS、markdown)一起写全,漏一个就丢一种输出。
开启后,每个栏目多出一个 URL。路径段 _print 在最前面,语言前缀之后:
| 页面 | 打印视图 |
|---|---|
/zh/docs/customize/ | /zh/_print/docs/customize/ |
/zh/docs/ | /zh/_print/docs/ |
/zh/blog/release/ | /zh/_print/blog/release/ |
页面操作菜单里同时出现「打印完整章节」,命令面板里也能搜到同一条(操作 ID print_section)。它打印的是 当前栏目:在 /zh/docs/customize/print/ 这页点它,得到的是整个「定制站点」栏目,不是这一页。
打印视图的结构
打开上面任意一个链接,从上到下是:
- 一条提示条:「这是本节的多页打印视图。点击此处打印。返回本页常规视图。」它带
d-print-none,只在屏幕上出现,不进纸。 - 栏目标题与摘要。
- 全栏目目录,条目编号是
1:、2:、2.1:这样的层级号,链接指向文档内的锚点。 - 每个页面依次排列,标题变成
1 - 配置总览这种「编号 - 标题」,描述作为导语,正文原样渲染。
页面顺序是侧栏顺序(weight),子栏目递归展开。第二页起每页都另起一页;第一页是否另起一页,取决于栏目首页自己的正文是否超过 50 个词:首页只有一句话时不单独占一张纸。阈值可以调整:
不需要那份目录:
也可以只对某个栏目关闭,写在栏目首页 front matter 里:
把某些页面排除在外
纯链接页、只有一段跳转说明的页、体积巨大的截图页进纸意义不大。给它们写 no_print:
它只影响整章打印视图,页面自己的 HTML 与浏览器 Cmd/Ctrl+P 不受影响。侧栏分隔项(sidebar_divider)也自动排除。
组件在打印态的形态
打印是四态输出之一,每个组件都有确定的打印形态。整章打印视图与浏览器打印单个页面,规则一致:能交互的降级成静态,可折叠的一律展开。
| 组件 | 打印形态 |
|---|---|
| 提示块 | 静态块,折叠型(- / + / DETAILS)全部展开;边框转灰、去底色 |
| 标签页 | 标签条消失,所有面板依次展开,每个面板带自己的标题 |
| 代码块 | 去掉复制与展开按钮,取消最大高度与滚动,长行改为自动折行 |
| 表格 | 满宽静态表,取消横向滚动;表头在跨页时重复 |
| 图片 | 图与图注保留,缩放相关的属性被剥掉,宽度收进版心 |
| 画廊 | 网格改为竖排堆叠 |
| 文件树 | 静态面板,目录全部展开,分栏停在构建期宽度 |
| 参数表 | 完整定义列表,两种形态一致 |
| 公式 | 静态渲染的 KaTeX / MathML |
| Mermaid · Markmap · PlantUML | 照常渲染成图:打印视图仍是一张 HTML 页,这几个运行时照常加载 |
| ECharts · Infographic | 降级成围栏源码块,不渲染图表 |
| 卡片 / 步骤 / 徽章 / 按键 | 静态呈现,内容不变 |
页面外壳不进纸:侧栏、目录、顶栏、页面操作菜单、反馈组件、标题旁的锚点链接、行内复制按钮。
上表里靠浏览器端运行时绘制的那三种图(Mermaid、Markmap、PlantUML),触发打印前要确认它们已经绘制完成。
浏览器打印样式
主题自带一层 @media print 规则,单页打印与整章打印共用:
- 纸张
A4,页边距18mm 16mm 20mm;正文10.5pt,强制浅色配色。 - 字体切到
--td-print-font-family这个排印令牌,见品牌外观。 - 标题不与正文分家(
break-after: avoid-page),段落与列表项保留 3 行孤行 / 寡行控制。 - 表格、图片、块引用、提示块、卡片、标签页尽量不跨页断开;代码块允许跨页,但会自动折行而不是截断。
- 链接加下划线、转深蓝色,不会在链接后面打印出 URL 文本。需要这个行为的站点自己加:
- 收起的
<details>一律展开:折叠的提示块与文件树目录在纸上是完整的。
自定义排版写在 assets/scss/_styles_project.scss 的 @media print 块里,不需要改模板。
替换打印模板
需要改结构(例如给每页加页眉、换编号格式)时,覆盖最窄的那个 partial,都在 layouts/_partials/print/ 下:
| Partial | 负责 |
|---|---|
print/render.html | 整章视图的骨架:提示条、目录、递归内容 |
print/page-heading.html | 文档开头的标题与导语 |
print/content.html | 单个页面在整章视图里的呈现 |
print/toc-li.html | 目录里的一行 |
后三个支持 按内容类型 分化:建 print/page-heading-blog.html、print/content-book.html,主题会优先用带类型后缀的那个。
整本书的打印(type: book)走另一条路径:章节编号、图表编号与交叉引用都保持全书连续,见书籍出版。
验证
再看页面:
- 浏览器打开
/zh/_print/docs/customize/,确认目录条数等于栏目页数(减去no_print: true的页)。 - 在这个视图里按
Cmd/Ctrl+P,打印预览里应当看不到提示条、顶栏与任何按钮。 - 找一页含标签页与折叠提示块的(例如标签页),确认预览里所有面板都展开。
- 打印一份 PDF 通读分页情况,阈值不合适时调整
section_break_wordcount。
相关
14 - Agent 支持
HTML 页面里有侧栏、脚本与样式,模型读它要先剥掉这层外壳。OINK 让同一份内容再产出一份纯 Markdown:每页一个 .md,站点根目录一份 llms.txt 索引,页面上一个「复制 Markdown 文本」按钮。三者都是构建期产物,没有运行时服务,也不需要内容协商。
这三件事都要站点自己在 outputs 里声明,主题不替站点打开。
每页一份 .md
markdown 是 Hugo 的内置输出格式。把它加进需要的页面类型:
这是本站的配置。outputs 的每个键是 整体替换 而不是合并:加 markdown 时要把该类型原本有的格式(RSS、print)一起写全,漏一个就丢一种输出。
URL 规律是在页面 URL 后面接 index.md:
| 页面 | Markdown |
|---|---|
/zh/docs/customize/agents/ | /zh/docs/customize/agents/index.md |
/zh/docs/customize/(栏目首页) | /zh/docs/customize/index.md |
/zh/(站点首页) | /zh/index.md |
每个 HTML 页的 <head> 里同时有一条发现用的链接,抓取工具不必推断 URL:
.md 的内容
不是把渲染好的 HTML 转回 Markdown,而是 你写的源码:front matter 换成一个 H1 标题加一段引用式摘要,其后是正文原文,shortcode 就地展开成各自的 Markdown 形态。
原生 Markdown 形态的组件(提示块、表格、参数表、图片属性行、代码围栏、数据围栏)在 .md 里原样保留源码,模型读到的与你写下的是同一份内容。栏目首页在正文之后还会附一份 Section pages: 子页链接清单。
shortcode 形态各有确定的降级:徽章变成强调文本或链接,按键变成 Ctrl + K,标签页变成一段段 **标签名** 小节,参数表变成条目列表。每个组件页的「输出形态」小节写了它自己那一行。
站点没有开 LLMS 输出时,上面那条 LLMS index: 不会出现:主题不指向未发布的文件。
llms.txt
llms.txt 是站点根目录的一份纯文本清单,告诉模型「这个站有什么、机器可读版本在哪」。给 首页 加上 LLMS 输出格式即可生成:
多语言站点每种语言各一份:/llms.txt 与 /zh/llms.txt。内容是自动生成的站点索引:
三段的来源:Site index 是本语言首页加站点主菜单(menus.main,条目有 Markdown 版就链 Markdown 版,带 description 的顺带写上);Documentation index 是 docs 栏目的子栏目及其下一层页面,缩进表示层级,每行附上该页的 description;Site locales 是站点配置里的全部语言。指向站外的菜单条目(GitHub、issue 跟踪器)会被剔除:它们属于导航外壳,不是本站内容。
改进 llms.txt 的入手处是主菜单与各栏目首页的 description,不是这个模板。
页面上的 Agent 动作
面包屑行右侧的操作菜单里,跟 Agent 有关的是四条:
| 条目 | 做什么 | 出现条件 |
|---|---|---|
| 复制 Markdown 文本 | 抓取本页 .md 写进剪贴板(悬停时预取,点击后无明显等待) | 本页有 markdown 输出 |
| 查阅 Markdown 源码 | 新标签页打开 .md | 本页有 markdown 输出 |
| 在 ChatGPT 中打开 | 带一句提示词跳转到 ChatGPT | assistant_links: true |
| 在 Claude 中打开 | 同上,跳转到 Claude | assistant_links: true |
前两条只要开了 markdown 输出就存在。「复制」是拆分按钮的左半边(剪贴板图标),复制成功后短暂显示一个对勾。
后两条默认关闭,要显式打开:
打开之后的边界:读者点击时,运行时用浏览器地址栏里的完整 URL(含真实域名、查询串与锚点)拼一句提示词,中文站是「请阅读
页面可以收紧站点策略,不能反向打开:front matter 里 page_context_menu: { assistant_links: false } 关掉本页的助手链接;站点没开时页面写 true 不会生效。整个菜单按页关闭用 page_context_menu: false,见页面参数。
命令面板里也能搜到这两条助手动作(用的是同一份动作清单),见命令面板。
按页面退出 .md 输出
在页面 front matter 里重写 outputs。它同样是整体替换,只写要保留的格式:
要保留 RSS、只去掉 Markdown,就把其它格式列全:
自定义输出
主题用 layouts/all.md 渲染 Markdown 输出,用 layouts/index.llms.txt 生成 llms.txt。站点在自己的 layouts/ 下放同名文件即可整体替换,但 先考虑更窄的做法:
- 按内容类型:
layouts/blog/single.md、layouts/docs/list.md这样带类型的路径只影响那一类内容,主题的打印模板即按此分化(layouts/blog/single.print.html)。查模板查找顺序确认你的组合。 - 按 shortcode:站点自己的 shortcode 可以加输出格式专属模板,让它在 Markdown 输出里给出更适合机器读的形式。
- 按页面:少数高价值页面手写内容,成本低于改模板。
llms.txt 的内容由站点结构决定,改模板之前先确认问题不在主菜单或 description。
验证
线上或本地预览用 curl:
再检查三处:
- 任一页 HTML 的
<head>里有rel="alternate" type="text/markdown"; - 面包屑行右侧的复制按钮点击后粘贴,得到的是 Markdown 而不是 HTML;
llms.txt里没有指向站外的链接。
限制
- 主题产出的机器可读表面只有两样:每页
.md与llms.txt。没有nav.json,也没有别的结构化目录接口;站点地图仍是 Hugo 自己的sitemap.xml。 LLMS输出格式声明为非替代格式,所以llms.txt不会出现在<head>的alternate链接里,也没有对应的页面操作;它靠约定俗成的根路径被发现。- 服务端内容协商(同一个 URL 按
Accept: text/markdown返回 Markdown)不属于主题范围,要做在托管层。 - Markdown 输出走 源码 路径:只在浏览器端由 JavaScript 生成的内容(运行时绘制的图表)在
.md里是围栏源码,不是图。