跳转到主要内容

发布上线

把 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 分支。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: 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.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_IDCLOUDFLARE_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/ 的内容整个铺上去:

/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
站点在 outputs.home 里开了 LLMS 时,首要语言与每种已启用语言根都能访问 llms.txt
已启用语言
每种语言的文档页、博客页、首页都能打开,语言切换落到对应页面而不是首页
外观与交互
深浅色切换、打印视图、代表性组件(提示块、标签页、代码块复制)正常
404
访问一个不存在的路径,看到站点自己的 404 页

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

回滚

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

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

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