本文介绍如何用两个 GitHub 仓库实现 Hugo 博客的自动化构建和部署,同时接入 Vercel 作为免费 CDN 加速方案。

架构概览

  graph TD
    A["🔒 {user}.github.io.source<br/>私有仓库 · 源码"] -->|git push| B["⚙️ GitHub Actions<br/>Hugo Build + Pagefind"]
    B -->|deploy key push| C["📦 {user}.github.io<br/>公开仓库 · 静态文件"]
    C -->|内置部署| D["🌐 GitHub Pages"]
    C -->|Webhook 触发| E["🚀 Vercel CDN"]

核心思路:

  • 源码仓库(私有)负责写作和版本管理
  • 静态文件仓库(公开)只存放 Hugo 构建产物
  • GitHub Pages 和 Vercel 各自从静态文件仓库部署

一、源码仓库配置

1.1 创建仓库

创建私有仓库 {username}.github.io.source,存放 Hugo 项目源码:

├── archetypes/
├── content/
│   └── post/          # 博客文章
├── layouts/           # 自定义模板(覆盖主题)
├── static/            # 静态资源
├── themes/
│   └── even/          # 主题(git submodule)
├── config.toml        # Hugo 配置
└── .github/
    └── workflows/
        └── gh-pages.yml   # CI/CD 配置

1.2 GitHub Actions Workflow

.github/workflows/gh-pages.yml

name: Deploy Hugo Site to Github Pages on Master Branch

on:
  push:
    branches:
      - master
  workflow_dispatch:    # 支持手动触发

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      # 1. 拉取源码(含 submodule 主题)
      - uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      # 2. 安装 Hugo
      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: '0.136.5'
          extended: true

      # 3. 构建静态文件
      - name: Build
        run: hugo --minify

      # 4. 构建搜索索引(可选,使用 Pagefind)
      - name: Build search index
        run: npx pagefind --site public

      # 5. 部署到静态文件仓库
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v4
        with:
          deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
          external_repository: {username}/{username}.github.io
          publish_dir: ./public
          keep_files: false
          publish_branch: master
          cname: www.example.com        # 自定义域名
          commit_message: ${{ github.event.head_commit.message }}

1.3 Workflow 原理

