这是本节的多页打印视图。 .
维护管理
本栏目覆盖内容写完之后的运维事项:在本机预览、构建并部署产物、接入评论与分析、跟随主题版本升级、故障定位。前面五个栏目决定站点的外观与内容,这一栏决定站点能否构建、部署在哪、出问题如何排查。
按任务导航
1 - 本地预览
两条命令覆盖日常工作:hugo server 在本机预览改动,hugo 产出可以部署到任何静态托管的 public/。前提是本机安装了 Hugo Extended(不低于 0.160.1);用 Hugo Module 引入主题时还需要 Go。构建不依赖 Node.js、npm 与 PostCSS,它们只服务于本仓库自身的回归检查。
预览服务器
在站点根目录(hugo.yml 所在的目录)执行:
打开 http://localhost:1313/。保存文件后 Hugo 重新构建并刷新浏览器,切换 Git 分支同样触发重建。首次启动较慢:用 Hugo Module 引入主题时,Hugo 要先通过 Go 把模块下载到缓存,之后的启动都走缓存。
常用开关
-D/--buildDrafts,- 把
draft: true的页面也构建出来 -F/--buildFuture,- 把
date/publishDate在未来的页面也构建出来 -E/--buildExpired,- 把
expiryDate已过的页面也构建出来 --disableFastRender,- 每次改动都整站重渲染,不用增量
-M/--renderToMemory,- 只在内存里渲染,不落
public/ -N/--navigateToChanged,- 保存哪个页面,浏览器就跳到哪个页面
--bind,- 监听地址;要让局域网或容器外访问就设
0.0.0.0 -p/--port,- 监听端口
--minify,- 预览也压缩输出,用来复现生产环境下的渲染
--printPathWarnings,- 有两个页面写到同一个目标路径时告警
本站开发时用的组合是:
-DFE 是 -D -F -E 的合写,草稿、未来与过期页面一并构建,写作时新建的页面才可见。
改动没有生效
Hugo 默认开启快速渲染(fast render),只重建它判定受影响的部分。修改布局、配置、data/ 或被 include 引用的文件时,增量判定可能不准,页面看起来没有变化。三步排查:
- 加
--disableFastRender重启,看是否恢复。 - 硬刷新浏览器(
Cmd/Ctrl+Shift+R),排除浏览器缓存。 - 仍未恢复则清缓存后重启。
从其它设备访问
hugo server 默认只监听 127.0.0.1,其它设备访问不到。要在手机或另一台机器上预览:
--baseURL 必须写成对方可访问的地址,否则页面能打开,但 CSS 与搜索索引这类走绝对路径的资源会指向 localhost。
生产构建
部署产物用 hugo 构建,不用 hugo server:
产物写入 public/,该目录可以脱离源码树独立部署。四个开关各管一件事:
--gc- 构建后清掉
resources/_gen里不再被引用的缓存资源 --minify- 压缩 HTML、CSS、JS 与 XML 输出
--printPathWarnings- 两个页面撞到同一个输出路径时告警,多语言站点最常见的静默错误
--panicOnWarning- 遇到第一条 WARNING 就让构建失败
--panicOnWarning 需要单独说明。OINK 的多数降级路径是告警而不是报错:giscus 必填键缺失、params.comments.type 取了不支持的值、Hugo 弃用的配置键,都只打一条 WARNING 然后跳过。CI 日志通常无人逐行阅读,这些问题会带到线上。把这个开关写进构建命令,等于要求零告警才算构建通过。
本站 CI 的构建步骤(.github/workflows/pages.yml)是 hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --panicOnWarning,任何一条告警都会让部署停在构建阶段。
baseURL 与构建环境
baseURL 写在 hugo.yml 里,也可以在命令行覆盖:
部署到子路径时 --baseURL 必须带上那段路径,细节见发布上线。
构建环境用 -e / --environment 选择,hugo 默认 production,hugo server 默认 development。这个选择在 OINK 里有三处可见后果:
production下才输出<meta name="robots" content="index, follow">,其它环境输出noindex, nofollow。production下robots.txt是Allow: /,其它环境是Disallow: /。production下才渲染 Hugo 的 Google Analytics 模板,静态资源也才做指纹与 SRI。
预览部署(PR preview、staging)用非 production 环境构建,产物自带不被搜索引擎收录、不上报分析的行为:
容器内预览
容器不是必需的。两种情况适合用容器:团队需要固定工具链版本,或不希望在每台开发机上安装 Hugo。
镜像里装 Go 的原因:用 Hugo Module 引入主题时,Hugo 需要 Go 解析并下载模块。用 submodule、离线归档或直接克隆的站点可以去掉 Go,镜像会小很多。
public/容器里的进程默认是 root,生成的 public/ 属于 root,宿主机上删不掉。共享环境里用 --user "$(id -u):$(id -g)" 映射用户 ID(上面的生产构建命令已经带了)。
镜像不需要 Node.js、npm 与 PostCSS,也不应出现拉取远程浏览器资源的步骤。网络隔离环境需要预先镜像基础镜像与这两个软件包。
清缓存
Hugo 的中间产物分三处,从轻到重依次清:
public/,- 删了页面但线上还在;或用
hugo --cleanDestinationDir让构建自己清 resources/_gen/,- 换了图片处理参数、换了字体或主色,页面还是旧样子
hugo mod clean,- 换了主题版本但解析出来还是旧的;加
--all清整个模块缓存
public/ 与 resources/ 都应该写进 .gitignore,不要提交生成产物。
与主题一起改
同时修改主题与站点时才需要这一节。用 HUGO_MODULE_REPLACEMENTS 把模块临时指向本地 checkout,go.mod 保持不变:
本站的 Makefile 封装了这几条命令,要求主题 checkout 在同级目录 ../oink:
无论用环境变量还是 Go workspace(go work init + HUGO_MODULE_WORKSPACE=go.work),CI 与生产构建都只看 go.mod;go.work 记录的是开发机的路径,不能提交。判定一个发布标签是否可用时,去掉替换、用 go.mod 里的版本单独构建一次。
断网构建验证
网络隔离环境的验收要同时覆盖构建阶段与浏览器阶段。六步:
- 从一份已校验的主题归档与空的模块缓存开始(
hugo mod clean --all)。 - 阻断出站 HTTP、HTTPS 与 Go module proxy。
- 运行生产构建
hugo --gc --minify --printPathWarnings --panicOnWarning。 - 浏览产物里两种语言的页面:文档页、博客页、首页、404。
- 操作搜索、深浅色切换、图表与内容组件。
- 检查子资源来源,确认没有意外的远程主机。
最后一步用主题仓库里的脚本,它不依赖站点的测试框架:
脚本扫描四种输出里的每个 href / src / srcset / poster 与表单 action,要求它们是站内相对路径或 http / https / mailto / tel,并拒绝行内 on* 事件处理器与 javascript: URL。指向别的主机的 <iframe> <script> <link> <img> <video> <audio> <embed> <object> <source> 一律报错,站点确实要嵌入第三方内容时加 --third-party 放行,多域名语言配置用 --allow-host 追加首方主机。
一次通过只证明当次提交与当次环境。每个主题候选版本、每次随附依赖更新之后都要重跑一遍。
验证
一次干净的生产构建应该是这样:
看到 Total in … 且没有 ERROR / WARNING 才算通过。然后确认:
- 日志里没有 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤。出现了说明配置里混进了上游 Docsy 的流程。
public/下有sitemap.xml、robots.txt,robots.txt是Allow: /。- 开了本地搜索的站点,
public/根下有offline-search-index.<语言>.json。 - 用
hugo server打开代表性页面:一个文档页、一个博客页、首页、404,两种语言、两种配色都看一遍。
构建失败或结果不对,去排错与检查。
相关
- 发布上线 — 把
public/发到 GitHub Pages、Cloudflare 或别处 - 排错与检查 — 构建、语言、搜索、平台四类常见故障
- 从零建站与其它安装方式 — Hugo Module / submodule / 离线归档的取舍
- 配置总览 —
hugo.yml里每个键的定义
2 - 发布上线
OINK 站点的产物是一个纯静态目录,任何能托管静态文件的地方都能部署,不需要 Node 运行时、服务端渲染或构建插件。托管商一侧只有三件事:用正确的 Hugo 版本执行一条命令、发布 public/、让 baseURL 与最终访问地址一致。
前提是本机已经能完成零告警的生产构建。
确定 baseURL
baseURL 是最常见的故障源,失败方式也隐蔽:页面能打开,但搜索索引 404、页面操作链接指向错误位置、部分资源加载失败。
部署到域名根目录:
部署到子路径(https://example.com/docs/)时,路径必须写进 baseURL:
也可以在构建时覆盖,让同一份源码部署到不同位置:
canonifyURLs 修子路径Hugo 的 canonifyURLs 默认 false,保持这个默认值。OINK 的模板与内容链接都基于 baseURL 解析:路径不对是 baseURL 不对,打开 canonifyURLs 会把本来正确的相对链接一起改写,让问题更难定位。
判断是否配对,看构建后搜索索引的请求路径:浏览器应当去 <baseURL>/offline-search-index.zh.json 取索引,取到别处就是 baseURL 不对。
选一个托管商
源码托管在 GitHub 时,一份 Actions 工作流就够:构建在 Actions 里执行,产物通过 Pages 部署 API 发布,不需要维护 gh-pages 分支。
把下面的文件提交到仓库:
这是本站正在使用的工作流。几处不能删:
fetch-depth: 0— 站点开了enableGitInfo时,「最后修改时间」和贡献者信息要读完整 Git 历史,浅克隆会让它们为空。setup-go+go mod download— Hugo Module 方式引入主题时,Hugo 需要 Go 才能解析模块。用 submodule 安装主题的站点改成submodules: recursive,用离线归档的站点把themes/oink/提交进仓库,这两步都可以去掉。GOWORK: off与HUGO_MODULE_WORKSPACE: off— 防止本地开发用的go.work意外参与 CI 构建,保证 CI 验证的是go.mod里固定的那个公开标签。--baseURL "${{ steps.pages.outputs.base_url }}/"— 项目站点的 URL 形如https://<OWNER>.github.io/<REPO>/,configure-pages会把它算出来,不用手写。--panicOnWarning— 有告警不发布。
在仓库 Settings → Pages → Build and deployment 里把 Source 设为 GitHub Actions,推一次 main,在 Actions 标签页查看第一次运行。
自定义域名在同一设置页的 Custom domain 里填写,并按提示配置 DNS,随后把 hugo.yml 里的 baseURL 换成这个域名。发布流程需要产物里带 CNAME 文件时,把它放进 static/CNAME,Hugo 会原样复制到 public/。
Cloudflare Pages 从关联的 GitHub / GitLab 仓库构建,并为每个评审分支创建预览部署。构建在平台侧完成,仓库里不用放工作流。
在 Workers & Pages 里导入仓库,选定生产分支:
构建命令hugo --gc --minify --printPathWarnings --panicOnWarning构建输出目录publicHUGO_VERSION0.164.0(或主题验证过的其它版本)GO_VERSION- 仅 Hugo Module 方式需要;固定一个构建镜像支持的版本
SKIP_DEPENDENCY_INSTALL1
四点说明:
HUGO_VERSION必须显式设置,Production 与 Preview 两个环境都要设。Cloudflare v3 构建镜像的默认 Hugo 版本低于 OINK 要求的0.160.1,不固定版本会在构建镜像更新时静默改变工具链。SKIP_DEPENDENCY_INSTALL=1关掉通用依赖安装步骤。OINK 消费端不需要 Node.js,仓库里只给维护工具用的package.json不应由平台安装。- Hugo 站点不在仓库根目录时,把 Root directory 设成站点目录,输出目录相对它解析。
- 预览部署不要当成生产发布。预览需要用自动生成的 Pages URL 作 base URL 时,构建命令改成
hugo --gc --minify --baseURL "$CF_PAGES_URL",生产发布用规范域名重新构建一次。
检查第一次构建日志:正常的 OINK 消费端构建只有一条 Hugo 命令,不会执行 npm、PostCSS、Autoprefixer,也不会下载主题自有的浏览器资源。
Netlify — 构建命令 hugo --gc --minify,发布目录 public,环境变量 HUGO_VERSION。同样的设置可以写进仓库:
用 submodule 安装主题就打开递归 submodule 检出;用 Hugo Module 就要求构建环境有 Git 和 Go。生产与预览应使用同一个 Hugo 版本,除非预览环境本来就是用来测升级的。
Vercel — 同样的三件事:构建命令 hugo --gc --minify、输出目录 public、环境变量 HUGO_VERSION。它同样不需要安装 npm 依赖。
任何静态服务器(Nginx / Caddy) — 把 public/ 的内容整个铺上去:
站点是纯静态的,没有需要转发给应用服务器的路径。
对象存储 — Hugo 自带 deploy 命令,把目标写进配置即可:
构建之后执行 hugo deploy:它比对远端与 public/ 的差异,只上传变化的文件,并在给了 cloudFrontDistributionID 时使 CDN 缓存失效。不带 --target 时用第一个目标,--dryRun 先看要改什么。两个前提:Hugo 二进制带 withdeploy(hugo version 的输出里能看到),云厂商凭据由标准环境变量或配置文件提供(AWS 上先用 aws s3 ls 确认)。
离线打包 — 网络隔离环境里,在能联网的机器上构建,把产物打成一个包带过去:
构建时就要用目标环境的 baseURL,产物里的绝对链接不能在解包之后再改。
托管商没有 Go — 用 Hugo Module 引入主题需要构建环境有 Go。平台不提供时,改用 Git submodule(构建前执行 git submodule update --init)或离线归档(把 themes/oink/ 提交进仓库),见从零建站与其它安装方式。
预览部署不要被收录
Hugo 的 -e / --environment 只选择构建期行为,不改变站点内容,但 OINK 有三处会跟着它变:production 环境才输出 <meta name="robots" content="index, follow">、才让 robots.txt 变成 Allow: /、才渲染 Google Analytics 模板。PR preview、staging 这类构建不要用 --environment production:
出来的产物自带 noindex, nofollow 与 Disallow: /,也不会向分析服务上报数据。
内容安全策略
主题自带的运行时、字体与图标都是同源资源,严格的内容安全策略(CSP)因此可行。主题不提供一份通用策略:需要哪些指令由站点启用了什么决定。
改变所需指令的地方有五处:
- 作者写的行内 HTML 与行内脚本,
renderer.unsafe: true之下由作者负责。 - ECharts 的
$fn:回调:回调函数由站点注册到window.OinkEchartsFunctions,注册脚本的来源要进script-src。 - 分析脚本:站点自己插入的那段脚本与它上报的目标。
- 远程 API 规范与自建图表服务:落在
connect-src与img-src。 - giscus:
script-src与frame-src要一起放行。
从只覆盖已审查功能的最小策略起步,逐项放行:不需要回调时让 ECharts 选项保持纯数据,审查作者写的行内脚本,只为站点主动启用的集成添加远程来源。产物里的子资源来源可以先用断网构建验证里的脚本扫一遍。
验收清单
部署完成后按这张表走一遍。前四项是构建期的,后面几项要在真实 URL 上查。
零告警构建- 构建命令带
--printPathWarnings --panicOnWarning,日志里有Total in … baseURL正确- 页面源码里
<link rel="canonical">指向真实生产地址(含子路径) 站点地图<baseURL>/sitemap.xml可访问;多语言站点是一个索引,指向/en/sitemap.xml、/zh/sitemap.xmlrobots<baseURL>/robots.txt是Allow: /并带Sitemap:行;预览部署应该是Disallow: /搜索索引- 浏览器能取到
<baseURL>/offline-search-index.<语言>.json,站内搜索有结果 Markdown 输出- 任一页面 URL 后面加
index.md能取到纯文本(站点在outputs.page里开了markdown时) llms.txt<baseURL>/llms.txt与<baseURL>/zh/llms.txt可访问(站点在outputs.home里开了LLMS时)两种语言- 两边的文档页、博客页、首页都能打开,语言切换落到对应页面而不是首页
外观与交互- 深浅色切换、打印视图、代表性组件(提示块、标签页、代码块复制)正常
404- 访问一个不存在的路径,看到站点自己的 404 页
sitemap.xml、robots.txt、.md 与 llms.txt 这几项的开关在配置总览,Agent 输出的细节见 Agent 支持。
回滚
静态站点的回滚就是重新发布上一个已知可用的 commit,不要在生产上手工改文件。
- GitHub Pages:在 Actions 里找到上一次成功的
Deploy Oink site to GitHub Pages运行,点 Re-run all jobs;或者git revert出问题的提交再推一次。 - Cloudflare Pages / Netlify / Vercel:在部署列表里选上一个成功的部署,用平台的 Rollback / Publish deploy 把它重新设为生产版本。
- 自建静态服务器:保留上一份
tar.gz,解压覆盖。离线打包里给产物加日期后缀就是为了这一步。
问题出在主题升级而不是内容时,回滚的是 go.mod 里固定的版本,见版本升级。
相关
3 - 启用评论
OINK 的评论走 giscus:每个页面对应一条 GitHub Discussion,读者用 GitHub 账号登录后发言,维护者在 GitHub Discussions 里审核与管理。主题不提供自建评论后端,也不内置 giscus 以外的服务商。
前提是一个公开的 GitHub 仓库,访客读不到私有仓库的 Discussions。
启用评论的页面会从 https://giscus.app 加载脚本和 iframe,网络隔离环境里用不了。它默认关闭,只在显式打开时才加载。站点有隐私政策时,这条外部数据边界应当写进去。
准备 GitHub 仓库
-
选一个公开仓库存放评论线程,可以就是站点源码仓库。
-
在仓库 Settings → General → Features 里勾选 Discussions。
-
为该仓库安装 giscus GitHub App。未安装 App 时访客无法评论或表态。
-
选一个 Discussion 分类。giscus 推荐 Announcements 类型:只有维护者与 giscus bot 能在该类型下新建 Discussion,读者不会误开话题。
仓库 ID 与分类 ID 是公开标识符,不是凭据。不要往 Hugo 配置里放 personal access token、OAuth secret 或密码。
生成配置
打开 giscus.app,按表单填仓库、映射方式和分类,页面下方会生成一段 <script>。把里面四个属性抄进 OINK 配置:
data-reporepodata-repo-idrepoIddata-categorycategorydata-category-idcategoryId
映射方式(mapping)决定哪个页面对应哪条 Discussion。OINK 默认 pathname,适合发布路径稳定、同一个仓库要服务多个域名或预览环境的站点。开始收集评论之后再改 mapping 或移动页面,giscus 会去找另一条 Discussion:已有评论不会被删除,但页面上再也找不到它们。映射方式要在上线前定好;确实要改 URL 时,同时保留重定向或重命名 Discussion。
全站启用
把生成的标识符写进站点配置:
上面是本站正在使用的配置。repo、repoId、category、categoryId 四个键缺一不可:任何一个缺失或只有空白字符,Hugo 打一条 WARNING 并跳过 giscus,构建不会失败,因此生产构建要带 --panicOnWarning。type 目前只接受 giscus,写别的值同样是告警加跳过。params.comments 的键名与 Hextra 同形,从 Hextra 迁来的配置可以照搬。
其余的键(strict、reactionsEnabled、emitMetadata、term、lang、lightTheme、darkTheme、ariaLabel、errorMessage)都有默认值,完整定义见配置总览。功能开关既可以写 YAML 布尔值,也可以写 giscus 风格的 0 / 1。
按页开关
front matter 里的 comments 可以从任一方向覆盖全站开关,离页面最近的值优先。
只给某些页面开评论。全站关掉但保留完整仓库配置,再让选中的页面显式打开:
只关掉某些页面。全站开着,让不适合讨论的页面退出:
整个栏目统一设置用 cascade。本站在 content/docs/_index.zh.md 的 cascade 里写了 comments: true,本页底部因此有一个真实的 giscus 评论区。
站点同时配了 services.disqus.shortname 时,giscus 优先:giscus 生效即抑制 Disqus,comments: false 同时关掉两者,giscus 必填键不全则告警跳过、由 Disqus 兜底。
多语言文案
giscus 的界面语言自动跟随当前 Hugo 语言:简体、繁体、香港繁体分别映射到对应的 giscus locale,不支持的语言回退英文。只有自动选择不合适时才显式设 lang。
需要翻译的是 OINK 一侧的两句文案:评论区的无障碍标签与加载失败提示。它们按语言配置,与全局仓库配置合并:
语言层只需要写差异部分,repo / repoId / category / categoryId 留在 params.comments 里就够了。
跟随深浅色
theme: auto 时,giscus iframe 跟随 OINK 的深浅色切换按钮和浏览器的 prefers-color-scheme,读者切换主题时评论区一起变。
需要更贴合站点配色时,用 lightTheme / darkTheme 分别指定两套 giscus 主题,取值是 giscus 内置主题名或站点自己托管的 CSS。本站用的是后者:
theme 写成固定主题名时不再跟随切换。
giscus 的 iframe 从 giscus.app 加载,要读站点上的这个 CSS 文件需要 CORS 允许。本站在 hugo.yml 的 server.headers 里给本地预览加了 Access-Control-Allow-Origin: '*';线上由托管商的响应头配置决定。
隐私与 CSP
- OINK 不会索取或保存读者的 GitHub 密码与访问令牌,登录与发帖全程在 giscus / GitHub 一侧完成。
- 评论初始化脚本是主题自带的同源资源,只加入启用了评论的页面,未开评论的页面没有这段脚本。
loading: lazy时,读者滚动到评论区附近才加载 iframe。- 站点有严格的内容安全策略时,
script-src和frame-src都要放行 giscus,合并进现有策略而不是替换其它指令(总则见内容安全策略):
外部脚本加载失败或没能创建 iframe 时,OINK 结束加载状态并在实时状态区域显示 errorMessage,不会让页面停在「加载中」。
验证
然后逐项确认:
- 打开一个应该有评论的页面,页面底部出现 giscus,显示「使用 GitHub 登录」,界面语言是当前页面的语言。
- 切换 OINK 的深浅色,评论区跟着变(
theme: auto时)。 - 打开设置了
comments: false的页面,确认那里既没有 giscus 也没有其它评论组件。 - 发一条测试评论,回到 GitHub 看指定分类下是否出现了对应的 Discussion,并且能在 GitHub 上管理。
首次评论或表态创建 Discussion 之前,浏览器控制台提示「找不到 Discussion」是正常现象。
出问题时按这个顺序查:构建日志里的 WARNING(四个必填键)→ params.comments.enable 与 type → 页面 front matter 的 comments → 仓库是否公开、Discussions 是否开启、giscus App 是否安装 → 浏览器控制台与响应头(CSP 是否拦了 giscus.app)。找不到已有评论线程,先恢复原来的 mapping 和页面路径。
相关
4 - 分析与 SEO
主题默认不加载任何分析、表单或广告脚本,不配置就没有对外请求。接入需要显式配置,并把这条外部数据边界写进站点的隐私说明。SEO 一侧相反:canonical、hreflang、robots meta、Open Graph 与 Twitter 卡片由主题逐页生成,需要你做的是把 baseURL 与每页的 description 写对。
接 Google Analytics
用 Hugo 内置的服务配置,填 GA4 的 measurement ID:
主题只在 production 环境渲染这段脚本(hugo 构建默认 production,hugo server 默认 development)。本地预览与预览部署因此不上报数据,不需要另加开关。
不要同时设置已经弃用的顶层 googleAnalytics 键。不需要分析时删掉整段配置,不要填一个假 ID。
配上之后,页面浏览量与事件会发给 Google。严格的同源内容安全策略也需要为它放行,见内容安全策略。这是站点决策,不是主题默认。
接其它分析服务
Plausible、Umami、Matomo 这类服务只要求插入一段脚本。主题提供两个注入点,在站点仓库里建同名文件即可,不用改主题:
layouts/_partials/hooks/head-end.html,- 分析脚本、cookie 同意脚本、主题没提供的 meta 标签
layouts/_partials/hooks/body-end.html,- 只影响交互、不影响首屏的第三方代码
hugo.IsProduction 这一层不要省:没有它,每个人的本地预览都会向你的统计上报数据。
这是有意的:cookie 同意脚本必须先于分析脚本运行,才能真正拦住它。
「这篇文档解决了你的问题吗」反馈组件是另一件事:默认关闭,不发网络请求,配置见仓库与页面信息。
页面描述
<meta name="description"> 按这个顺序取值,取到第一个非空的就停:
- 页面 front matter 的
description - Hugo 计算出的页面摘要(
.Summary) - 站点配置里的
params.description
每页写一句 description 是唯一需要作者做的 SEO 动作。它同时用于三处:搜索引擎的摘要、栏目首页的卡片副标题、站内搜索的结果预览。
多语言站点要给每种语言各写一句,不要把英文描述抄到中文页上。站点级默认值也是分语言的:
canonical 与 hreflang
主题为每个页面输出一条 canonical 和一组 hreflang 备用链接,不需要配置:
hreflang 的语言代码来自各语言的 locale(本站是 en-US / zh-CN),链接来自 Hugo 的译文关系。上面英文那一条指向站点首页而不是对应的英文页:本页没有英文对等文件,Hugo 找不到译文时回退到目标语言首页。这是预期行为,也可以用来判断译文关系有没有被 Hugo 认出来。
canonical 由 baseURL 拼出。baseURL 配错时 canonical 会把搜索引擎指向不存在的地址,比构建失败更难发现。上线前照发布上线的验收清单查一遍。
多语言的完整配置在多语言。
社交卡片
主题调用 Hugo 内置的 Open Graph 与 Twitter 卡片模板,标题、描述、URL、语言、站名都是自动的:
要让分享出去的链接带图,在 front matter 里给 images:
给全站一张兜底图就把同样的键写进 params:
有图时 twitter:card 从 summary 变成 summary_large_image,并多出 og:image 与 twitter:image 两条。本站两处都没有设置,上面的渲染结果里因此看不到图片相关的标签。
站点地图
Hugo 自动生成,多语言站点生成的是一个索引:
站点级默认值和页面级覆盖都是 Hugo 原生的:
changefreq 与 priority 是提示不是承诺,搜索引擎可以忽略。值得做的是发布前确认草稿、私有内容与非规范副本没有进入站点地图,并且每种语言的那份都生成了。
robots.txt 与不收录
Hugo 只在站点配置里打开开关时才生成 robots.txt:
主题提供的模板按构建环境给出两种结果,不需要你写内容:
页面里的 robots meta 跟着同一个开关走:production 且不是打印输出时是 index, follow,否则是 noindex, nofollow。预览部署不要用 --environment production 构建,非 production 自带不收录的行为。
主题没有按页 noindex 的开关。某一页不该被收录时,可靠的做法是不发布它(draft: true,或用 Hugo 的 _build 选项)。既要发布又不想被收录,就用 head-end.html 钩子自己输出;主题已经输出了一条 robots meta,两条同时存在时如何合并由搜索引擎决定。
收录检查
上线一两周后,按这个顺序确认搜索引擎看到的东西和你以为的一致:
- 抓取权限:访问
<baseURL>/robots.txt,确认是Allow: /而不是Disallow: /。 - 页面清单:访问
<baseURL>/sitemap.xml,点进语言子地图,看页面数量对不对。 - 收录数量:在搜索引擎里查
site:你的域名,数量级对得上就行,不必逐页核对。 - 规范地址:搜索结果应当落在 canonical 指向的 URL 上,而不是带
?参数或旧域名的版本。 - 主动提交:在 Google Search Console / Bing Webmaster Tools 里加上站点并提交
sitemap.xml的地址,比等着被爬快。
搜索元数据补不了内容本身的问题:单薄、重复、过时的页面,写再好的 description 也一样。
验证
在产物里查这几项:
浏览器里再确认一次:打开一个代表性页面,看开发者工具的网络面板,没接分析的站点不应有指向第三方域名的请求。
相关
5 - 版本升级
升级 OINK 是换一个固定的模块版本,再确认站点仍能零告警构建。内容多数不用改;需要改的场景(0.4 的 shortcode 换成 v5 的 Markdown 原生形态)有一个可以干跑的迁移工具,不必手改几百个文件。
升级会改变渲染结果。先建一个升级分支再动手,回退的代价就是丢弃一个分支。
先看发布注记
每个版本的变更、破坏性改动与升级要点都写在发布注记里,升级前先读一遍目标版本那篇:
- 本站的 项目博客 里的 release 系列
- GitHub 上的 Releases 页面
注记说明这次要不要改内容、有没有配置键被移除、默认行为有没有变化。跳过这一步的代价是升级后对着一个变了样的页面猜原因。
升级 Hugo Module
生产站点固定发布标签或不可变 commit,不跟随分支,也不用 @latest:
最后一条要能看到解析结果是那个标签本身,而不是伪版本(v0.0.0-2026...-abcdef)或 main。固定的版本落在 go.mod 里,跟着代码一起提交:
make dev 和 make check 会仅对当前命令设置 HUGO_MODULE_REPLACEMENTS,使用同级的主题 checkout。判定某个发布标签是否可用时使用不带替换的 make build,否则验证的是本地那份代码。
其它安装方式各一句。Git submodule:用 git submodule update --remote themes/oink 拉到新 ref,再提交 submodule 指针。离线归档与克隆:把 themes/oink/ 整个换成新版本的解压结果,确认 theme: 的值仍与目录名一致。三种方式的取舍见从零建站与其它安装方式。
升级后必做
三件事一起做了:清掉可能过期的缓存、用新版本重新构建、把任何告警变成失败。
--logLevel info 是为了看见 Hugo 的弃用提示。Hugo 的弃用分两级:先是 WARN 级提示(仍可使用),下一个版本变成 ERROR(构建失败)。带上 --panicOnWarning 相当于提前一个版本发现它们,把修复的时间留给自己。
构建通过之后,人眼再过一遍:首页、一个文档页、一个博客页、404、两种语言、两种配色、打印视图,以及站点自己定制过的地方。
内容迁移工具
0.4 的一批 shortcode 在 v5 里换成了 Markdown 原生形态。主题仓库带了一个只依赖 Python 标准库的工具做这件事:
用它的时候记住四条:
- 干跑是默认行为,只有
--write才落盘。先干跑,读 diff,再写。 - 重跑一次应该零改动。第二次
--write还报改动,说明有转换不收敛,停下来看那几个文件。 - 围栏里的文字不动,文档站里示范旧写法的代码块不会被误伤。
- 表达不了的构造原样保留,并附
file:line与原因列出,作为手工处理清单,不是失败。
只想先转某一类时用 --only,键名见下表最后一列:
改完重新构建一次(带 --panicOnWarning),并逐页看渲染结果:工具保证语法正确,不保证语义符合预期。
0.4 → v5 语法映射
{{%/* alert color= title= */%}}、{{%/* details */%}}、{{%/* pageinfo */%}}、手写<details><summary>,callout{{</* tabpane */>}}+{{%/* tab header= */%}}、{{</* code-group */>}}+{{</* code-tab */>}},tabs{{</* filetree */>}}与filetree/folder、filetree/file,filetree{{</* gallery */>}}与gallery/image,gallery{{</* echarts */>}}、{{</* infographic */>}},datafencedoc-cards/doc-card、nav-cards/nav-card、card/cardpane、doc-carousel,cards{{</* imgproc */>}}、{{</* image */>}},image{{</* readfile file= */>}},include围栏属性,{filename="x"}fencetitle{{</* badge outline= */>}},badge{{</* example */>}}+ 围栏、{{</* book-figures kind="tbl" */>}},eg{{%/* _param x */%}}、iframe、conditional-text、blocks/*、netlify、不带 kind 的xref,reportonly
每个新写法长什么样、有哪些参数,去组件里对应的那一页。
从 Docsy 迁移
OINK 是 Docsy 的硬分支:内容模型、td- 命名、Sass 变量、大部分 front matter 都还在。迁移的核心动作是删掉站点里复制的公共外壳,让主题的实现接管,而不是重写正文。
-
固定目标版本。在
go.mod里换成 OINK 的发布标签,或者用完整的版本化归档。评估期可以用不提交的go.work指向本地 checkout。 -
清点覆盖项。把
layouts/、assets/、static/下每个站点级文件归成四类:公共外壳的副本(验证后删)、OINK 已提供的组件(删或机械重命名)、品牌定制(保留,缩到最小 hook)、业务专属数据与交互(留在站点)。按引用关系删,不要清空layouts/:首页、下载页这些地方可能还在调用你要删的 partial。 -
搬配置。
title、languages.*、github_repo、github_branch、page_width、params.ui.*全部留在原来的语义位置,OINK 没有另起一套命名空间。搜索与 Logo 这类只要打开对应的键:hugo.ymlDocsy 的驼峰式检索键在 OINK 中已改名:
offlineSearch、offlineSearchIndex、offlineSearchMaxResults、offlineSearchOnServe、offlineSearchSummaryLength一律改为下划线形式。这一步要自己盯着改——那份「中断构建并报出新键名」的迁移登记表已经删除,旧键现在只是一个没人读的键,检索会一声不响地保持关闭。 -
字体与样式的兼容点。站点的
assets/scss/_variables_project.scss里那些 Docsy Sass 变量仍然生效,会作为字体角色的种子值,不用为了升级把它们删掉:$td-fonts-serif、$font-family-sans-serif、$headings-font-family、$font-family-code各自喂给对应的字体角色。Docsy 的 Google Fonts 开关$td-enable-google-fonts、$td-google-font-name与$td-web-font-path主题已不再读取,留在文件里不影响构建,也不产生任何效果:OINK 自带 Inter、Chakra Petch 与 IBM Plex Mono,任何预设都不向 Google Fonts 发请求。想换字体走 token 层,见品牌外观。 -
换 shortcode。Docsy 的
alert、pageinfo、tabpane、card系列在 v5 里都有对应形态,用上面的迁移工具批量转,--only一类一类来。 -
一次删一组,每组构建一次。在临时副本里演练,记下主题 commit、Hugo 版本、删了哪些文件、产出多少个 HTML;确认等价之后再在生产分支上重做一遍。
第二步里「验证后删」的那一类,通常是这些文件:
layouts/baseof.html与公共的 docs / blogbaseof*.html;- navbar、footer、sidebar、TOC、search、head CSS 的 partial 及其对应 hook;
- 旧的品牌文档外壳 partial;
asciinema、echarts、infographic、doc-carousel、details、tab/tabpane、card 与param的 shortcode 副本;- 只服务于上述实现的 JavaScript、Lunr 副本、轮播代码与 SCSS;
- 不再被任何站点资源需要的 PostCSS 与 Autoprefixer 步骤。
删完之后有两类问题会浮出来。
站点自己的脚本报 $ is not defined:主题不带 jQuery,它以前由 Docsy 在每个页面的 <head> 里加载。主题的功能都不需要它,仍然需要的站点自己引入:
用 Docsy blocks/* 搭的首页在 v5 构建失败,报 template for shortcode "blocks/cover" not found:主题没有这一组 shortcode。改用 data/home/<语言>.yaml 的首页分区,或给页面写 layout: landing,见首页与落地页。
从 0.4 升级的要点
0.4 改了几个默认行为。升级后发现页面多了或少了东西,先看这几条:
-
顺序翻页默认开启。
docs、book、blog页尾都有上一页 / 下一页;文档沿侧栏树走,博客沿时间走。刻意不属于任何序列的页面用pager: false退出。 -
顶栏在所有布局上都显示。紧凑状态只有一行图标导航,没有第二套移动端手风琴菜单,依赖旧移动菜单的本地脚本与测试要删掉。整个分区不要顶栏时用 cascade 里的
navbar_enabled: false。 -
页脚默认
fat且全站生效。只接受fat/slim/none;页脚数据必须放在data/footer/<语言>.yaml(单语言站点用data/footer.yaml),data/home里残留的footer键会让构建失败并提示新位置。 -
单键导航默认开启:
/打开完整搜索,\只进命令模式。培训材料里描述旧行为的地方要改。页面操作也挪到了面包屑旁边的拆分按钮上。 -
代码块的 DOM 变了。
.td-code外壳套在原来的.highlight外面(.highlight与.chroma都保留),站点 CSS 里.td-content > .highlight这类直接子选择器要改成后代选择器.td-content .highlight。 -
两个 ICP 页脚参数被移除:
footer_icp与footer_icp_url换成一个支持行内 Markdown 的字符串。hugo.yml -
数学公式要站点自己开 passthrough。Hugo 不会合并主题的
markup配置,用\(…\)、\[…\]、$$…$$的站点必须在自己的hugo.yml里启用 goldmark passthrough 扩展,见公式。
验证
升级不是「构建通过」就算完,按表面分别看:
文档 / Book- 侧栏顺序、翻页、标题、页面操作、编号与交叉引用
博客- 时间顺序翻页、RSS 归属、顶栏与页脚
首页 / Landing- 无 JS 时的内容、紧凑菜单、打印
发布页- 推导出的下载 URL、校验和、发布状态
组件- 站点用得最多的那几个组件各找一页看渲染结果
无障碍- 纯键盘走一遍、焦点顺序、两种配色、强制颜色模式
部署- 站内链接与资源都保留了 base path 前缀
本站的完整门禁是:
其它站点跑等价的构建、链接、输出与浏览器检查即可,细节见排错与检查。
源码可构建、标签已签名并能通过 Go proxy 解析、站点已固定该标签、线上已部署,这是四件事,要分别记录。别用一次绿色的本地构建代替它们。
最后一步在真实环境上做:先部署一份预览,在真实 URL 上验证页面与浏览器的网络请求,评审通过再合并,合并后在生产上做一次冒烟测试。
回滚
回滚的是版本固定,不是工作树:
三条原则:
- 保留升级前的模块固定、站点 commit 与已知可用的部署产物,回滚时三者一起恢复。
- 不要只回滚一部分。给新主题塞回几个旧布局副本,会得到一个比任何完整版本都更难诊断的混合状态。
- 升级分支与验收证据都留着。回滚是为了先恢复线上,不是丢掉已经做完的工作。
线上产物本身的回滚(重新发布上一个部署)见发布上线。
相关
- 发布上线 — 部署产物的回滚
- 排错与检查 — 升级后构建报错怎么读
- 本地预览 — 清缓存与
go.work工作区 - 从零建站与其它安装方式 — 四种安装方式的取舍
- 组件总览 — v5 每个组件的新写法
6 - 排错与检查
出问题时先做一次干净的生产构建,从第一条错误开始看,后面的多半是级联结果:
日志里出现 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤,说明配置里混进了上游 Docsy 的流程。OINK 消费端的构建只有一条 Hugo 命令。
下面四张表按「症状 → 原因 → 修法」组织,找到症状那一行即可,不必从头读。
构建
| 症状 | 原因 | 修法 |
|---|---|---|
| 构建报要求更高的 Hugo 版本 | 装的是标准版而不是 Extended,或版本低于 0.160.1 | hugo version 输出里必须有 extended。多个 Hugo 共存时先查 PATH 与版本固定配置,而不是再装一份 |
module "github.com/pgsty/oink" not found |
主题没解析出来 | Hugo Module:看 hugo mod graph、go.mod、go.sum,以及有没有多余的 workspace / replace。submodule:CI 有没有在 Hugo 之前跑 git submodule update --init。归档 / 克隆:theme: 的值要与 themes/ 下的目录名一致 |
| 模块下载卡住或超时 | Go 的模块代理不通 | Hugo 通过 Go 拉模块,所以走 GOPROXY。国内网络可以 export GOPROXY=https://goproxy.cn,direct;隔离环境改用离线归档或提交 themes/oink/ |
页面上出现 {.cards}、{.steps}、{caption=…} 这类原样文字 |
站点没开 goldmark 的块级属性 | 站点的 hugo.yml 里必须有下面那三项,主题的 markup 配置不会被 Hugo 合并进来 |
图片带属性行时被包进了 <p>,图注没生效 |
缺 wrapStandAloneImageWithinParagraph: false |
同上,三项一起加 |
| 行内 HTML 被转义成文字 | 缺 renderer.unsafe: true |
同上 |
\(…\) $$…$$ 原样显示 |
站点没启用 goldmark passthrough | 见公式;math: true 不是启用开关 |
shortcode "tabs" must be closed or self-closed |
有 {{< tabs >}} 没写对应的 {{< /tabs >}} |
报错里带 文件:行:列,去那一行补上闭合标记 |
template for shortcode "tabs" not found |
正文里写了一个不存在的 shortcode,或引用 shortcode 语法时没有转义 | 文档里讲解 shortcode 语法时必须转义:在开标记与闭标记的内侧各加一对 /* 与 */,Hugo 才会把它当文字而不是调用。名字打错就改回正确的名字 |
... attributes: unknown attribute "witdh" at ... |
属性行里的键拼错或不被允许 | 属性行只接受该组件的允许键、class、data-*、aria-*;style 与 on* 一律构建失败。允许的键就写在报错括号里 |
shortcode "field": unsupported parameter "colour" at ... |
shortcode 参数名不对 | 组件参数——shortcode 参数与属性行的键——一律构建失败,不做静默降级。报错格式固定为「哪个 shortcode → 哪个参数 → 哪个文件的第几行」,照着改即可 |
invalid params.ui.page_width "widee" (allowed: normal | wide | full) -- using "normal" |
配置或 front matter 的取值,不在允许集合里 | 配置类的错误降级而不中断,一个笔误不会让 hugo server 下每个 URL 都返回 500。消息里带键名、收到的值和实际用的回退值。构建加 --panicOnWarning,它就上不了线 |
| 某个页面设置不生效,也没有任何提示 | 键写在了 front matter 的 ui: 段里 |
页面键写在 front matter 顶层,键名是站点键去掉 ui.。写进 ui: 段的键没有人读,也没有人报错,见页面参数 |
| 构建通过但线上少东西 | 有 WARNING 没人看 | 构建命令加 --panicOnWarning。非法配置取值、giscus 必填键缺失、不支持的 comments.type、Hugo 的弃用提示都只是告警 |
那三项 goldmark 配置:
两个最常见的 shortcode 报错长这样,注意结尾的 文件:行:列:
语言
| 症状 | 原因 | 修法 |
|---|---|---|
| 译文页面不出现 | 四种可能,按顺序查 | ① hugo.yml 里有 languages.zh 且设了 weight;② 文件名是 page.zh.md,zh 必须小写;③ 译文 front matter 没有 draft: true,date 不在未来;④ 影响路由的元数据与源文件一致 |
| 语言切换跳到了首页 | Hugo 没找到对应译文 | 这是设计行为:找不到译文就回退到目标语言首页。要跳到对应页面,需要那个译文文件确实存在 |
| 锚点链接打开了页面却不定位 | 译文标题文字不同,自动生成的 ID 也不同 | 在译文标题上显式写英文 ID:## 安装 {#installation}。标题里含 shortcode 或行内 HTML 时不要凭文本猜 ID,去看英文页渲染出来的 HTML |
| 菜单 / 首页分区没翻译 | 这些不在页面里,在配置和数据文件里 | 菜单在 languages.<lang>.menus,首页分区在 data/home/<lang>.yaml,界面字符串在 i18n/<lang>.yaml,见多语言 |
中文页 hreflang 指向英文首页 |
该页没有英文对等文件 | 补上英文页,或接受这个回退:它同时是「Hugo 有没有认出译文关系」的探针 |
搜索
| 症状 | 原因 | 修法 |
|---|---|---|
| 搜索框有但一直没结果 | 索引没生成 | params.offline_search: true 之后,产物根目录下应该有 offline-search-index.<语言>.json,每种语言一份。没有就是没开 |
| 索引文件请求 404 | baseURL 不对 |
子路径部署下 baseURL 配错是索引 404 最常见的原因。先在浏览器网络面板看它去哪里取索引,见发布上线 |
hugo server 下搜不了,构建出来就正常 |
站点把预览期的索引关掉了 | params.offline_search_on_serve 默认为 true,预览与线上行为一致;配置里显式写成 false 时预览不生成索引,删掉或改回 true |
| 中文搜不到 | 多数不是分词问题 | 中文查询走主题的 CJK 子串回退。先确认那个中文页面的内容进了中文索引(打开 offline-search-index.zh.json 查一下),再看分词 |
| 新页面搜不到,旧页面正常 | 索引是构建产物 | 重新构建。hugo server 下改了页面要等它重建完 |
params.search.algolia requires explicit appId, apiKey, and indexName values |
Algolia 三个键没配全 | 三个键必须显式给全,主题不会替你用别的项目的 DocSearch 凭据。不用 Algolia 就把这段配置删掉 |
| 命令面板搜不到内容 | 它与全文检索是两件事 | 索引不可用时命令面板仍然能打开,只是提示索引不可用,页面操作与命令照常,见命令面板 |
平台
| 症状 | 原因 | 修法 |
|---|---|---|
| GitHub Pages 上页面 404 或样式全丢 | 项目站点的 URL 带仓库路径,baseURL 没带 |
用工作流里的 --baseURL "${{ steps.pages.outputs.base_url }}/",别手写。完整工作流见发布上线 |
| GitHub Pages 上「最后修改时间」「贡献者」全空 | checkout 是浅克隆 | actions/checkout 加 fetch-depth: 0:enableGitInfo 要读完整历史 |
| Cloudflare Pages 构建报 Hugo 版本太低 | 构建镜像的默认 Hugo 低于主题要求 | 在 Production 和 Preview 两个环境都设 HUGO_VERSION,并设 SKIP_DEPENDENCY_INSTALL=1 |
| 托管商构建时拉不到主题 | 构建环境没有 Go | Hugo Module 需要 Go。平台不提供就改用 submodule 或把 themes/oink/ 提交进仓库 |
| CI 上构建结果和本地不一样 | go.work 参与了 CI 构建 |
CI 里设 GOWORK: off 与 HUGO_MODULE_WORKSPACE: off,让它只认 go.mod 里固定的版本 |
| 预览部署被搜索引擎收录了 | 预览也用了 production 环境构建 | 预览构建不要带 --environment production,非 production 自带 noindex 与 Disallow: /,见分析与 SEO |
| macOS 报打开文件过多 | 实时预览监视的文件超过了 shell 限制 | 先把生成目录与无关目录排除出监视范围,这通常才是根因;再考虑 ulimit -n |
| WSL 下很慢或漏掉改动 | 跨 Windows 挂载点工作 | 让 Hugo 处理 Linux 文件系统里的路径,跨文件系统的变更通知和权限行为会让实时重载失效 |
| 缺 Bootstrap / Font Awesome / Lunr / Mermaid 之类资源 | 发行物不完整 | 不要用 CDN URL 掩盖。确认 assets/third_party/、assets/js/third_party/、static/webfonts/、VENDOR.json 都在;确实缺就重新获取同一个固定版本 |
站点自带检查
除了构建本身,站点还可以自己跑这几项。前两条任何 OINK 站点都能用,后面几条是本仓库的 npm 脚本,其它站点跑等价的检查即可。
零告警构建,- 重复输出路径、参数非法、外部集成配置不全
输出信任检查,- 四种输出里的每个
href/src都是站内相对或http(s)/mailto/tel;没有javascript:URL、没有行内on*事件处理器;跨站的<iframe><script><img>等要显式加--third-party才放行 翻译对等,- 每个英文页有没有中文对等页,以及渲染后的标题 ID 是否逐一对齐;锚点链接错位在这里暴露
完整门禁,- 下面六项串起来跑
npm test 里的六项各管一段:
test:base— 先构建一次,再跑 Markdown 风格、翻译对等、渲染后的 Markdown 与链接检查。test:hugo-build— 构建断言:博客元数据、RSS、内容组件、构建过程零弃用提示。test:md-output— Markdown 与llms.txt输出的 golden 比对,字节级。改了组件的 Markdown 形态就会在这里挂。test:alt-site— 用tests/fixtures/*.yml里的替代配置各构建一次,确认不同配置组合都能起来。test:favicons— head 输出的 golden 比对。test:release-pin-contract— 站点公告的版本与go.mod固定的版本是否一致。
浏览器行为另开一套:npm run test:browser 依次跑 Playwright 的无障碍(axe WCAG AA)、响应式外壳、键盘导航、内容组件、代码块与场景组件六个套件。
check-output-security.py 在主题仓库里它在主题仓库的 bin/ 下,是产品级的信任检查,任何 OINK 站点都可以跑,不依赖站点的测试框架。克隆主题仓库后指向自己的 public/ 即可,参数与用法见断网构建验证。
诊断习惯
上面的表覆盖不到的问题,按这几条挖:
- 用固定的 Hugo Extended 版本复现,不在版本浮动的环境里判断。
- 清掉
public/与resources/_gen再重建,排除陈旧缓存。 - 对比开发与生产两套配置层,很多只在线上出现的问题是环境差异。
- 看第一条错误,不是最后那条。
- 用一个最小页面区分「主题行为」和「站点覆盖」:把可疑内容单独放一页,站点覆盖分批重新启用,定位到具体那一项。
- 看故障页面的浏览器控制台与网络面板,尤其是 404 的资源路径。
求助渠道
开 issue 时带上这几样,能省掉一轮来回:Hugo 版本(hugo version 完整输出)、主题版本(hugo mod graph | grep oink)、第一条完整错误、能复现的最小页面或最小站点。
- 主题与文档的问题:https://github.com/pgsty/oink/issues
- 本站内容的问题:https://github.com/pgsty/oink.pgsty.com/issues
- 上游 Docsy 的兼容性讨论:https://github.com/google/docsy/discussions