跳转到主要内容

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

返回本页常规视图.

维护管理

站点从本机到线上的运维事项:本地预览、发布上线、评论、分析与 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/site-checks.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 默认 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 --minify --environment staging --baseURL "$PREVIEW_URL"

容器内预览

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

Dockerfile
FROM debian:bookworm-slim

ARG HUGO_VERSION=0.165.0
ARG GO_VERSION=1.27.0
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   # 按生产配置起预览服务器
本地覆盖可能掩盖公开版本

CI 与生产构建仍可能继承替换或 workspace。不要提交包含开发机路径的 workspace。 验证公开标签时,移除 HUGO_MODULE_REPLACEMENTS,同时设置 GOWORK=off 和 HUGO_MODULE_WORKSPACE=off。还要检查 go.mod、Hugo 配置中的持久替换,以及 _vendor/ 副本;先在同一环境下用 hugo mod graph 确认精确版本,再执行构建。

断网构建验证

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

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

最后一步使用与固定发布版本对应的主题 checkout 中的输出检查器。它需要 Python 3, 不依赖本文档站的测试框架。将下面两个绝对路径换成自己的目录,base URL 与构建时保持一致:

终端
python3 /path/to/oink/bin/check-output-security.py \
  --public /path/to/my-site/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.xml、robots.txt,robots.txt 是 Allow: /。
  • 启用本地搜索后,public/ 下每种语言各有一份索引:生产文件名为 offline-search-index.<语言>.<hash>.json,开发环境不带 hash。打开搜索,确认 data-td-index-src 指定的实际 URL 返回 200。
  • 用 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 会把本来正确的相对链接一起改写,让问题更难定位。

启用本地搜索后,打开搜索,在浏览器 Network 面板检查实际索引请求:语言与部署子路径 应正确,响应应为 200。页面的 data-td-index-src 属性给出完整地址;生产文件名是 offline-search-index.<语言>.<hash>.json,开发环境不带 hash。不要通过猜文件名来验证。

选一个托管商

源码托管在 GitHub 时,一份 Actions 工作流就够:构建在 Actions 里执行,产物通过 Pages 部署 API 发布,不需要维护 gh-pages 分支。OINK Starter 已经包含下面的文件; 只有手工组装站点时才需要复制。

.github/workflows/github-pages.yaml
 1name: Deploy 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: github-pages
15  cancel-in-progress: false
16
17env:
18  HUGO_VERSION: 0.165.0
19  # 同级 checkout 的 workspace 绝不能参与 CI 构建
20  GOWORK: off
21  HUGO_MODULE_WORKSPACE: off
22  HUGO_CACHEDIR: ${{ github.workspace }}/.hugo_cache
23
24jobs:
25  build:
26    name: Build Pages artifact
27    runs-on: ubuntu-latest
28    steps:
29      - name: Check out source
30        uses: actions/checkout@v7
31        with:
32          fetch-depth: 0
33
34      - name: Set up Go
35        uses: actions/setup-go@v7
36        with:
37          go-version-file: go.mod
38          cache-dependency-path: go.sum
39
40      - name: Configure GitHub Pages
41        id: pages
42        uses: actions/configure-pages@v6
43
44      - name: Install Hugo Extended
45        run: |
46          curl --fail --location --silent --show-error \
47            --output "${RUNNER_TEMP}/hugo.deb" \
48            "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
49          sudo dpkg -i "${RUNNER_TEMP}/hugo.deb"
50
51      - name: Download OINK
52        run: go mod download github.com/pgsty/oink
53
54      - name: Build
55        run: |
56          hugo --cleanDestinationDir --gc --minify --environment production \
57            --printPathWarnings --panicOnWarning \
58            --baseURL "${{ steps.pages.outputs.base_url }}/"
59
60      - name: Upload Pages artifact
61        uses: actions/upload-pages-artifact@v5
62        with:
63          path: public
64
65  deploy:
66    name: Deploy
67    environment:
68      name: github-pages
69      url: ${{ steps.deployment.outputs.page_url }}
70    runs-on: ubuntu-latest
71    needs: build
72    steps:
73      - name: Publish
74        id: deployment
75        uses: actions/deploy-pages@v5

这是 OINK Starter 内置的工作流。几处不能删:

  • 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.yaml 里的 baseURL 换成这个域名。发布流程需要产物里带 CNAME 文件时, 把它放进 static/CNAME,Hugo 会原样复制到 public/。

