跳转到主要内容

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

返回本页常规视图.

维护管理

站点从本机到线上的运维事项:本地预览、发布上线、评论、分析与 SEO、版本升级与排错。

本栏目覆盖内容写完之后的运维事项:在本机预览、构建并部署产物、接入评论与分析、跟随主题版本升级、故障定位。前面五个栏目决定站点的外观与内容,这一栏决定站点能否构建、部署在哪、出问题如何排查。

按任务导航

你要做的事 去哪
在本机看到改动 本地预览
构建出能部署的 public/ 本地预览
部署到 GitHub Pages / Cloudflare / Netlify 发布上线
部署到 example.com/docs/ 这样的子路径 发布上线
让读者在页面底部留言 启用评论
接 Google Analytics 或自建统计 分析与 SEO
让搜索引擎正确收录 分析与 SEO
升到新版主题 / 从 Docsy 或 0.4 迁移 版本升级
构建报错、搜不到、页面 404 排错与检查

1 - 本地预览

用 hugo server 在本机预览改动,用 hugo –panicOnWarning 构建可部署的 public/,不需要 Node 与 CDN。

两条命令覆盖日常工作:hugo server 在本机预览改动,hugo 产出可以部署到任何静态托管的 public/。前提是本机安装了 Hugo Extended(不低于 0.160.1);用 Hugo Module 引入主题时还需要 Go。构建不依赖 Node.js、npm 与 PostCSS,它们只服务于本仓库自身的回归检查。

预览服务器

在站点根目录(hugo.yml 所在的目录)执行:

终端
hugo server

打开 http://localhost:1313/。保存文件后 Hugo 重新构建并刷新浏览器,切换 Git 分支同样触发重建。首次启动较慢:用 Hugo Module 引入主题时,Hugo 要先通过 Go 把模块下载到缓存,之后的启动都走缓存。

常用开关

-D / --buildDrafts , default
draft: true 的页面也构建出来
-F / --buildFuture , default
date / publishDate 在未来的页面也构建出来
-E / --buildExpired , default
expiryDate 已过的页面也构建出来
--disableFastRender , default
每次改动都整站重渲染,不用增量
-M / --renderToMemory , default关(写磁盘)
只在内存里渲染,不落 public/
-N / --navigateToChanged , default
保存哪个页面,浏览器就跳到哪个页面
--bind , default127.0.0.1
监听地址;要让局域网或容器外访问就设 0.0.0.0
-p / --port , default1313
监听端口
--minify , default
预览也压缩输出,用来复现生产环境下的渲染
--printPathWarnings , default
有两个页面写到同一个目标路径时告警

本站开发时用的组合是:

终端
hugo server -DFE \
  --disableFastRender --renderToMemory --minify \
  --printPathWarnings --logLevel info

-DFE-D -F -E 的合写,草稿、未来与过期页面一并构建,写作时新建的页面才可见。

改动没有生效

Hugo 默认开启快速渲染(fast render),只重建它判定受影响的部分。修改布局、配置、data/ 或被 include 引用的文件时,增量判定可能不准,页面看起来没有变化。三步排查:

  1. --disableFastRender 重启,看是否恢复。
  2. 硬刷新浏览器(Cmd/Ctrl + Shift + R),排除浏览器缓存。
  3. 仍未恢复则清缓存后重启。

从其它设备访问

hugo server 默认只监听 127.0.0.1,其它设备访问不到。要在手机或另一台机器上预览:

终端
hugo server --bind 0.0.0.0 --port 1313 --baseURL http://192.168.1.10:1313/

--baseURL 必须写成对方可访问的地址,否则页面能打开,但 CSS 与搜索索引这类走绝对路径的资源会指向 localhost

生产构建

部署产物用 hugo 构建,不用 hugo server

终端
hugo --gc --minify --printPathWarnings --panicOnWarning

产物写入 public/,该目录可以脱离源码树独立部署。四个开关各管一件事:

--gc
构建后清掉 resources/_gen 里不再被引用的缓存资源
--minify
压缩 HTML、CSS、JS 与 XML 输出
--printPathWarnings
两个页面撞到同一个输出路径时告警,多语言站点最常见的静默错误
--panicOnWarning
遇到第一条 WARNING 就让构建失败

--panicOnWarning 需要单独说明。OINK 的多数降级路径是告警而不是报错:giscus 必填键缺失、params.comments.type 取了不支持的值、Hugo 弃用的配置键,都只打一条 WARNING 然后跳过。CI 日志通常无人逐行阅读,这些问题会带到线上。把这个开关写进构建命令,等于要求零告警才算构建通过。

本站 CI 的构建步骤(.github/workflows/pages.yml)是 hugo --cleanDestinationDir --gc --minify --environment production --printPathWarnings --panicOnWarning,任何一条告警都会让部署停在构建阶段。

baseURL 与构建环境

baseURL 写在 hugo.yml 里,也可以在命令行覆盖:

hugo.yml
baseURL: https://oink.pgsty.com
终端
hugo --minify --baseURL "https://example.com/docs/"

部署到子路径时 --baseURL 必须带上那段路径,细节见发布上线

构建环境用 -e / --environment 选择,hugo 默认 productionhugo server 默认 development。这个选择在 OINK 里有三处可见后果:

  • production 下才输出 <meta name="robots" content="index, follow">,其它环境输出 noindex, nofollow
  • productionrobots.txtAllow: /,其它环境是 Disallow: /
  • production 下才渲染 Hugo 的 Google Analytics 模板,静态资源也才做指纹与 SRI。

预览部署(PR preview、staging)用非 production 环境构建,产物自带不被搜索引擎收录、不上报分析的行为:

终端
hugo --minify --environment staging --baseURL "$PREVIEW_URL"

容器内预览

容器不是必需的。两种情况适合用容器:团队需要固定工具链版本,或不希望在每台开发机上安装 Hugo。

Dockerfile
FROM debian:bookworm-slim

