这篇文章最初写于 2021 年,当时我刚搭建好自己的站点。具体实现后来已经变化,因此这次修订围绕仍然有用的部分重新组织教程。

持久的经验来自区分源码、构建环境、生成产物和公开托管。主题和产品界面变化后,这套模型仍然适用。

先理解系统,再选择工具

GitHub Pages 托管什么

GitHub Pages 是静态站点托管服务。它向读者提供 HTML、CSS、JavaScript、图片、字体等静态文件。Pages 可以先从源码构建这些文件,但不会运行长期驻留的应用服务器,也不会在每次请求时执行服务端 PHP、Ruby 或 Python。

这条边界带来三条实用规则:

  • 浏览器端 JavaScript 可以请求外部 API,但写入仓库或随 JavaScript 下发的凭据都是公开的。密钥和高权限操作不应放在 Pages 站点中。
  • 账号、私有数据、支付、可信表单处理等功能需要独立后端,或采用安全模型合适的托管服务。
  • 静态站点生成器在部署前完成编译;Jekyll 生成文件,再由 Pages 对外提供。

Pages 支持两种 URL 形态。用户或组织站点通常存放在 <owner>.github.io 仓库并发布在域名根路径;项目站点属于某个仓库,默认发布在 /<repository>/ 下。生成链接和资源地址时,不能忽略这段额外路径。

站点类型 典型仓库 默认 URL 常见 URL 注意点
用户或组织站点 <owner>.github.io https://<owner>.github.io/ 基础路径通常为空
项目站点 任意项目仓库 https://<owner>.github.io/<repository>/ 链接和资源必须包含项目基础路径

仓库可见性、入口文件、账号套餐和设置界面等当前细节,应以官方站点创建指南为准,并在每次配置时重新核对。

四阶段模型

一套可维护的部署可以分成四个阶段:

  1. 源码: Git 中保存的 Markdown、布局、数据、配置、代码和依赖声明。
  2. 构建: 由锁定的工具链把源码转换成静态目录,并报告错误。
  3. 产物: 已通过自动检查的那一个生成目录。
  4. 部署: GitHub Pages 发布该产物,并把它关联到 Pages 环境和域名。

分开这四个阶段,许多故障便有了明确归属。Markdown 错误属于源码或构建;_site 中缺少文件属于构建或产物;有效产物没有到达公开 URL 属于部署;公开页面中的图片指向错误路径,虽然表现于浏览器,通常仍是构建配置问题。

选择发布路径

GitHub 目前支持从分支发布或通过自定义 GitHub Actions 工作流发布。两者都通过 Pages 发布,构建责任由不同主体承担。

从分支发布

在仓库 Pages 设置中选择 Deploy from a branch,再指定分支以及其根目录或 /docs 目录。GitHub 会发布该位置的变更;除非发布源中存在 .nojekyll,否则默认使用 Jekyll 构建。

如果发布源本来就是静态产物,或者传统 Jekyll 站点完全适配 GitHub 内置环境,这条路径最省事。代价是对 Ruby、Jekyll、插件、构建前处理和验证步骤的控制较少。若必须使用不受支持的插件或其它生成器,应把构建移入 Actions,由工作流直接执行所需的自定义构建。

通过 GitHub Actions 发布

当项目需要明确工具链、任意生成器、发布前测试,或需要将源码和生成产物彻底分开时,选择 GitHub Actions。当前官方流程是:

  1. 检出源码;
  2. 配置 Pages 元数据;
  3. 构建静态站点;
  4. 上传一个 Pages 产物;
  5. 由拥有 Pages 与身份令牌必要权限的作业部署该产物。

下面的骨架使用本文修订日 GitHub 官方文档展示的 Actions 主版本。复制到新仓库前,应与当前的自定义工作流指南或仓库 Pages 设置提供的工作流模板比较。

name: pages

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read
  pages: read

concurrency:
  group: pages-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'production' }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      JEKYLL_ENV: production
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
      - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
        with:
          bundler-cache: true
      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
      - run: bundle exec jekyll build --trace --baseurl "${{ steps.pages.outputs.base_path }}"
      - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
        with:
          path: ./_site

  deploy:
    if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
    needs: build
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        id: deployment
        uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