OINK Starter 内置 .github/workflows/cloudflare-pages.yaml,使用 Direct Upload。 严格构建留在 GitHub Actions,Wrangler 把同一份 public/ 产物上传到 Cloudflare Pages 项目。

  1. 创建一个 Direct Upload Pages 项目。项目名默认与仓库相同,也可用仓库变量 CLOUDFLARE_PROJECT_NAME 覆盖。
  2. 添加仓库 secrets:CLOUDFLARE_ACCOUNT_ID 与 CLOUDFLARE_API_TOKEN。token 需要 Account → Cloudflare Pages → Edit 权限。
  3. 手动运行一次 Deploy to Cloudflare Pages。设置仓库变量 CLOUDFLARE_PAGES_ENABLED=true 后,每次推送 main 才自动部署。
  4. 规范 URL 默认是 https://<project>.pages.dev/;自定义域名成为生产地址时设置 CLOUDFLARE_SITE_URL。

workflow 固定 Hugo Extended 0.165.0,从 go.mod 读取 Go 版本,关闭本地模块 workspace,并在上传前用 --panicOnWarning 构建。这是 Starter 用户最可复现的推荐路径。

Cloudflare Git integration 仍然是另一种有效模式:构建命令设为 hugo --gc --minify --printPathWarnings --panicOnWarning,输出目录 public,Hugo 固定 0.165.0,Go 固定 1.27。同一个项目只用 Git integration 或 Direct Upload workflow 其中一种。预览部署仍不等于生产证明;它要按自己的 URL 重建并保持不收录。

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

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

[build.environment]
HUGO_VERSION = "0.165.0"

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

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

任意静态服务器(Nginx / Caddy) — 原样提供 public/ 的内容。下例通过 current 符号链接指向一份发布目录,创建方法见下方离线打包步骤。配置 Nginx 或 Caddy 时,将宿主的站点根目录设为这个链接:

/etc/nginx/conf.d/docs.conf
server {
    listen 80;
    server_name docs.example.com;
    root /var/www/oink/current;
    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 二进制带 withdeploy(hugo version 的输出里能看到),云厂商凭据由标准环境变量或配置文件提供(AWS 上先用 aws s3 ls 确认)。

离线打包 — 网络隔离环境里,在能联网的机器上构建,把产物打成一个包带过去。 先将归档路径换成自己的值,每份产物使用新的发布名称。Linux 宿主上的命令需要 /var/www/oink 的写权限及 GNU mv;current 应不存在或为符号链接, current.next 应不存在。按上例配置 Nginx 或 Caddy,从 current 提供文件; 这是宿主配置,不是 Hugo 选项。

终端
hugo --cleanDestinationDir --gc --minify --panicOnWarning --baseURL "https://docs.internal.example.com/" &&
  tar -czf oink-site-20260929-01.tar.gz -C public .

仅在构建与打包成功后,把这份归档传到 Linux 宿主,再在那里执行以下命令。 每份产物使用新的发布名称:

Linux 宿主
release_dir=/var/www/oink/releases/20260929-01
mkdir -p /var/www/oink/releases
mkdir "$release_dir" &&
  tar -xzf oink-site-20260929-01.tar.gz -C "$release_dir" &&
  test -f "$release_dir/index.html" &&
  ln -s "$release_dir" /var/www/oink/current.next &&
  mv -Tf /var/www/oink/current.next /var/www/oink/current

这组命令只有在新目录创建成功、解压完成且 index.html 存在时才切换 current。 随后检查线上页面,并保留上一份发布目录用于回滚。不要把新包解压覆盖到已有发布目录。

构建时就要用目标环境的 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, nofollow 与 Disallow: /,也不会向分析服务上报数据。

内容安全策略

主题自带的运行时、字体与图标都是同源资源,但仅有 script-src 'self'; style-src 'self' 并不能覆盖普通 OINK 页面。主题会输出行内的 主题初始化与外壳预绘制脚本、初始画布及主题色和字体角色样式,以及部分组件的 style 属性;Markmap 还会增加行内配置与样式。主题不提供通用策略,也不自动生成 CSP 哈希或注入 nonce;部署方需根据实际构建产物制定策略。

额外功能也会改变所需指令:

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

对允许执行的行内脚本和样式块,按最终部署字节计算哈希;或者由托管层为每次响应 同时向策略和对应标签注入新的 nonce。style 标签上的 nonce 不会授权 style 属性, 后者要在 style-src-attr 下单独审查。正文、配置、压缩方式或主题变更后,都要重新 核对哈希。先以 Content-Security-Policy-Report-Only 观察,再验证明暗主题启动、 外壳状态、菜单和所有启用的组件,之后再强制执行。仅扫描资源来源不能证明 CSP 兼容。

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

验收清单

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

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

sitemap.xml、robots.txt、.md 与 llms.txt 这几项的开关在配置总览,Agent 输出的细节见 Agent 支持。

回滚

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

  • GitHub Pages:在 Actions 里找到上一次成功的 Deploy to GitHub Pages 运行,点 Re-run all jobs;或者 git revert 出问题的提交再推一次。
  • Cloudflare Pages / Netlify / Vercel:在部署列表里选上一个成功的部署,用平台的 Rollback / Publish deploy 把它重新设为生产版本。
  • 自建静态服务器:把 current 切回离线打包时保留的上一份发布目录。不要把旧包覆盖到新文件上,否则新版本独有的路径仍会在线上保留。

在 Linux 宿主上,把示例路径换成保留的已知可用版本。符号链接与 GNU mv 的前提同上:

终端
previous_release=/var/www/oink/releases/20260928-01
test -d "$previous_release" &&
  ln -s "$previous_release" /var/www/oink/current.next &&
  mv -Tf /var/www/oink/current.next /var/www/oink/current

通过线上 URL 检查一个旧版代表页,并确认仅在被撤回版本中新增的路径已不再提供。

问题出在主题升级而不是内容时,回滚的是 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: YOUR_OWNER/YOUR_REPO
      repoId: YOUR_REPO_ID
      category: YOUR_CATEGORY
      categoryId: YOUR_CATEGORY_ID
      mapping: pathname
      inputPosition: bottom
      theme: auto
      loading: lazy

repo、repoId、category、categoryId 必须来自同一个仓库及其选定的 Discussion 分类,四个键缺一不可:任何一个缺失或只有空白字符,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 可以从任一方向覆盖全站开关,离页面最近的值优先。

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

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.yml 的 server.headers 里给本地预览加了 Access-Control-Allow-Origin: '*';线上由托管商的响应头配置决定。

隐私与 CSP

  • OINK 不会索取或保存读者的 GitHub 密码与访问令牌,登录与发帖全程在 giscus / GitHub 一侧完成。
  • 评论初始化脚本是主题自带的同源资源,只加入启用了评论的页面,未开评论的页面没有这段脚本。
  • loading: lazy 时,读者滚动到评论区附近才加载 iframe。
  • 站点有严格的内容安全策略时,script-src 和 frame-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.enable 与 type → 页面 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 内置的服务配置。启用前,将 G-YOUR_MEASUREMENT_ID 换成你自己的 GA4 measurement ID:

hugo.yml
services:
  googleAnalytics:
    id: G-YOUR_MEASUREMENT_ID

主题只在 production 环境渲染这段脚本。普通 hugo server 默认使用 development, 不会上报;但 hugo 构建默认 production,即使运行在预览宿主上也一样。PR 与 staging 部署需要明确选择非 production 环境,并将 PREVIEW_URL 设为预览的实际地址:

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

详见预览部署配置。

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

这与网络隔离环境不兼容

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

接其它分析服务

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

layouts/_partials/hooks/head-end.html , 插入位置</head> 之前,在 Google Analytics 模板之前
分析脚本、cookie 同意脚本、主题没提供的 meta 标签
layouts/_partials/hooks/body-end.html , 插入位置页面脚本的最后
只影响交互、不影响首屏的第三方代码

使用 Plausible 时,先把 your-site.example 换成你自己账户中登记的域名,再加入这个钩子:

layouts/_partials/hooks/head-end.html
{{ if hugo.IsProduction }}
<script defer data-domain="your-site.example"
        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/docs/admin/analytics/">

hreflang 的语言代码来自各语言的 locale(本站是 en-US / zh-CN),链接来自 Hugo 的译文关系。1.2.0 实现会从 hreflang 和 og:locale:alternate 中省略 缺失的译文。可见的语言切换器仍可跳到目标语言首页,但这种导航回退不代表译文关系。

博客索引的每一分页使用自身的 canonical URL。从第 2 页起不输出语言备用链接, 因为分页不代表各语言存在一一对应的译文页。这些修正已随 1.2.0 发布; 1.1.0 仍保留之前的行为。

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.8.0 发布
images: [/images/releasenote.webp]
---

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

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

有图时 twitter:card 从 summary 变成 summary_large_image,并多出 og:image 与 twitter: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
---

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

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 也一样。

验证

在自己的站点根目录执行。将 PAGE 换成自己站点实际生成的页面,并按需包含语言前缀:

终端
hugo --gc --minify --printPathWarnings --panicOnWarning
PAGE=public/zh/docs/getting-started/index.html
test -f "$PAGE"

# canonical 应使用真实生产地址;production 的 robots 允许收录。
grep -o '<link[^>]*canonical[^>]*>' "$PAGE"
grep -o '<meta[^>]*robots[^>]*>' "$PAGE"
cat public/robots.txt
head -5 public/sitemap.xml

# 未配置 Google Analytics 时,应没有匹配结果。
grep -o '<script[^>]*googletagmanager[^>]*>' "$PAGE"

在浏览器 Network 面板确认已配置的统计请求使用自己的 measurement ID 或登记域名。 再检查用 --environment staging 构建的预览部署,应没有统计请求。未配置分析时, 两种环境均不应产生统计请求;其他显式启用的集成仍可能访问各自的远程服务。

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

5 - 版本升级

固定已发布的主题版本、验证 1.2.0 变化、迁移旧内容或 Docsy 站点,并在出问题时安全回滚。

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

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

先看发布注记

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

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

升级 Hugo Module

生产站点固定已发布标签或主动选定的不可变 commit,不跟随分支,也不用 @latest。 下面升级到已发布的 v1.2.0 标签;选择后续版本时,先确认已经发布且模块可以解析, 再替换示例中的版本。

终端
hugo mod get github.com/pgsty/oink@v1.2.0   # 已发布的版本标签
hugo mod tidy
hugo mod graph | grep github.com/pgsty/oink

选择标签时,确认模块图显示的就是该版本。主动选定的不可变 commit 通常会记录为 Go 伪版本;只要它解析到预期提交就是有效固定,但不能作为某个命名版本已发布的证据。 提交生成的 go.mod 与 go.sum。使用上面的公开标签时,go.mod 包含:

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

go 1.27.0

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

make dev 和 make check 只为当次命令设置 HUGO_MODULE_REPLACEMENTS, 使用同级主题 checkout。判定发布标签是否可用时,移除该环境变量替换,同时禁用 GOWORK 和 HUGO_MODULE_WORKSPACE。还要检查持久替换与 _vendor/;仅执行 make build 无法证明实际解析的是哪个主题版本。参见 本地预览指南。

使用 Git submodule 时,先确认没有本地修改,再拉取标签并检出精确的公开版本, 不要跟随远端分支:

终端
git -C themes/oink fetch origin --tags
git -C themes/oink checkout --detach v1.2.0
git add themes/oink

验证后提交更新的 submodule 指针。离线归档与克隆则用选定版本的完整内容替换 themes/oink/,确认 theme: 的值仍与目录名一致。安装方式的取舍见 从零建站与其它安装方式。

升级后必做

终端
rm -rf public resources/_gen
env -u HUGO_MODULE_REPLACEMENTS GOWORK=off HUGO_MODULE_WORKSPACE=off \
  hugo --gc --minify --printPathWarnings --panicOnWarning --logLevel info

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

--logLevel info 包含信息级诊断,--panicOnWarning 将警告视为失败。升级 Hugo 之前,先处理当前固定版本发出的弃用提示;诊断级别与移除时间取决于具体的弃用功能。

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

从 1.0 升到 1.1

OINK 1.1.0 升级清单

本清单对应已发布的 v1.1.0。各消费站仍需更新依赖固定版本、重新构建和部署; 主题发布不会自动升级既有站点。

从 1.0.0 升级不需要迁移源码。Hugo Extended 0.160.1 仍是下限,CI 固定使用 0.165.0, 模块的 Go 1.27.0 声明与 1.0.0 相同。在 Hugo 0.160.x 上,非默认通用 zh 与区域中文 目录并存时,需要配置 locale: zh-CN。

选择新固定版本前,检查这些受影响的页面与行为:

语言
32 份界面目录均具有相同的原生消息结构。检查站点语言标签、复数计数与 RTL 方向;正文译文仍由站点负责。
分类法
根页变成术语卡片目录,并提供分类法切换器。检查分类法模板或 CSS 覆盖、作者头像与本地化面包屑。
侧栏
缓存树保留页面有效设置,没有 JavaScript 时也可使用。检查折叠、悬停恢复、移动抽屉与键盘焦点,隐藏内容必须退出焦点顺序。
分组
sidebar_divider: true 保留分区子文档。仅在明确不发布分组自身输出时添加 build.render: never;检查子导航、面包屑、翻页、Print 与 Book 目录。
根菜单
显式 sidebar_root_menu: false 对自根分区也生效;当前可链接的根仍作为位置标记显示。
自定义脚本
若还需支持 1.0.0,先检测 OinkSidebar 与 OinkCommandPalette.registerSearchTail。通过 API 恢复分支状态,不要直接修改 class 或 ARIA 属性。
文章复制
启用图片缩放时,以纯文本与富文本 HTML 复制图片和图注。预览提示不得进入文章复制内容,缩放与键盘操作仍需正常工作。
Print 与 Redoc
检查单页和 Book 聚合 Print、标题与标签页链接,以及真实部署前缀下的本地 Redoc 规范。本地规范路径相对于 static/。

params.ui.image_zoom 与 params.offline_search 仍默认关闭。新搜索钩子不会启用远程 服务,也不会添加查询遥测。params.ui.scroll_spy 与页面级 scroll_spy 在 1.x 中仍 作为 no-op 接受;移除无效补丁不影响普通大纲跟踪。

将受影响的站点级主题副本与新实现比较后再更新或移除。保留旧图片缩放脚本或侧栏 partial,会让站点继续使用旧实现,无法获得上游修复。

在文档站验证本地主题修改时,使用同级主题 checkout,不要提交文件系统模块替换:

终端 — 在 oink.pgsty.com 目录内
make check
make browser
make dev

这些命令验证的是本地 checkout。验收正式版本时,固定已发布标签,在没有模块替换的情况下 构建,并验证部署后的页面。创作与 API 细节见内容分组、 侧栏契约、 搜索动作 与图片缩放。

从 1.1 升级到 1.2

默认外观变化

OINK 1.2.0 默认改为 Paper。需要保留原有外观的站点,在采用此改动前设置 params.ui.preset: slate。preset_menu: true 开启读者切换,默认仍为 false。 Ink 与 Terminal 需要显式设置预设或菜单列表;按钮不显示实验标记,配置启用边界不变。 自定义深色品牌选择器的兼容处理见品牌外观。

OINK 1.2.0 已发布。除上面的默认外观变化外,无需迁移内容源码。 Hugo Extended 下限仍为 0.160.1。更新模块后,按以下清单验收站点:

  • 检查所选预设、明暗图标、键盘与手机菜单、保存的偏好,以及自定义字体和强调色覆盖。 太阳表示亮色,月亮表示暗色;切换风格不得改变保存的明暗偏好。
  • 复查显式导航、隐藏子树、页面链接、博客分页 canonical,以及缺少译文页面的 SEO 备用链接。
  • 检查仅关键词命中的 CJK 搜索摘要,以及带字面百分号的大纲链接。复查 Windows 或挂载内容的编辑、历史与新建子页链接;映射结果必须是仓库相对路径。
  • 检查 JavaScript 被禁用或阻断时的 Landing 内容、指标格式、弹窗与快捷键、 复制回退、Draw.io 操作,以及窄屏上的编号公式。
  • 对图表端点与资源 alt 元数据执行将警告视为失败的构建;非法值现在会警告并采用 安全回退。要有意禁用 PlantUML 或 Draw.io 端点,使用 false 或空字符串。
  • 测试出版或内容转换时,使用修订后的 PDF 与迁移工具。审查 PDF 远程资源开关 和迁移 diff,包括嵌套在列表中的代码示例。消费站升级工具也随 1.2.0 一同发布,可用于批量清点、更新与验证模块版本。

架构、组件、 外壳与迁移契约描述 1.2.0 的正式行为。

内容迁移工具

0.4 的一批 shortcode 已换成当前 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 → 当前语法映射

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

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

从 Docsy 迁移

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

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

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

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

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

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

  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 的 alert、pageinfo、tabpane、card 系列都有当前对应 形态,用上面的迁移工具批量转,--only 一类一类来。

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

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

  • layouts/baseof.html 与公共的 docs / blog baseof*.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> 里加载。主题的功能都不需要它,仍然需要的站点自己引入:

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

用 Docsy blocks/* 搭的首页在 OINK 构建中报 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
    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 前缀

本站的完整门禁是:

终端
make check     # 同级本地主题:构建、输出、翻译与渲染后链接
make browser   # 同级本地主题:无障碍、响应式与交互行为
make build     # go.mod 固定的公开主题,不带本地替换

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

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

源码提交通过验收、公开标签能通过模块代理解析、消费站固定版本及校验和、生产部署 通过验证,是彼此独立的状态。一次绿色的本地构建不能代替其它证据。

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

回滚

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

终端
hugo mod get github.com/pgsty/oink@v1.0.0   # 示例:本站上一个已知可用的标签
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.1hugo 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 ...属性行里的键拼错或不被允许警告列出允许键并忽略坏属性;style 与 on* 同样丢弃。--panicOnWarning 在发布时把它变成失败
shortcode "field": unsupported parameter "colour" at ...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.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 指向另一语言首页已发布的 1.1.0 行为或复制的旧 SEO partial 可能沿用语言切换器的回退1.2.0 开发实现会从 SEO 备用链接中省略缺失译文。检查实际解析的主题版本与模板覆盖;需要译文时补上对应页面。可见语言切换器的首页回退仍然有效
症状原因修法
搜索框有但一直没结果索引没生成检查 params.offline_search 并打开搜索,在 Network 中查看页面 data-td-index-src 指定的请求。生产文件名为 offline-search-index.<语言>.<hash>.json,开发环境不带 hash;hugo server 未生成索引时还要检查 offline_search_on_serve
索引文件请求 404baseURL 不对子路径部署下 baseURL 配错是索引 404 最常见的原因。先在浏览器网络面板看它去哪里取索引,见发布上线
hugo server 下搜不了,构建出来就正常站点把预览期的索引关掉了params.offline_search_on_serve 默认为 true,预览与线上行为一致;配置里显式写成 false 时预览不生成索引,删掉或改回 true
中文搜不到多数不是分词问题中文查询走主题的 CJK 子串回退。先确认那个中文页面的内容进了中文索引(打开中文页面 data-td-index-src 指定的实际 URL),再看分词
新页面搜不到,旧页面正常索引是构建产物重新构建。hugo server 下改了页面要等它重建完
params.search.algolia requires explicit appId, apiKey, and indexName valuesAlgolia 三个键没配全三个键必须显式给全,主题不会替你用别的项目的 DocSearch 凭据。不用 Algolia 就把这段配置删掉
命令面板搜不到内容它与全文检索是两件事索引不可用时命令面板仍然能打开,只是提示索引不可用,页面操作与命令照常,见命令面板

平台

症状原因修法
GitHub Pages 上页面 404 或样式全丢项目站点的 URL 带仓库路径,baseURL 没带用工作流里的 --baseURL "${{ steps.pages.outputs.base_url }}/",别手写。完整工作流见发布上线
GitHub Pages 上「最后修改时间」「贡献者」全空checkout 是浅克隆actions/checkout 加 fetch-depth: 0:enableGitInfo 要读完整历史
Cloudflare Pages 构建报 Hugo 版本太低构建镜像的默认 Hugo 低于主题要求在 Production 和 Preview 两个环境都设 HUGO_VERSION,并设 SKIP_DEPENDENCY_INSTALL=1
托管商构建时拉不到主题构建环境没有 GoHugo 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 都在;确实缺就重新获取同一个固定版本

站点自带检查

构建命令在自己的站点根目录执行。输出检查器是独立脚本,来自与固定发布版本对应的 主题 checkout;将 /path/to/oink、/path/to/my-site/public 和 base URL 换成自己的值。 其余命令是本文档仓库的测试实例,并非每个 OINK 消费站都自带的命令。

零告警构建 , 命令hugo --printPathWarnings --panicOnWarning
重复输出路径、参数非法、外部集成配置不全
输出信任检查 , 命令python3 /path/to/oink/bin/check-output-security.py --public /path/to/my-site/public --base-url https://my-site.example/
四种输出里的每个 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/ 下,不依赖站点测试框架。主题工具路径与待检查的站点产物路径应分别指定,完整例子见断网构建验证。

诊断习惯

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

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

求助渠道

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