ARG HUGO_VERSION=0.164.0
ARG GO_VERSION=1.26.6
ARG TARGETARCH

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl git \
    && curl -L -o /tmp/hugo.deb \
      "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.deb" \
    && apt-get install -y /tmp/hugo.deb \
    && curl -L -o /tmp/go.tgz \
      "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" \
    && tar -C /usr/local -xzf /tmp/go.tgz \
    && rm -rf /var/lib/apt/lists/* /tmp/hugo.deb /tmp/go.tgz

ENV PATH="/usr/local/go/bin:${PATH}"
WORKDIR /src
EXPOSE 1313
ENTRYPOINT ["hugo"]
CMD ["server", "--bind", "0.0.0.0", "--disableFastRender"]
终端
docker build -t oink-hugo .

# 预览:把站点源码挂进去,顺带挂上 Go 模块缓存
docker run --rm -it -p 1313:1313 \
  -v "$PWD:/src" \
  -v "$HOME/go/pkg/mod:/root/go/pkg/mod" \
  oink-hugo

# 生产构建:覆盖默认的 server 命令
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/src" \
  oink-hugo --gc --minify

镜像里装 Go 的原因:用 Hugo Module 引入主题时,Hugo 需要 Go 解析并下载模块。用 submodule、离线归档或直接克隆的站点可以去掉 Go,镜像会小很多。

不要让 root 写 public/

容器里的进程默认是 root,生成的 public/ 属于 root,宿主机上删不掉。共享环境里用 --user "$(id -u):$(id -g)" 映射用户 ID(上面的生产构建命令已经带了)。

镜像不需要 Node.js、npm 与 PostCSS,也不应出现拉取远程浏览器资源的步骤。网络隔离环境需要预先镜像基础镜像与这两个软件包。

清缓存

Hugo 的中间产物分三处,从轻到重依次清:

public/ , 内容上一次的构建产物
删了页面但线上还在;或用 hugo --cleanDestinationDir 让构建自己清
resources/_gen/ , 内容处理过的图片与编译出的 CSS
换了图片处理参数、换了字体或主色,页面还是旧样子
hugo mod clean , 内容Hugo Module 缓存
换了主题版本但解析出来还是旧的;加 --all 清整个模块缓存
终端
rm -rf public resources/_gen
hugo mod clean          # 只清当前项目用到的模块
hugo mod clean --all    # 清整个模块缓存,下次构建重新下载

public/resources/ 都应该写进 .gitignore,不要提交生成产物。

与主题一起改

同时修改主题与站点时才需要这一节。用 HUGO_MODULE_REPLACEMENTS 把模块临时指向本地 checkout,go.mod 保持不变:

终端
HUGO_MODULE_REPLACEMENTS='github.com/pgsty/oink -> /absolute/path/to/oink' hugo server

本站的 Makefile 封装了这几条命令,要求主题 checkout 在同级目录 ../oink

Makefile 目标
make dev     # 替换为 ../oink 的开发服务器
make check   # 替换为 ../oink 跑完整回归套件(npm test)
make build   # 用 go.mod 里的版本构建
make serve   # 按生产配置起预览服务器
替换只作用于本机

无论用环境变量还是 Go workspace(go work init + HUGO_MODULE_WORKSPACE=go.work),CI 与生产构建都只看 go.modgo.work 记录的是开发机的路径,不能提交。判定一个发布标签是否可用时,去掉替换、用 go.mod 里的版本单独构建一次。

断网构建验证

网络隔离环境的验收要同时覆盖构建阶段与浏览器阶段。六步:

  1. 从一份已校验的主题归档与空的模块缓存开始(hugo mod clean --all)。
  2. 阻断出站 HTTP、HTTPS 与 Go module proxy。
  3. 运行生产构建 hugo --gc --minify --printPathWarnings --panicOnWarning
  4. 浏览产物里两种语言的页面:文档页、博客页、首页、404。
  5. 操作搜索、深浅色切换、图表与内容组件。
  6. 检查子资源来源,确认没有意外的远程主机。

最后一步用主题仓库里的脚本,它不依赖站点的测试框架:

终端
python3 bin/check-output-security.py \
  --public public --base-url https://docs.internal.example.com/

脚本扫描四种输出里的每个 href / src / srcset / poster 与表单 action,要求它们是站内相对路径或 http / https / mailto / tel,并拒绝行内 on* 事件处理器与 javascript: URL。指向别的主机的 <iframe> <script> <link> <img> <video> <audio> <embed> <object> <source> 一律报错,站点确实要嵌入第三方内容时加 --third-party 放行,多域名语言配置用 --allow-host 追加首方主机。

一次通过只证明当次提交与当次环境。每个主题候选版本、每次随附依赖更新之后都要重跑一遍。

验证

一次干净的生产构建应该是这样:

终端
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --panicOnWarning

看到 Total in … 且没有 ERROR / WARNING 才算通过。然后确认:

  • 日志里没有 npm、PostCSS、Autoprefixer 或下载浏览器资源的步骤。出现了说明配置里混进了上游 Docsy 的流程。
  • public/ 下有 sitemap.xmlrobots.txtrobots.txtAllow: /
  • 开了本地搜索的站点,public/ 根下有 offline-search-index.<语言>.json
  • hugo server 打开代表性页面:一个文档页、一个博客页、首页、404,两种语言、两种配色都看一遍。

构建失败或结果不对,去排错与检查

2 - 发布上线

把 public/ 部署到 GitHub Pages、Cloudflare Pages 或任何静态托管:baseURL 配对、内容安全策略、验收清单与回滚。

OINK 站点的产物是一个纯静态目录,任何能托管静态文件的地方都能部署,不需要 Node 运行时、服务端渲染或构建插件。托管商一侧只有三件事:用正确的 Hugo 版本执行一条命令、发布 public/、让 baseURL 与最终访问地址一致。

前提是本机已经能完成零告警的生产构建

确定 baseURL

baseURL 是最常见的故障源,失败方式也隐蔽:页面能打开,但搜索索引 404、页面操作链接指向错误位置、部分资源加载失败。

部署到域名根目录:

hugo.yml
baseURL: https://oink.pgsty.com

部署到子路径(https://example.com/docs/)时,路径必须写进 baseURL

hugo.yml
baseURL: https://example.com/docs/

也可以在构建时覆盖,让同一份源码部署到不同位置:

终端
hugo --gc --minify --baseURL "https://example.com/docs/"
不要用 canonifyURLs 修子路径

Hugo 的 canonifyURLs 默认 false,保持这个默认值。OINK 的模板与内容链接都基于 baseURL 解析:路径不对是 baseURL 不对,打开 canonifyURLs 会把本来正确的相对链接一起改写,让问题更难定位。

判断是否配对,看构建后搜索索引的请求路径:浏览器应当去 <baseURL>/offline-search-index.zh.json 取索引,取到别处就是 baseURL 不对。

选一个托管商

源码托管在 GitHub 时,一份 Actions 工作流就够:构建在 Actions 里执行,产物通过 Pages 部署 API 发布,不需要维护 gh-pages 分支。

把下面的文件提交到仓库:

.github/workflows/pages.yml
 1name: Deploy Oink site to GitHub Pages
 2
 3on:
 4  push:
 5    branches: [main]
 6  workflow_dispatch:
 7
 8permissions:
 9  contents: read
10  pages: write
11  id-token: write
12
13concurrency:
14  group: pages
15  cancel-in-progress: false
16
17env:
18  GO_VERSION: 1.26.6
19  HUGO_VERSION: 0.164.0
20  # 同级 checkout 的 workspace 绝不能参与 CI 构建
21  GOWORK: off
22  HUGO_MODULE_WORKSPACE: off
23  HUGO_CACHEDIR: ${{ github.workspace }}/.hugo_cache
24  GOMODCACHE:
25    ${{ github.workspace }}/.hugo_cache/modules/filecache/modules/pkg/mod
26
27jobs:
28  build:
29    name: Build Pages artifact
30    runs-on: ubuntu-latest
31    steps:
32      - name: Checkout
33        uses: actions/checkout@v7
34        with:
35          fetch-depth: 0
36
37      - name: Set up Go
38        uses: actions/setup-go@v6
39        with:
40          go-version: ${{ env.GO_VERSION }}
41
42      - name: Set up Pages
43        id: pages
44        uses: actions/configure-pages@v6
45
46      - name: Install Hugo Extended
47        run: |
48          curl --fail --location --silent --show-error \
49            --output "${RUNNER_TEMP}/hugo.deb" \
50            "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
51          sudo dpkg -i "${RUNNER_TEMP}/hugo.deb"
52
53      - name: Download Hugo module
54        run: go mod download github.com/pgsty/oink
55
56      - name: Build site
57        run: |
58          hugo --cleanDestinationDir --gc --minify --environment production \
59            --printPathWarnings --panicOnWarning \
60            --baseURL "${{ steps.pages.outputs.base_url }}/"
61
62      - name: Upload Pages artifact
63        uses: actions/upload-pages-artifact@v5
64        with:
65          path: public
66
67  deploy:
68    name: Deploy to GitHub Pages
69    environment:
70      name: github-pages
71      url: ${{ steps.deployment.outputs.page_url }}
72    runs-on: ubuntu-latest
73    needs: build
74    steps:
75      - name: Deploy
76        id: deployment
77        uses: actions/deploy-pages@v5

这是本站正在使用的工作流。几处不能删:

  • fetch-depth: 0 — 站点开了 enableGitInfo 时,「最后修改时间」和贡献者信息要读完整 Git 历史,浅克隆会让它们为空。
  • setup-go + go mod download — Hugo Module 方式引入主题时,Hugo 需要 Go 才能解析模块。用 submodule 安装主题的站点改成 submodules: recursive,用离线归档的站点把 themes/oink/ 提交进仓库,这两步都可以去掉。
  • GOWORK: offHUGO_MODULE_WORKSPACE: off — 防止本地开发用的 go.work 意外参与 CI 构建,保证 CI 验证的是 go.mod 里固定的那个公开标签。
  • --baseURL "${{ steps.pages.outputs.base_url }}/" — 项目站点的 URL 形如 https://<OWNER>.github.io/<REPO>/configure-pages 会把它算出来,不用手写。
  • --panicOnWarning — 有告警不发布。

在仓库 Settings → Pages → Build and deployment 里把 Source 设为 GitHub Actions,推一次 main,在 Actions 标签页查看第一次运行。

自定义域名在同一设置页的 Custom domain 里填写,并按提示配置 DNS,随后把 hugo.yml 里的 baseURL 换成这个域名。发布流程需要产物里带 CNAME 文件时,把它放进 static/CNAME,Hugo 会原样复制到 public/

Cloudflare Pages 从关联的 GitHub / GitLab 仓库构建,并为每个评审分支创建预览部署。构建在平台侧完成,仓库里不用放工作流。

在 Workers & Pages 里导入仓库,选定生产分支:

构建命令
hugo --gc --minify --printPathWarnings --panicOnWarning
构建输出目录
public
HUGO_VERSION
0.164.0(或主题验证过的其它版本)
GO_VERSION
仅 Hugo Module 方式需要;固定一个构建镜像支持的版本
SKIP_DEPENDENCY_INSTALL
1

四点说明:

  1. HUGO_VERSION 必须显式设置,Production 与 Preview 两个环境都要设。Cloudflare v3 构建镜像的默认 Hugo 版本低于 OINK 要求的 0.160.1,不固定版本会在构建镜像更新时静默改变工具链。
  2. SKIP_DEPENDENCY_INSTALL=1 关掉通用依赖安装步骤。OINK 消费端不需要 Node.js,仓库里只给维护工具用的 package.json 不应由平台安装。
  3. Hugo 站点不在仓库根目录时,把 Root directory 设成站点目录,输出目录相对它解析。
  4. 预览部署不要当成生产发布。预览需要用自动生成的 Pages URL 作 base URL 时,构建命令改成 hugo --gc --minify --baseURL "$CF_PAGES_URL",生产发布用规范域名重新构建一次。

检查第一次构建日志:正常的 OINK 消费端构建只有一条 Hugo 命令,不会执行 npm、PostCSS、Autoprefixer,也不会下载主题自有的浏览器资源。

Netlify — 构建命令 hugo --gc --minify,发布目录 public,环境变量 HUGO_VERSION。同样的设置可以写进仓库:

netlify.toml
[build]
command = "hugo --gc --minify --printPathWarnings --panicOnWarning"
publish = "public"

[build.environment]
HUGO_VERSION = "0.164.0"

用 submodule 安装主题就打开递归 submodule 检出;用 Hugo Module 就要求构建环境有 Git 和 Go。生产与预览应使用同一个 Hugo 版本,除非预览环境本来就是用来测升级的。

Vercel — 同样的三件事:构建命令 hugo --gc --minify、输出目录 public、环境变量 HUGO_VERSION。它同样不需要安装 npm 依赖。

任何静态服务器(Nginx / Caddy) — 把 public/ 的内容整个铺上去:

/etc/nginx/conf.d/docs.conf
server {
    listen 80;
    server_name docs.example.com;
    root /var/www/oink;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    error_page 404 /404.html;
}

站点是纯静态的,没有需要转发给应用服务器的路径。

对象存储 — Hugo 自带 deploy 命令,把目标写进配置即可:

hugo.yml
deployment:
  targets:
    - name: aws
      URL: 's3://www.your-domain.tld'
      cloudFrontDistributionID: E9RZ8T1EXAMPLEID

构建之后执行 hugo deploy:它比对远端与 public/ 的差异,只上传变化的文件,并在给了 cloudFrontDistributionID 时使 CDN 缓存失效。不带 --target 时用第一个目标,--dryRun 先看要改什么。两个前提:Hugo 二进制带 withdeployhugo version 的输出里能看到),云厂商凭据由标准环境变量或配置文件提供(AWS 上先用 aws s3 ls 确认)。

离线打包 — 网络隔离环境里,在能联网的机器上构建,把产物打成一个包带过去:

终端
hugo --gc --minify --baseURL "https://docs.internal.example.com/"
tar -czf oink-site-$(date +%Y%m%d).tar.gz -C public .

# 目标机器上
tar -xzf oink-site-20260817.tar.gz -C /var/www/oink

构建时就要用目标环境的 baseURL,产物里的绝对链接不能在解包之后再改。

托管商没有 Go — 用 Hugo Module 引入主题需要构建环境有 Go。平台不提供时,改用 Git submodule(构建前执行 git submodule update --init)或离线归档(把 themes/oink/ 提交进仓库),见从零建站与其它安装方式

预览部署不要被收录

Hugo 的 -e / --environment 只选择构建期行为,不改变站点内容,但 OINK 有三处会跟着它变:production 环境才输出 <meta name="robots" content="index, follow">、才让 robots.txt 变成 Allow: /、才渲染 Google Analytics 模板。PR preview、staging 这类构建不要用 --environment production

终端
hugo --gc --minify --environment staging --baseURL "$PREVIEW_URL"

出来的产物自带 noindex, nofollowDisallow: /,也不会向分析服务上报数据。

内容安全策略

主题自带的运行时、字体与图标都是同源资源,严格的内容安全策略(CSP)因此可行。主题不提供一份通用策略:需要哪些指令由站点启用了什么决定。

改变所需指令的地方有五处:

  • 作者写的行内 HTML 与行内脚本,renderer.unsafe: true 之下由作者负责。
  • ECharts 的 $fn: 回调:回调函数由站点注册到 window.OinkEchartsFunctions,注册脚本的来源要进 script-src
  • 分析脚本:站点自己插入的那段脚本与它上报的目标。
  • 远程 API 规范自建图表服务:落在 connect-srcimg-src
  • giscusscript-srcframe-src 要一起放行。

从只覆盖已审查功能的最小策略起步,逐项放行:不需要回调时让 ECharts 选项保持纯数据,审查作者写的行内脚本,只为站点主动启用的集成添加远程来源。产物里的子资源来源可以先用断网构建验证里的脚本扫一遍。

验收清单

部署完成后按这张表走一遍。前四项是构建期的,后面几项要在真实 URL 上查。

零告警构建
构建命令带 --printPathWarnings --panicOnWarning,日志里有 Total in …
baseURL 正确
页面源码里 <link rel="canonical"> 指向真实生产地址(含子路径)
站点地图
<baseURL>/sitemap.xml 可访问;多语言站点是一个索引,指向 /en/sitemap.xml/zh/sitemap.xml
robots
<baseURL>/robots.txtAllow: / 并带 Sitemap: 行;预览部署应该是 Disallow: /
搜索索引
浏览器能取到 <baseURL>/offline-search-index.<语言>.json,站内搜索有结果
Markdown 输出
任一页面 URL 后面加 index.md 能取到纯文本(站点在 outputs.page 里开了 markdown 时)
llms.txt
<baseURL>/llms.txt<baseURL>/zh/llms.txt 可访问(站点在 outputs.home 里开了 LLMS 时)
两种语言
两边的文档页、博客页、首页都能打开,语言切换落到对应页面而不是首页
外观与交互
深浅色切换、打印视图、代表性组件(提示块、标签页、代码块复制)正常
404
访问一个不存在的路径,看到站点自己的 404 页

sitemap.xmlrobots.txt.mdllms.txt 这几项的开关在配置总览,Agent 输出的细节见 Agent 支持

回滚

静态站点的回滚就是重新发布上一个已知可用的 commit,不要在生产上手工改文件。

  • GitHub Pages:在 Actions 里找到上一次成功的 Deploy Oink site to GitHub Pages 运行,点 Re-run all jobs;或者 git revert 出问题的提交再推一次。
  • Cloudflare Pages / Netlify / Vercel:在部署列表里选上一个成功的部署,用平台的 Rollback / Publish deploy 把它重新设为生产版本。
  • 自建静态服务器:保留上一份 tar.gz,解压覆盖。离线打包里给产物加日期后缀就是为了这一步。

问题出在主题升级而不是内容时,回滚的是 go.mod 里固定的版本,见版本升级

3 - 启用评论

用 giscus 把 GitHub Discussions 接成页面底部的评论区,全站开、按页关、跟随深浅色。

OINK 的评论走 giscus:每个页面对应一条 GitHub Discussion,读者用 GitHub 账号登录后发言,维护者在 GitHub Discussions 里审核与管理。主题不提供自建评论后端,也不内置 giscus 以外的服务商。

前提是一个公开的 GitHub 仓库,访客读不到私有仓库的 Discussions。

这是主题里少数对外发请求的功能

启用评论的页面会从 https://giscus.app 加载脚本和 iframe,网络隔离环境里用不了。它默认关闭,只在显式打开时才加载。站点有隐私政策时,这条外部数据边界应当写进去。

准备 GitHub 仓库

  1. 选一个公开仓库存放评论线程,可以就是站点源码仓库。

  2. 在仓库 Settings → General → Features 里勾选 Discussions。

  3. 为该仓库安装 giscus GitHub App。未安装 App 时访客无法评论或表态。

  4. 选一个 Discussion 分类。giscus 推荐 Announcements 类型:只有维护者与 giscus bot 能在该类型下新建 Discussion,读者不会误开话题。

仓库 ID 与分类 ID 是公开标识符,不是凭据。不要往 Hugo 配置里放 personal access token、OAuth secret 或密码。

生成配置

打开 giscus.app,按表单填仓库、映射方式和分类,页面下方会生成一段 <script>。把里面四个属性抄进 OINK 配置:

data-repo
repo
data-repo-id
repoId
data-category
category
data-category-id
categoryId

映射方式(mapping)决定哪个页面对应哪条 Discussion。OINK 默认 pathname,适合发布路径稳定、同一个仓库要服务多个域名或预览环境的站点。开始收集评论之后再改 mapping 或移动页面,giscus 会去找另一条 Discussion:已有评论不会被删除,但页面上再也找不到它们。映射方式要在上线前定好;确实要改 URL 时,同时保留重定向或重命名 Discussion。

全站启用

把生成的标识符写进站点配置:

hugo.yml
params:
  comments:
    enable: true
    type: giscus
    giscus:
      repo: pgsty/oink.pgsty.com
      repoId: R_kgDOTzFZAg
      category: Announcements
      categoryId: DIC_kwDOTzFZAs4DDCm-
      mapping: pathname
      inputPosition: bottom
      theme: auto
      loading: lazy

上面是本站正在使用的配置。reporepoIdcategorycategoryId 四个键缺一不可:任何一个缺失或只有空白字符,Hugo 打一条 WARNING 并跳过 giscus,构建不会失败,因此生产构建要带 --panicOnWarningtype 目前只接受 giscus,写别的值同样是告警加跳过。params.comments 的键名与 Hextra 同形,从 Hextra 迁来的配置可以照搬。

其余的键(strictreactionsEnabledemitMetadatatermlanglightThemedarkThemeariaLabelerrorMessage)都有默认值,完整定义见配置总览。功能开关既可以写 YAML 布尔值,也可以写 giscus 风格的 0 / 1

按页开关

front matter 里的 comments 可以从任一方向覆盖全站开关,离页面最近的值优先。

只给某些页面开评论。全站关掉但保留完整仓库配置,再让选中的页面显式打开:

content/blog/2026-roadmap.md
---
title: 2026 路线图
comments: true
---

只关掉某些页面。全站开着,让不适合讨论的页面退出:

content/about/security.md
---
title: 安全政策
comments: false
---

整个栏目统一设置用 cascade。本站在 content/docs/_index.zh.md 的 cascade 里写了 comments: true,本页底部因此有一个真实的 giscus 评论区。

content/docs/_index.zh.md
---
title: OINK 文档
cascade:
  type: docs
  comments: true
---

站点同时配了 services.disqus.shortname 时,giscus 优先:giscus 生效即抑制 Disqus,comments: false 同时关掉两者,giscus 必填键不全则告警跳过、由 Disqus 兜底。

多语言文案

giscus 的界面语言自动跟随当前 Hugo 语言:简体、繁体、香港繁体分别映射到对应的 giscus locale,不支持的语言回退英文。只有自动选择不合适时才显式设 lang

需要翻译的是 OINK 一侧的两句文案:评论区的无障碍标签与加载失败提示。它们按语言配置,与全局仓库配置合并:

hugo.yml
languages:
  en:
    params:
      comments:
        giscus:
          ariaLabel: Comments
          errorMessage: Comments could not be loaded. Please try again later.
  zh:
    params:
      comments:
        giscus:
          ariaLabel: 评论
          errorMessage: 评论加载失败,请稍后重试。

语言层只需要写差异部分,repo / repoId / category / categoryId 留在 params.comments 里就够了。

跟随深浅色

theme: auto 时,giscus iframe 跟随 OINK 的深浅色切换按钮和浏览器的 prefers-color-scheme,读者切换主题时评论区一起变。

需要更贴合站点配色时,用 lightTheme / darkTheme 分别指定两套 giscus 主题,取值是 giscus 内置主题名或站点自己托管的 CSS。本站用的是后者:

hugo.yml
params:
  comments:
    giscus:
      theme: auto
      lightTheme: /css/giscus-oink-light.css?v=0.4.0
      darkTheme: /css/giscus-oink-dark.css?v=0.4.0

theme 写成固定主题名时不再跟随切换。

自定义 giscus 主题需要跨域可读

giscus 的 iframe 从 giscus.app 加载,要读站点上的这个 CSS 文件需要 CORS 允许。本站在 hugo.ymlserver.headers 里给本地预览加了 Access-Control-Allow-Origin: '*';线上由托管商的响应头配置决定。

隐私与 CSP

  • OINK 不会索取或保存读者的 GitHub 密码与访问令牌,登录与发帖全程在 giscus / GitHub 一侧完成。
  • 评论初始化脚本是主题自带的同源资源,只加入启用了评论的页面,未开评论的页面没有这段脚本。
  • loading: lazy 时,读者滚动到评论区附近才加载 iframe。
  • 站点有严格的内容安全策略时,script-srcframe-src 都要放行 giscus,合并进现有策略而不是替换其它指令(总则见内容安全策略):
CSP 片段
script-src 'self' https://giscus.app;
frame-src 'self' https://giscus.app;

外部脚本加载失败或没能创建 iframe 时,OINK 结束加载状态并在实时状态区域显示 errorMessage,不会让页面停在「加载中」。

验证

终端
hugo --minify --panicOnWarning     # 必填键缺失会在这里失败
hugo server --disableFastRender

然后逐项确认:

  1. 打开一个应该有评论的页面,页面底部出现 giscus,显示「使用 GitHub 登录」,界面语言是当前页面的语言。
  2. 切换 OINK 的深浅色,评论区跟着变(theme: auto 时)。
  3. 打开设置了 comments: false 的页面,确认那里既没有 giscus 也没有其它评论组件。
  4. 发一条测试评论,回到 GitHub 看指定分类下是否出现了对应的 Discussion,并且能在 GitHub 上管理。

首次评论或表态创建 Discussion 之前,浏览器控制台提示「找不到 Discussion」是正常现象。

出问题时按这个顺序查:构建日志里的 WARNING(四个必填键)→ params.comments.enabletype → 页面 front matter 的 comments → 仓库是否公开、Discussions 是否开启、giscus App 是否安装 → 浏览器控制台与响应头(CSP 是否拦了 giscus.app)。找不到已有评论线程,先恢复原来的 mapping 和页面路径。

4 - 分析与 SEO

接入一个分析服务(或者不接),并把主题已经生成的 canonical、hreflang、社交卡片、站点地图与 robots 配对。

主题默认不加载任何分析、表单或广告脚本,不配置就没有对外请求。接入需要显式配置,并把这条外部数据边界写进站点的隐私说明。SEO 一侧相反:canonical、hreflang、robots meta、Open Graph 与 Twitter 卡片由主题逐页生成,需要你做的是把 baseURL 与每页的 description 写对。

接 Google Analytics

用 Hugo 内置的服务配置,填 GA4 的 measurement ID:

hugo.yml
services:
  googleAnalytics:
    id: G-6JLQEHYFQG

主题只在 production 环境渲染这段脚本(hugo 构建默认 production,hugo server 默认 development)。本地预览与预览部署因此不上报数据,不需要另加开关。

不要同时设置已经弃用的顶层 googleAnalytics 键。不需要分析时删掉整段配置,不要填一个假 ID。

这与网络隔离环境不兼容

配上之后,页面浏览量与事件会发给 Google。严格的同源内容安全策略也需要为它放行,见内容安全策略。这是站点决策,不是主题默认。

接其它分析服务

Plausible、Umami、Matomo 这类服务只要求插入一段脚本。主题提供两个注入点,在站点仓库里建同名文件即可,不用改主题:

layouts/_partials/hooks/head-end.html , 插入位置</head> 之前,在 Google Analytics 模板之前
分析脚本、cookie 同意脚本、主题没提供的 meta 标签
layouts/_partials/hooks/body-end.html , 插入位置页面脚本的最后
只影响交互、不影响首屏的第三方代码
layouts/_partials/hooks/head-end.html
{{ if hugo.IsProduction }}
<script defer data-domain="oink.pgsty.com"
        src="https://plausible.io/js/script.js"></script>
{{ end }}

hugo.IsProduction 这一层不要省:没有它,每个人的本地预览都会向你的统计上报数据。

head-end 在 Google Analytics 之前执行

这是有意的:cookie 同意脚本必须先于分析脚本运行,才能真正拦住它。

「这篇文档解决了你的问题吗」反馈组件是另一件事:默认关闭,不发网络请求,配置见仓库与页面信息

页面描述

<meta name="description"> 按这个顺序取值,取到第一个非空的就停:

  1. 页面 front matter 的 description
  2. Hugo 计算出的页面摘要(.Summary
  3. 站点配置里的 params.description

每页写一句 description 是唯一需要作者做的 SEO 动作。它同时用于三处:搜索引擎的摘要、栏目首页的卡片副标题、站内搜索的结果预览。

content/docs/admin/analytics.zh.md(本页)
---
title: 分析与 SEO
description: 接入一个分析服务(或者不接),并把主题已经生成的 canonical、hreflang、社交卡片、站点地图与 robots 配对。
---

多语言站点要给每种语言各写一句,不要把英文描述抄到中文页上。站点级默认值也是分语言的:

hugo.yml
languages:
  en:
    params:
      description: A Hugo theme for engineering docs
  zh:
    params:
      description: 为工程而设计的 Hugo 文档主题

canonical 与 hreflang

主题为每个页面输出一条 canonical 和一组 hreflang 备用链接,不需要配置:

渲染结果(本页)
<link rel="canonical" href="https://oink.pgsty.com/zh/docs/admin/analytics/">
<link rel="alternate" hreflang="zh-CN" href="https://oink.pgsty.com/zh/docs/admin/analytics/">
<link rel="alternate" hreflang="en-US" href="https://oink.pgsty.com/">

hreflang 的语言代码来自各语言的 locale(本站是 en-US / zh-CN),链接来自 Hugo 的译文关系。上面英文那一条指向站点首页而不是对应的英文页:本页没有英文对等文件,Hugo 找不到译文时回退到目标语言首页。这是预期行为,也可以用来判断译文关系有没有被 Hugo 认出来。

canonical 由 baseURL 拼出。baseURL 配错时 canonical 会把搜索引擎指向不存在的地址,比构建失败更难发现。上线前照发布上线的验收清单查一遍。

多语言的完整配置在多语言

社交卡片

主题调用 Hugo 内置的 Open Graph 与 Twitter 卡片模板,标题、描述、URL、语言、站名都是自动的:

渲染结果(本页)
<meta property="og:title" content="分析与 SEO">
<meta property="og:type" content="article">
<meta property="og:url" content="https://oink.pgsty.com/zh/docs/admin/analytics/">
<meta property="og:locale" content="zh_CN">
<meta property="og:locale:alternate" content="en_US">
<meta name="twitter:card" content="summary">

要让分享出去的链接带图,在 front matter 里给 images

任意页面
---
title: OINK v0.6.0 发布
images: [/images/releasenote.webp]
---

给全站一张兜底图就把同样的键写进 params

hugo.yml
params:
  images: [/images/oink.webp]

有图时 twitter:cardsummary 变成 summary_large_image,并多出 og:imagetwitter:image 两条。本站两处都没有设置,上面的渲染结果里因此看不到图片相关的标签。

站点地图

Hugo 自动生成,多语言站点生成的是一个索引:

public/ 下的结构
sitemap.xml        ← 索引,指向下面两个
en/sitemap.xml
zh/sitemap.xml

站点级默认值和页面级覆盖都是 Hugo 原生的:

hugo.yml
sitemap:
  changefreq: monthly
  filename: sitemap.xml
  priority: 0.5
某个页面
---
title: 发布说明
sitemap:
  priority: 0.8
---

changefreqpriority 是提示不是承诺,搜索引擎可以忽略。值得做的是发布前确认草稿、私有内容与非规范副本没有进入站点地图,并且每种语言的那份都生成了。

robots.txt 与不收录

Hugo 只在站点配置里打开开关时才生成 robots.txt

hugo.yml
enableRobotsTXT: true

主题提供的模板按构建环境给出两种结果,不需要你写内容:

production 构建
User-agent: *
Allow: /

Sitemap: https://oink.pgsty.com/sitemap.xml
非 production 构建
User-agent: *
Disallow: /

页面里的 robots meta 跟着同一个开关走:production 且不是打印输出时是 index, follow,否则是 noindex, nofollow。预览部署不要用 --environment production 构建,非 production 自带不收录的行为。

主题没有按页 noindex 的开关。某一页不该被收录时,可靠的做法是不发布它(draft: true,或用 Hugo 的 _build 选项)。既要发布又不想被收录,就用 head-end.html 钩子自己输出;主题已经输出了一条 robots meta,两条同时存在时如何合并由搜索引擎决定。

收录检查

上线一两周后,按这个顺序确认搜索引擎看到的东西和你以为的一致:

  1. 抓取权限:访问 <baseURL>/robots.txt,确认是 Allow: / 而不是 Disallow: /
  2. 页面清单:访问 <baseURL>/sitemap.xml,点进语言子地图,看页面数量对不对。
  3. 收录数量:在搜索引擎里查 site:你的域名,数量级对得上就行,不必逐页核对。
  4. 规范地址:搜索结果应当落在 canonical 指向的 URL 上,而不是带 ? 参数或旧域名的版本。
  5. 主动提交:在 Google Search Console / Bing Webmaster Tools 里加上站点并提交 sitemap.xml 的地址,比等着被爬快。

搜索元数据补不了内容本身的问题:单薄、重复、过时的页面,写再好的 description 也一样。

验证

终端
hugo --gc --minify --printPathWarnings --panicOnWarning

在产物里查这几项:

终端
# canonical 指向真实生产地址
grep -o '<link rel="canonical"[^>]*>' public/zh/docs/admin/analytics/index.html

# production 构建才有 index, follow
grep -o '<meta name="robots"[^>]*>' public/zh/docs/admin/analytics/index.html

# robots.txt 与站点地图
cat public/robots.txt
head -5 public/sitemap.xml

# 没接分析时,产物里不应该有任何 gtag / analytics 请求
grep -rl 'googletagmanager\|gtag(' public/ | head

浏览器里再确认一次:打开一个代表性页面,看开发者工具的网络面板,没接分析的站点不应有指向第三方域名的请求。

  • 发布上线baseURL、验收清单与预览部署不被收录
  • 仓库与页面信息 — 页面反馈组件、编辑本页与最后修改时间
  • 多语言 — 语言配置决定 hreflang 与译文关系
  • Agent 支持 — 给大模型看的 .md 输出与 llms.txt
  • 配置总览servicessitemapenableRobotsTXT 等键的定义

5 - 版本升级

升到新版主题、用迁移工具把 0.4 的 shortcode 改成 v5 语法、从 Docsy 迁过来,以及出问题怎么退回去。

升级 OINK 是换一个固定的模块版本,再确认站点仍能零告警构建。内容多数不用改;需要改的场景(0.4 的 shortcode 换成 v5 的 Markdown 原生形态)有一个可以干跑的迁移工具,不必手改几百个文件。

升级会改变渲染结果。先建一个升级分支再动手,回退的代价就是丢弃一个分支。

先看发布注记

每个版本的变更、破坏性改动与升级要点都写在发布注记里,升级前先读一遍目标版本那篇:

注记说明这次要不要改内容、有没有配置键被移除、默认行为有没有变化。跳过这一步的代价是升级后对着一个变了样的页面猜原因。

升级 Hugo Module

生产站点固定发布标签或不可变 commit,不跟随分支,也不用 @latest

终端
hugo mod get github.com/pgsty/[email protected]   # 换成发布注记里的标签
hugo mod tidy
hugo mod graph | grep github.com/pgsty/oink

最后一条要能看到解析结果是那个标签本身,而不是伪版本(v0.0.0-2026...-abcdef)或 main。固定的版本落在 go.mod 里,跟着代码一起提交:

go.mod
module github.com/pgsty/oink.pgsty.com

go 1.26.6

require github.com/pgsty/oink v0.6.0
本地模块替换会盖掉这个固定版本

make devmake check 会仅对当前命令设置 HUGO_MODULE_REPLACEMENTS,使用同级的主题 checkout。判定某个发布标签是否可用时使用不带替换的 make build,否则验证的是本地那份代码。

其它安装方式各一句。Git submodule:用 git submodule update --remote themes/oink 拉到新 ref,再提交 submodule 指针。离线归档与克隆:把 themes/oink/ 整个换成新版本的解压结果,确认 theme: 的值仍与目录名一致。三种方式的取舍见从零建站与其它安装方式

升级后必做

终端
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --panicOnWarning --logLevel info

三件事一起做了:清掉可能过期的缓存、用新版本重新构建、把任何告警变成失败。

--logLevel info 是为了看见 Hugo 的弃用提示。Hugo 的弃用分两级:先是 WARN 级提示(仍可使用),下一个版本变成 ERROR(构建失败)。带上 --panicOnWarning 相当于提前一个版本发现它们,把修复的时间留给自己。

构建通过之后,人眼再过一遍:首页、一个文档页、一个博客页、404、两种语言、两种配色、打印视图,以及站点自己定制过的地方。

内容迁移工具

0.4 的一批 shortcode 在 v5 里换成了 Markdown 原生形态。主题仓库带了一个只依赖 Python 标准库的工具做这件事:

终端
git clone https://github.com/pgsty/oink
cd oink

# 1. 只读盘点:一次看多个站点要改什么,可导出 Markdown / JSON 报告
python3 bin/migrations/oink06.py report --sites ~/pgsty/oink.pgsty.com ~/www/ddia --md report.md

# 2. 干跑:打印每个文件的 diff 与计数,不写任何东西
python3 bin/migrations/oink06.py migrate --site ~/pgsty/oink.pgsty.com

# 3. 真改:原子写入
python3 bin/migrations/oink06.py migrate --site ~/pgsty/oink.pgsty.com --write

# 4. 查残留:还有旧语法就退出码 1
python3 bin/migrations/oink06.py check --site ~/pgsty/oink.pgsty.com

用它的时候记住四条:

  • 干跑是默认行为,只有 --write 才落盘。先干跑,读 diff,再写。
  • 重跑一次应该零改动。第二次 --write 还报改动,说明有转换不收敛,停下来看那几个文件。
  • 围栏里的文字不动,文档站里示范旧写法的代码块不会被误伤。
  • 表达不了的构造原样保留,并附 file:line 与原因列出,作为手工处理清单,不是失败。

只想先转某一类时用 --only,键名见下表最后一列:

终端
python3 bin/migrations/oink06.py migrate --site ~/www/ddia --only callout,tabs --write

改完重新构建一次(带 --panicOnWarning),并逐页看渲染结果:工具保证语法正确,不保证语义符合预期。

0.4 → v5 语法映射

{{%/* alert color= title= */%}}{{%/* details */%}}{{%/* pageinfo */%}}、手写 <details><summary> , v5 的写法> [!TYPE] 标题 / > [!DETAILS]-
callout
{{</* tabpane */>}} + {{%/* tab header= */%}}{{</* code-group */>}} + {{</* code-tab */>}} , v5 的写法相邻围栏加 {tab= group= value=};正文型标签页用 {{</* tabs */>}} + {{</* tab */>}}
tabs
{{</* filetree */>}}filetree/folderfiletree/file , v5 的写法filetree 数据围栏
filetree
{{</* gallery */>}}gallery/image , v5 的写法gallery 数据围栏
gallery
{{</* echarts */>}}{{</* infographic */>}} , v5 的写法同名数据围栏($fn: 不变,js 子围栏要挪到 window.OinkEchartsFunctions
datafence
doc-cards / doc-cardnav-cards / nav-cardcard / cardpanedoc-carousel , v5 的写法{{</* cards */>}} + {{</* card */>}},或链接列表加 {.cards}
cards
{{</* imgproc */>}}{{</* image */>}} , v5 的写法![alt](src) 加属性行 {command= options= caption=}
image
{{</* readfile file= */>}} , v5 的写法{{</* include file= */>}}
include
围栏属性 {filename="x"} , v5 的写法{title="x"}
fencetitle
{{</* badge outline= */>}} , v5 的写法去掉 outline 参数
badge
{{</* example */>}} + 围栏、{{</* book-figures kind="tbl" */>}} , v5 的写法{{</* eg */>}}…{{</* /eg */>}}{{</* book-tables */>}}
eg
{{%/* _param x */%}}iframeconditional-textblocks/*netlify、不带 kind 的 xref , v5 的写法工具只报告,需要手工处理
reportonly

每个新写法长什么样、有哪些参数,去组件里对应的那一页。

从 Docsy 迁移

OINK 是 Docsy 的硬分支:内容模型、td- 命名、Sass 变量、大部分 front matter 都还在。迁移的核心动作是删掉站点里复制的公共外壳,让主题的实现接管,而不是重写正文。

  1. 固定目标版本。在 go.mod 里换成 OINK 的发布标签,或者用完整的版本化归档。评估期可以用不提交的 go.work 指向本地 checkout。

  2. 清点覆盖项。把 layouts/assets/static/ 下每个站点级文件归成四类:公共外壳的副本(验证后删)、OINK 已提供的组件(删或机械重命名)、品牌定制(保留,缩到最小 hook)、业务专属数据与交互(留在站点)。按引用关系删,不要清空 layouts/:首页、下载页这些地方可能还在调用你要删的 partial。

  3. 搬配置。titlelanguages.*github_repogithub_branchpage_widthparams.ui.* 全部留在原来的语义位置,OINK 没有另起一套命名空间。搜索与 Logo 这类只要打开对应的键:

    hugo.yml
    params:
      logo: img/product.svg
      offline_search: true

    Docsy 的驼峰式检索键在 OINK 中已改名:offlineSearchofflineSearchIndexofflineSearchMaxResultsofflineSearchOnServeofflineSearchSummaryLength 一律改为下划线形式。这一步要自己盯着改——那份「中断构建并报出新键名」的迁移登记表已经删除,旧键现在只是一个没人读的键,检索会一声不响地保持关闭。

  4. 字体与样式的兼容点。站点的 assets/scss/_variables_project.scss 里那些 Docsy Sass 变量仍然生效,会作为字体角色的种子值,不用为了升级把它们删掉:$td-fonts-serif$font-family-sans-serif$headings-font-family$font-family-code 各自喂给对应的字体角色。Docsy 的 Google Fonts 开关 $td-enable-google-fonts$td-google-font-name$td-web-font-path 主题已不再读取,留在文件里不影响构建,也不产生任何效果:OINK 自带 Inter、Chakra Petch 与 IBM Plex Mono,任何预设都不向 Google Fonts 发请求。想换字体走 token 层,见品牌外观

  5. 换 shortcode。Docsy 的 alertpageinfotabpanecard 系列在 v5 里都有对应形态,用上面的迁移工具批量转,--only 一类一类来。

  6. 一次删一组,每组构建一次。在临时副本里演练,记下主题 commit、Hugo 版本、删了哪些文件、产出多少个 HTML;确认等价之后再在生产分支上重做一遍。

第二步里「验证后删」的那一类,通常是这些文件:

  • layouts/baseof.html 与公共的 docs / blog baseof*.html
  • navbar、footer、sidebar、TOC、search、head CSS 的 partial 及其对应 hook;
  • 旧的品牌文档外壳 partial;
  • asciinemaechartsinfographicdoc-carouseldetailstab / tabpane、card 与 param 的 shortcode 副本;
  • 只服务于上述实现的 JavaScript、Lunr 副本、轮播代码与 SCSS;
  • 不再被任何站点资源需要的 PostCSS 与 Autoprefixer 步骤。

删完之后有两类问题会浮出来。

站点自己的脚本报 $ is not defined:主题不带 jQuery,它以前由 Docsy 在每个页面的 <head> 里加载。主题的功能都不需要它,仍然需要的站点自己引入:

layouts/_partials/hooks/head-end.html
<script src="{{ (resources.Get "js/jquery.min.js").RelPermalink }}"></script>

用 Docsy blocks/* 搭的首页在 v5 构建失败,报 template for shortcode "blocks/cover" not found:主题没有这一组 shortcode。改用 data/home/<语言>.yaml 的首页分区,或给页面写 layout: landing,见首页与落地页

从 0.4 升级的要点

0.4 改了几个默认行为。升级后发现页面多了或少了东西,先看这几条:

  • 顺序翻页默认开启。docsbookblog 页尾都有上一页 / 下一页;文档沿侧栏树走,博客沿时间走。刻意不属于任何序列的页面用 pager: false 退出。

  • 顶栏在所有布局上都显示。紧凑状态只有一行图标导航,没有第二套移动端手风琴菜单,依赖旧移动菜单的本地脚本与测试要删掉。整个分区不要顶栏时用 cascade 里的 navbar_enabled: false

  • 页脚默认 fat 且全站生效。只接受 fat / slim / none;页脚数据必须放在 data/footer/<语言>.yaml(单语言站点用 data/footer.yaml),data/home 里残留的 footer 键会让构建失败并提示新位置。

  • 单键导航默认开启:/ 打开完整搜索,\ 只进命令模式。培训材料里描述旧行为的地方要改。页面操作也挪到了面包屑旁边的拆分按钮上。

  • 代码块的 DOM 变了。.td-code 外壳套在原来的 .highlight 外面(.highlight.chroma 都保留),站点 CSS 里 .td-content > .highlight 这类直接子选择器要改成后代选择器 .td-content .highlight

  • 两个 ICP 页脚参数被移除:footer_icpfooter_icp_url 换成一个支持行内 Markdown 的字符串。

    hugo.yml
    params:
      footer_center_info: '[京ICP备00000000号](https://beian.miit.gov.cn/)'
  • 数学公式要站点自己开 passthrough。Hugo 不会合并主题的 markup 配置,用 \(…\)\[…\]$$…$$ 的站点必须在自己的 hugo.yml 里启用 goldmark passthrough 扩展,见公式

这几项的完整配置都在配置总览布局与页面类型

验证

升级不是「构建通过」就算完,按表面分别看:

文档 / Book
侧栏顺序、翻页、标题、页面操作、编号与交叉引用
博客
时间顺序翻页、RSS 归属、顶栏与页脚
首页 / Landing
无 JS 时的内容、紧凑菜单、打印
发布页
推导出的下载 URL、校验和、发布状态
组件
站点用得最多的那几个组件各找一页看渲染结果
无障碍
纯键盘走一遍、焦点顺序、两种配色、强制颜色模式
部署
站内链接与资源都保留了 base path 前缀

本站的完整门禁是:

终端
npm test           # 构建断言、Markdown 与 favicon goldens、翻译对等、渲染后链接
npm run test:browser   # Playwright:无障碍、响应式外壳、键盘导航、内容组件、代码块、场景组件

其它站点跑等价的构建、链接、输出与浏览器检查即可,细节见排错与检查

本地构建成功不等于发布完成

源码可构建、标签已签名并能通过 Go proxy 解析、站点已固定该标签、线上已部署,这是四件事,要分别记录。别用一次绿色的本地构建代替它们。

最后一步在真实环境上做:先部署一份预览,在真实 URL 上验证页面与浏览器的网络请求,评审通过再合并,合并后在生产上做一次冒烟测试。

回滚

回滚的是版本固定,不是工作树:

终端
hugo mod get github.com/pgsty/[email protected]   # 上一个已知可用的标签
hugo mod tidy
rm -rf public resources/_gen
hugo --gc --minify --panicOnWarning

三条原则:

  • 保留升级前的模块固定、站点 commit 与已知可用的部署产物,回滚时三者一起恢复。
  • 不要只回滚一部分。给新主题塞回几个旧布局副本,会得到一个比任何完整版本都更难诊断的混合状态。
  • 升级分支与验收证据都留着。回滚是为了先恢复线上,不是丢掉已经做完的工作。

线上产物本身的回滚(重新发布上一个部署)见发布上线

6 - 排错与检查

构建、语言、搜索、平台四类故障的症状 → 原因 → 修法,以及站点可以自己跑的那几项检查。

出问题时先做一次干净的生产构建,从第一条错误开始看,后面的多半是级联结果:

终端
rm -rf public resources/_gen
hugo --gc --minify --printPathWarnings --panicOnWarning --logLevel info

日志里出现 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 graphgo.modgo.sum,以及有没有多余的 workspace / replace。submodule:CI 有没有在 Hugo 之前跑 git submodule update --init。归档 / 克隆:theme: 的值要与 themes/ 下的目录名一致
模块下载卡住或超时 Go 的模块代理不通 Hugo 通过 Go 拉模块,所以走 GOPROXY。国内网络可以 export GOPROXY=https://goproxy.cn,direct;隔离环境改用离线归档或提交 themes/oink/
页面上出现 {.cards}{.steps}{caption=…} 这类原样文字 站点没开 goldmark 的块级属性 站点的 hugo.yml 里必须有下面那三项,主题的 markup 配置不会被 Hugo 合并进来
图片带属性行时被包进了 <p>,图注没生效 wrapStandAloneImageWithinParagraph: false 同上,三项一起加
行内 HTML 被转义成文字 renderer.unsafe: true 同上
\(…\) $$…$$ 原样显示 站点没启用 goldmark passthrough 公式math: true 不是启用开关
shortcode "tabs" must be closed or self-closed {{< tabs >}} 没写对应的 {{< /tabs >}} 报错里带 文件:行:列,去那一行补上闭合标记
template for shortcode "tabs" not found 正文里写了一个不存在的 shortcode,或引用 shortcode 语法时没有转义 文档里讲解 shortcode 语法时必须转义:在开标记与闭标记的内侧各加一对 /**/,Hugo 才会把它当文字而不是调用。名字打错就改回正确的名字
... attributes: unknown attribute "witdh" at ... 属性行里的键拼错或不被允许 属性行只接受该组件的允许键、classdata-*aria-*styleon* 一律构建失败。允许的键就写在报错括号里
shortcode "field": unsupported parameter "colour" at ... shortcode 参数名不对 组件参数——shortcode 参数与属性行的键——一律构建失败,不做静默降级。报错格式固定为「哪个 shortcode → 哪个参数 → 哪个文件的第几行」,照着改即可
invalid params.ui.page_width "widee" (allowed: normal | wide | full) -- using "normal" 配置或 front matter 的取值,不在允许集合里 配置类的错误降级而不中断,一个笔误不会让 hugo server 下每个 URL 都返回 500。消息里带键名、收到的值和实际用的回退值。构建加 --panicOnWarning,它就上不了线
某个页面设置不生效,也没有任何提示 键写在了 front matter 的 ui: 段里 页面键写在 front matter 顶层,键名是站点键去掉 ui.。写进 ui: 段的键没有人读,也没有人报错,见页面参数
构建通过但线上少东西 有 WARNING 没人看 构建命令加 --panicOnWarning。非法配置取值、giscus 必填键缺失、不支持的 comments.type、Hugo 的弃用提示都只是告警

那三项 goldmark 配置:

hugo.yml
markup:
  goldmark:
    parser:
      wrapStandAloneImageWithinParagraph: false
      attribute:
        block: true
    renderer:
      unsafe: true

两个最常见的 shortcode 报错长这样,注意结尾的 文件:行:列

构建输出
ERROR error building site: assemble: failed to create page from pageMetaSource /a:
  "…/content/docs/x.md:4:1": failed to extract shortcode:
  shortcode "tabs" must be closed or self-closed

ERROR error building site: assemble: failed to create page from pageMetaSource /a:
  "…/content/docs/x.md:4:5": failed to extract shortcode:
  template for shortcode "tabs" not found

语言

症状 原因 修法
译文页面不出现 四种可能,按顺序查 hugo.yml 里有 languages.zh 且设了 weight;② 文件名是 page.zh.mdzh 必须小写;③ 译文 front matter 没有 draft: truedate 不在未来;④ 影响路由的元数据与源文件一致
语言切换跳到了首页 Hugo 没找到对应译文 这是设计行为:找不到译文就回退到目标语言首页。要跳到对应页面,需要那个译文文件确实存在
锚点链接打开了页面却不定位 译文标题文字不同,自动生成的 ID 也不同 在译文标题上显式写英文 ID:## 安装 {#installation}。标题里含 shortcode 或行内 HTML 时不要凭文本猜 ID,去看英文页渲染出来的 HTML
菜单 / 首页分区没翻译 这些不在页面里,在配置和数据文件里 菜单在 languages.<lang>.menus,首页分区在 data/home/<lang>.yaml,界面字符串在 i18n/<lang>.yaml,见多语言
中文页 hreflang 指向英文首页 该页没有英文对等文件 补上英文页,或接受这个回退:它同时是「Hugo 有没有认出译文关系」的探针
症状 原因 修法
搜索框有但一直没结果 索引没生成 params.offline_search: true 之后,产物根目录下应该有 offline-search-index.<语言>.json,每种语言一份。没有就是没开
索引文件请求 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/checkoutfetch-depth: 0enableGitInfo 要读完整历史
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: offHUGO_MODULE_WORKSPACE: off,让它只认 go.mod 里固定的版本
预览部署被搜索引擎收录了 预览也用了 production 环境构建 预览构建不要带 --environment production,非 production 自带 noindexDisallow: /,见分析与 SEO
macOS 报打开文件过多 实时预览监视的文件超过了 shell 限制 先把生成目录与无关目录排除出监视范围,这通常才是根因;再考虑 ulimit -n
WSL 下很慢或漏掉改动 跨 Windows 挂载点工作 让 Hugo 处理 Linux 文件系统里的路径,跨文件系统的变更通知和权限行为会让实时重载失效
缺 Bootstrap / Font Awesome / Lunr / Mermaid 之类资源 发行物不完整 不要用 CDN URL 掩盖。确认 assets/third_party/assets/js/third_party/static/webfonts/VENDOR.json 都在;确实缺就重新获取同一个固定版本

站点自带检查

除了构建本身,站点还可以自己跑这几项。前两条任何 OINK 站点都能用,后面几条是本仓库的 npm 脚本,其它站点跑等价的检查即可。

零告警构建 , 命令hugo --printPathWarnings --panicOnWarning
重复输出路径、参数非法、外部集成配置不全
输出信任检查 , 命令python3 bin/check-output-security.py --public public --base-url https://oink.pgsty.com/
四种输出里的每个 href / src 都是站内相对或 http(s) / mailto / tel;没有 javascript: URL、没有行内 on* 事件处理器;跨站的 <iframe> <script> <img> 等要显式加 --third-party 才放行
翻译对等 , 命令node scripts/check-doc-translations.mjs --public public
每个英文页有没有中文对等页,以及渲染后的标题 ID 是否逐一对齐;锚点链接错位在这里暴露
完整门禁 , 命令npm test
下面六项串起来跑

npm test 里的六项各管一段:

  • test:base — 先构建一次,再跑 Markdown 风格、翻译对等、渲染后的 Markdown 与链接检查。
  • test:hugo-build — 构建断言:博客元数据、RSS、内容组件、构建过程零弃用提示。
  • test:md-output — Markdown 与 llms.txt 输出的 golden 比对,字节级。改了组件的 Markdown 形态就会在这里挂。
  • test:alt-site — 用 tests/fixtures/*.yml 里的替代配置各构建一次,确认不同配置组合都能起来。
  • test:favicons — head 输出的 golden 比对。
  • test:release-pin-contract — 站点公告的版本与 go.mod 固定的版本是否一致。

浏览器行为另开一套:npm run test:browser 依次跑 Playwright 的无障碍(axe WCAG AA)、响应式外壳、键盘导航、内容组件、代码块与场景组件六个套件。

check-output-security.py 在主题仓库里

它在主题仓库的 bin/ 下,是产品级的信任检查,任何 OINK 站点都可以跑,不依赖站点的测试框架。克隆主题仓库后指向自己的 public/ 即可,参数与用法见断网构建验证

诊断习惯

上面的表覆盖不到的问题,按这几条挖:

  • 用固定的 Hugo Extended 版本复现,不在版本浮动的环境里判断。
  • 清掉 public/resources/_gen 再重建,排除陈旧缓存。
  • 对比开发与生产两套配置层,很多只在线上出现的问题是环境差异。
  • 看第一条错误,不是最后那条。
  • 用一个最小页面区分「主题行为」和「站点覆盖」:把可疑内容单独放一页,站点覆盖分批重新启用,定位到具体那一项。
  • 看故障页面的浏览器控制台与网络面板,尤其是 404 的资源路径。

求助渠道

开 issue 时带上这几样,能省掉一轮来回:Hugo 版本(hugo version 完整输出)、主题版本(hugo mod graph | grep oink)、第一条完整错误、能复现的最小页面或最小站点。