这段内容是一份架构示例。注释标出本文修订日核验的 action 版本,每一条 uses: 则固定到当时实际核验的完整 commit;GitHub 的安全使用指南把完整 commit SHA 视为不可变引用。固定完整 SHA,再让 Dependabot 或明确的维护流程提出可审阅的更新。Ruby 版本应保存在 .ruby-version,或在工作流中显式设置;持续维护的 ruby/setup-ruby action能读取项目版本,执行 bundle install 时遵循 Gemfile.lock,并通过 bundler-cache 缓存依赖。构建作业只获得仓库和 Pages 读权限;只有部署作业获得写入及身份令牌权限,并只允许 main 分支发布。工作流级并发控制让生产提交依次完成整套构建与部署,避免较慢的旧提交随后覆盖新站点;同一拉取请求的新运行则取消旧运行。configure-pages 还会把项目站点基础路径传给 Jekyll。在构建和上传之间加入项目自己的检查命令,让拉取请求执行构建但不执行部署;同时把 github-pages 环境保护作为第二道分支护栏。使用其它生成器时,可以替换 Ruby 与构建步骤,但应保留产物与部署阶段。

按责任边界选择

问题 分支发布 自定义 Actions
谁定义构建环境? GitHub 内置的 Pages/Jekyll 环境 仓库工作流和锁文件
能否运行任意构建前检查? 有限 可以
是否要把生成产物保存在源码分支? 有时需要 不需要;上传产物即可
适合场景 小型静态站点或传统 Jekyll 站点 有测试、定制或多阶段构建的站点

生产环境只维护一条部署路径。本地脚本和持续集成(CI)应产生相同的产物结构,并且只授权这条路径发布。

让 Jekyll 构建可以复现

分清各个 Ruby 工具的职责

Jekyll 是 Ruby gem;RubyGems 负责分发 gem;Gemfile 声明项目直接需要的 gem;Bundler 解析并安装这张依赖图,把精确版本记录在 Gemfile.lock;bundle exec 则在该依赖环境内运行命令,避免无声调用另一个系统级可执行文件。

官方的 Jekyll 入门文档与 Bundler 指南解释了这层关系。命令是 bundle,不是 buddle;通常的本地入口是 bundle exec jekyll serve。

使用自定义 Actions 构建时,最小依赖文件可以从下面开始:

source "https://rubygems.org"

gem "jekyll"

group :jekyll_plugins do
  gem "jekyll-feed"
end

运行 bundle install 解析并安装依赖。对于网站这样的应用,应提交 Gemfile 和 Gemfile.lock;Bundler 的锁文件文档说明了后续安装为何能够复用同一份依赖快照。需要升级时,用 bundle update <gem> 或等价的受审依赖升级流程明确更新,重新构建,再提交产生的锁文件变更。

从分支发布是一个特例:GitHub 内置 Jekyll 环境、支持的插件和 github-pages gem 决定生产兼容性。应按 GitHub 当前的 Pages 与 Jekyll 文档核对本地 Jekyll 版本及第三方插件的支持情况。

建立唯一的本地命令路径

从全新检出开始,基本循环应该短到每位贡献者都会实际使用:

bundle install
bundle exec jekyll serve --livereload

推送前应执行生产构建和项目检查:

bundle exec jekyll build --trace
bundle exec jekyll doctor

GitHub 也维护了一份本地 Jekyll 测试指南。项目应记录所需 Ruby 版本,并把链接、HTML、无障碍或浏览器检查包装进一个统一验证命令。CI 调用同一个入口,使本地与远端验证保持等价。

若要增强可复现性,应定期用空依赖缓存测试全新检出。长期使用的本机环境可能掩盖未声明的 gem、被忽略的生成文件,或者只有全新 Linux runner 才会暴露的平台依赖。

测试生成产物

jekyll build 成功后,CI 还应验证以下站点行为:

  • 预期入口文件存在于 _site;
  • 站内链接和引用的本地资源可以解析;
  • 项目站点 URL 正确遵循 baseurl;
  • 依赖 JavaScript 的页面仍有有意义的静态 HTML;
  • 密钥、草稿、缓存和仅供源码使用的文件没有进入产物;
  • 浏览器能无控制台错误地加载代表性桌面与移动页面;
  • 只有已经通过检查的同一个产物才能进入部署作业。

若视觉审阅很重要,可以为拉取请求保存预览产物,让审阅者检查已经通过测试的候选输出。

安全配置自定义域名