整个流程分 5 步:

  1. Checkout:拉取源码,submodules: true 确保主题代码也被拉取,fetch-depth: 0 获取完整 git 历史(用于 .GitInfo.Lastmod

  2. Setup Hugo:下载指定版本的 Hugo Extended(Extended 版本支持 SCSS/SASS 编译)

  3. Buildhugo --minify 将 Markdown 编译为 HTML,输出到 ./public 目录,--minify 压缩 HTML/CSS/JS

  4. Search Index(可选):Pagefind 扫描 ./public 中的 HTML 文件,生成静态搜索索引到 ./public/pagefind/

  5. Deploy:使用 peaceiris/actions-gh-pages./public 目录的内容 push 到目标仓库。keep_files: false 表示每次清空目标仓库再推送,确保内容一致

1.4 Deploy Key 配置

源码仓库通过 SSH Deploy Key 向静态文件仓库推送,配置步骤:

# 生成密钥对
ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
  • 公钥 deploy_key.pub → 添加到 {username}.github.io 仓库:Settings → Deploy keys → Add deploy key → 勾选 “Allow write access”
  • 私钥 deploy_key → 添加到 {username}.github.io.source 仓库:Settings → Secrets and variables → Actions → New repository secret,名称为 ACTIONS_DEPLOY_KEY

⚠️ 重要限制:Deploy Key push 的 commit 不会触发目标仓库的 GitHub Actions workflow(GitHub 安全机制,防止循环触发)。但 GitHub Pages 的内置 pages-build-deployment 和 Vercel 的 Webhook 不受此限制。

1.5 Mermaid 图表支持

Hugo 不内置 Mermaid 支持,但通过代码块渲染钩子可以轻松实现,且不需要修改主题代码。

Hugo 的模板优先级是 项目 layouts/ > 主题 layouts/,只需在项目根目录添加文件即可。

只需两个文件:

文件一:代码块渲染钩子

layouts/_default/_markup/render-codeblock-mermaid.html

<pre class="mermaid">
  {{ "{{" }} .Inner | htmlEscape | safeHTML {{ "}}" }}
</pre>
{{ "{{" }} .Page.Store.Set "hasMermaid" true {{ "}}" }}
  • 将 mermaid 代码块输出为 <pre class="mermaid"> 标签
  • 标记当前页面包含 Mermaid 内容

文件二:覆盖 baseof.html

将主题的 baseof.html 复制到 layouts/_default/baseof.html,在 </body> 前添加:

{{ "{{" }} if .Store.Get "hasMermaid" {{ "}}" }}
<script type="module">
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
  mermaid.initialize({ startOnLoad: true });
</script>
{{ "{{" }} end {{ "}}" }}

只在包含 Mermaid 代码块的页面才加载 JS,按需加载不影响其他页面性能。语法与 Obsidian 完全一致,两边可以无缝复制。

二、静态文件仓库配置

2.1 创建仓库

创建公开仓库 {username}.github.io。这是一个特殊的仓库名,GitHub 会自动将其识别为 GitHub Pages 站点。

这个仓库不需要手动维护,所有内容由源码仓库的 workflow 自动推送。

2.2 GitHub Pages 配置

  1. 进入仓库 Settings → Pages
  2. Build and deployment → Source → 选择 “Deploy from a branch”
  3. Branch 选 master,目录选 / (root)
  4. 保存

选择 “Deploy from a branch” 而不是 “GitHub Actions” 的原因:

  • Deploy Key push 的 commit 不会触发自定义 workflow
  • 但 “Deploy from a branch” 使用的是 GitHub 内置的 pages-build-deployment,不受此限制
  • 每次源码仓库 push 新内容过来,GitHub Pages 会自动部署

2.3 自定义域名

如果需要自定义域名:

  1. 在 DNS 服务商添加 CNAME 记录:www.example.com{username}.github.io
  2. workflow 中的 cname: www.example.com 会自动在仓库根目录创建 CNAME 文件
  3. 在 GitHub Pages 设置中勾选 “Enforce HTTPS”

三、Vercel 免费部署方案

3.1 为什么用 Vercel

  • 全球 CDN 加速,国内访问速度优于 GitHub Pages
  • 免费 HTTPS
  • 自动部署,零配置
  • 支持自定义域名
  • Hobby 计划完全免费

3.2 接入步骤

  1. 注册 Vercel,使用 GitHub 账号登录
  2. Import Project → 选择 {username}.github.io 仓库
  3. Framework Preset 选择 Other(因为这是已经构建好的静态文件,不需要 Vercel 再构建)
  4. Build Command 留空或设为 echo "skip build"
  5. Output Directory 设为 .(根目录)
  6. 点击 Deploy

3.3 Deployment 原理

  sequenceDiagram
    participant R as GitHub Repo
    participant W as Vercel Webhook
    participant V as Vercel Build
    participant C as Vercel CDN

    R->>W: push 事件(HTTP POST)
    W->>V: 触发构建
    V->>R: 拉取最新代码
    V->>V: 构建阶段(静态文件跳过)
    V->>C: 分发到全球边缘节点
    C-->>C: 用户就近访问
  1. Webhook 监听:Vercel 在你导入项目时,会在 GitHub 仓库自动注册一个 Webhook。每次仓库收到 push 事件,GitHub 会向 Vercel 发送 HTTP POST 通知

  2. 拉取代码:Vercel 收到通知后,从 GitHub 拉取最新代码

  3. 构建阶段:由于静态文件仓库已经是构建好的 HTML/CSS/JS,不需要额外构建。Vercel 会检测到没有框架配置,直接将文件作为静态资源处理

  4. 部署到 CDN:Vercel 将静态文件分发到全球边缘节点(Edge Network),用户访问时自动路由到最近的节点

  5. Deployment 类型

    • Production Deployment:master 分支的 push 会触发生产环境部署,绑定到自定义域名
    • Preview Deployment:其他分支的 push 会生成预览 URL(本方案中不涉及,因为静态文件仓库只有 master 分支)

3.4 Vercel 自定义域名

  1. Vercel Dashboard → 项目 → Settings → Domains
  2. 添加自定义域名,如 www.example.com
  3. 按提示在 DNS 服务商添加 CNAME 记录:www.example.comcname.vercel-dns.com
  4. Vercel 自动签发 SSL 证书

💡 可以让 GitHub Pages 和 Vercel 使用不同的域名,比如 {username}.github.io 走 GitHub Pages,www.example.com 走 Vercel。

3.5 Node.js 版本配置

Vercel 构建时需要 Node.js 环境。如果遇到版本问题:

  • 方式一:Vercel Dashboard → 项目 → Settings → Build and Deployment → Node.js Version → 选择 24.x
  • 方式二:在仓库根目录添加 package.json
{
  "engines": {
    "node": "24.x"
  }
}

四、完整部署流程

  sequenceDiagram
    participant D as 开发者
    participant S as 源码仓库
    participant A as GitHub Actions
    participant T as 静态文件仓库
    participant GP as GitHub Pages
    participant VC as Vercel CDN

    D->>S: 1. git push(写文章)
    S->>A: 2. 触发 workflow
    A->>A: 2a. Hugo 构建 Markdown → HTML
    A->>A: 2b. Pagefind 生成搜索索引
    A->>T: 2c. Deploy Key push 静态文件
    T->>GP: 3a. 内置部署自动触发
    T->>VC: 3b. Webhook 自动触发
    GP-->>GP: 部署完成
    VC-->>VC: 部署到全球 CDN

整个过程从 push 到上线通常在 1 分钟内完成。

五、注意事项

  1. Deploy Key vs PAT:Deploy Key 只能访问单个仓库,更安全;PAT 可以访问所有仓库,但 PAT push 的 commit 能触发目标仓库的 workflow。根据需求选择

  2. 主题作为 Submodule:使用 git submodule add 引入主题,workflow 中 submodules: true 确保主题被正确拉取。升级主题只需 git submodule update --remote

  3. 模板覆盖:Hugo 的模板优先级是 项目 layouts/ > 主题 layouts/,自定义模板放在项目的 layouts/ 下即可,不需要修改主题代码

  4. keep_files: false:每次部署会清空目标仓库再推送。如果在静态文件仓库手动添加了文件(如 CNAME),需要通过 workflow 配置(如 cname 参数)来保留

  5. 构建缓存:如果构建产物没有变化,peaceiris/actions-gh-pages 会跳过 push(nothing to commit),不会产生多余的 commit