跳转到主要内容

发布上线

把 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 里固定的版本,见版本升级