这篇文章最初写于 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>/ | 链接和资源必须包含项目基础路径 |
仓库可见性、入口文件、账号套餐和设置界面等当前细节,应以官方站点创建指南为准,并在每次配置时重新核对。
四阶段模型
一套可维护的部署可以分成四个阶段:
- 源码: Git 中保存的 Markdown、布局、数据、配置、代码和依赖声明。
- 构建: 由锁定的工具链把源码转换成静态目录,并报告错误。
- 产物: 已通过自动检查的那一个生成目录。
- 部署: 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。当前官方流程是:
- 检出源码;
- 配置 Pages 元数据;
- 构建静态站点;
- 上传一个 Pages 产物;
- 由拥有 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 限制页面。链接到这份实时契约,比把今天的流量或构建频率数值抄入教程并当作永久限制更可靠。
每次新部署或大规模维护前,可以复核下面的短清单:
- 当前套餐下,Pages 是否支持仓库可见性和预期用途。
- 所选分支或工作流是否仍是配置中的发布源。
- Ruby、Jekyll、插件、Actions 与锁文件是否描述同一套兼容构建。
- 全新本地或 CI 构建是否产出了后续检查实际使用的产物。
- DNS 值和 HTTPS 状态是否符合 GitHub 当前域名文档。
- 当前限制是否容纳站点生成体积、构建时间与预期流量。
这份清单能够跨越产品改版,因为它追问权威事实位于哪里。具体答案可能变化,获取并验证答案的方法不会变化。
评论
评论公开保存在 GitHub Discussions。只有在你手动显示评论或开启自动加载后,本页才会连接 giscus.app 与 GitHub,并发送当前页面路径。首次加载约 0.13 MB,实际用量随评论内容变化。请勿留下私人信息。