域名配置横跨两个控制面:GitHub Pages 必须知道哪个域名属于该站点,DNS 提供商必须把这个域名路由到 Pages。应先在 Pages 设置中配置自定义域名,再发布 DNS 记录;GitHub 建议验证域名,以降低域名接管风险。

对于 www.example.com 一类子域名,通常使用 CNAME 直接指向 <owner>.github.io,不能带仓库路径。根域名可以使用 DNS 提供商支持的 ALIAS 或 ANAME,也可以使用 Pages 当前的 A 和可选 AAAA 记录。不要从旧文章复制 IP 地址:修改 DNS 时应阅读当前的自定义域名说明。除非另有经过论证和加固的设计,否则避免通配符记录。

直接检查公共 DNS:

dig example.com A
dig www.example.com CNAME

在 PowerShell 中,Resolve-DnsName example.com 可以完成同类外部检查。DNS 传播与证书签发都是异步过程,因此需要区分“记录尚未公开可见”和“记录指向了错误目标”。

DNS 正确后,启用 Enforce HTTPS。GitHub 的 HTTPS 指南覆盖证书签发与混合内容故障。所有样式表、脚本、图片和字体都应通过 HTTPS 或同源加载;否则即使页面证书有效,浏览器仍可能拦截不安全资源。

从分支发布时,设置自定义域名可能在源码中创建 CNAME 文件;使用自定义 Actions 工作流时,Pages 设置或 API 才是权威来源,仓库中已有的 CNAME 并不能启用域名。切换发布模式时应重新核对当前文档。

按失败阶段排查故障

先查看最具体的证据:失败的工作流作业、其中第一条有意义的错误、生成产物和 Pages 设置。邮件通知与浏览器现象只是次级线索。

现象 首先检查 可能的修正
本地构建失败 bundle exec jekyll build --trace 的第一条错误 在部署前修正 front matter、配置、依赖、插件或源码语法
本地通过但 CI 失败 Ruby/平台假设、被忽略文件、文件名大小写、锁文件、生产环境 使用 CI 的输入从全新检出复现
工作流成功但站点内容仍旧 已部署的运行记录、产物身份、部署作业条件、Pages 发布源 确认预期提交确实生成并部署了该产物
根 URL 返回 404 Pages 发布源、仓库命名、产物根目录入口文件 把入口文件放入配置的发布源或产物根目录
HTML 正常但 CSS 或图片返回 404 生成 URL、url、baseurl、绝对与相对路径 针对实际部署的用户站点或项目站点路径生成 URL
自定义域名或证书失败 公共 DNS 记录、冲突的额外记录、Pages 域名设置 修正 DNS,等待传播,必要时再重启证书签发
HTTPS 页面报告不安全内容 浏览器网络面板和生成后的资源 URL 替换 http:// 资源引用并重新构建

使用内置 Jekyll 构建时,参考 GitHub 当前的构建错误指南。使用自定义工作流时,应让构建在 pull_request 事件运行,使错误在合并前出现,并通过 Actions 日志定位失败阶段。

如果配置看起来正确但公开站点仍有故障,应先检查 GitHub Status 和仓库的 Pages 部署历史,再修改 DNS 或代码。同时尝试多种猜测性修复,会破坏定位失败阶段所需的证据。

将易变事实留在架构之外

这一领域有些事实本来就具有时效性:套餐资格、额度、构建超时、受支持的 gem 与插件、runner 镜像、Actions 主版本、DNS 地址、计费方式和设置界面。只有它们会影响当前决策时,才应该重新查证。

例如,GitHub 维护了一份独立的 Pages 限制页面。链接到这份实时契约,比把今天的流量或构建频率数值抄入教程并当作永久限制更可靠。

每次新部署或大规模维护前,可以复核下面的短清单:

  1. 当前套餐下,Pages 是否支持仓库可见性和预期用途。
  2. 所选分支或工作流是否仍是配置中的发布源。
  3. Ruby、Jekyll、插件、Actions 与锁文件是否描述同一套兼容构建。
  4. 全新本地或 CI 构建是否产出了后续检查实际使用的产物。
  5. DNS 值和 HTTPS 状态是否符合 GitHub 当前域名文档。
  6. 当前限制是否容纳站点生成体积、构建时间与预期流量。

这份清单能够跨越产品改版,因为它追问权威事实位于哪里。具体答案可能变化,获取并验证答案的方法不会变化。

官方资料