↓ 跳过正文

用 Hugo + Blowfish 在 GitHub Pages 上搭一个中文博客

·4577 字·10 分钟
WXG
作者
WXG

这是本站的第一篇文章。与其写"Hello World",不如把搭这个博客的过程记下来——既能帮到想做同样事情的人,也能让我下次重装时不用再搜一遍。

我想要的是什么
#

需求 原因
用 Markdown 写作 不想为了写篇文章去点富文本编辑器
推送即上线 不想登服务器、不想手动上传
0 成本 个人博客不值得为它付服务器钱
自己的域名 将来换托管平台时链接不作废
中文显示和搜索正常 很多静态博客主题在这两点上是坏的

技术选型
#

静态站点生成器:Hugo。 对比过 Hexo 和 Astro:

  • Hugo:一个 exe 文件、零依赖、构建毫秒级,主题多且维护规范;
  • Hexo:中文教程最多,但要装 Node + 一堆 npm 插件,插件和新版 Node 偶尔打架;
  • Astro:主题最好看,但依赖多、迭代快,教程容易过期。

主题:Blowfish。 深色模式、分类标签、站内搜索、代码复制按钮都是原生支持,中文文档齐全。

托管:GitHub Pages + Cloudflare。 静态文件放在 GitHub 免费托管,前面套一层 Cloudflare 做 CDN——因为国内直连 GitHub Pages 的裸地址经常很慢。

整体链路是这样的:

本地写 Markdown  →  git push  →  GitHub Actions 自动构建
                                        ↓
        Cloudflare(CDN/HTTPS)  ←  GitHub Pages(静态文件)

日常只有两个动作:写 .md 文件、点一下推送。构建在云端完成,本地不需要装编译环境。

搭建步骤
#

1. 装 Hugo(版本必须锁死)
#

这是第一个坑,先说结论:Hugo 不能装最新版。

Blowfish 在主题配置里声明了它能接受的版本区间(我这份是 min = 0.162.0、max = 0.165.0)。装一个比 max 更新的版本,Hugo 会因为不满足主题声明而直接报错退出——不是"可能有问题",是构建失败。

所以做法是:手动下载指定版本的压缩包 + 校验 SHA256 + 解压到固定目录 + 加进 PATH。

# 本机安装(Windows)
# 1) 下载指定版本
#    https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_extended_0.165.0_windows-amd64.zip
# 2) 校验(下载页会给出 SHA256,务必对一遍)
Get-FileHash .\hugo_extended_0.165.0_windows-amd64.zip -Algorithm SHA256
# 3) 解压出 hugo.exe 放到固定目录,并把该目录加入用户 PATH
# 4) 验证:必须带 +extended
hugo version
# hugo v0.165.0+extended windows/amd64

网页上 “extended” 版本是必须的——主题配置里写了 extended = true。

2. 用 submodule 引入主题
#

git init -b main
git submodule add --depth 1 -b main \
  https://github.com/nunocoracao/blowfish.git themes/blowfish

为什么用 submodule 而不是直接把主题文件复制进来:主仓库里只保存一个"指针"(记录主题的 commit),主题那一大堆文件不会进你的仓库。我这份主题的完整历史接近 90 MB,而我的博客仓库只有几百 KB。升级主题时也只是一条命令。

3. 配置:中文、搜索、代码复制
#

Blowfish 的配置放在 config/_default/ 下的多个文件里,不是根目录一个 hugo.toml(这一点跟着网上旧教程做很容易踩空)。几个关键项:

# config/_default/hugo.toml
baseURL = "https://你的域名/"
defaultContentLanguage = "zh-cn"
timeZone = "Asia/Shanghai"      # 不写的话日期可能按 UTC 走,差 8 小时
hasCJKLanguage = true           # 中文按字统计字数与阅读时间
mainSections = ["posts"]        # 明确"文章区",避免关于页混进文章流
summaryLength = 70
# config/_default/params.toml
enableSearch = true             # 站内搜索
enableCodeCopy = true           # 代码块复制按钮

4. 自动部署
#

在仓库里放一个 GitHub Actions 工作流(.github/workflows/deploy.yml),推送到 main 就自动构建并发布。三个关键参数:

参数 作用 不写的后果
submodules: recursive 把主题 submodule 一起拉下来 themes/blowfish 是空目录,构建失败
fetch-depth: 0 拉完整提交历史 文章的"最后更新"时间不准
--baseURL "${{ steps.pages.outputs.base_url }}/" 让 GitHub 自动给出站点地址 项目站点会因地址带子路径而样式全丢

最后别忘了在仓库 Settings → Pages → Source 里选 GitHub Actions——不选的话工作流会在部署那步报 HttpError: Not Found。

搭建期间踩到的 8 个坑
#

# 现象 原因 解决
1 构建直接失败 Hugo 版本超出主题声明的 max 锁定主题支持的版本,别装最新版
2 CI 里装 Hugo 那步 404 .deb 资产名写成了 Linux-64bit 实际是 linux-amd64.deb(小写)
3 文章页显示「1 词 · 1 分钟阅读」 没开 hasCJKLanguage,整段中文被当成一个"词" 开启后按字统计
4 首页有头像,但整个站点构建失败 头像放进了 static/,而主题用 resources.Get 从 assets/ 取图 头像放 assets/img/
5 搜标签名搜不到文章 主题的搜索索引不含 tags/categories,而且混进了分类页、标签页这类空页面 覆盖索引模板,只收录真实文章并加上这两个字段
6 线上"页面能打开但样式全丢" baseURL 和真实地址不一致(项目站点带 /仓库名/ 子路径) 让 baseURL 指向真实地址,工作流里用 pages 的输出
7 以为草稿是"私密"的 draft: true 只是不构建,文件本身仍在公开仓库里 敏感内容从一开始就不要写进仓库
8 本地图片正常、线上裂图 Obsidian 默认写 Wiki 链接 ![[图.jpg]],Hugo 不认 关掉 Wiki 链接,新链接格式改成相对路径

其中两个值得多说一句:

坑 2 的教训:那个错误的文件名是我从主题自己的 CI 配置里抄来的——“别人的 CI 里这么写"不等于"一定能用”。最后是把两个候选文件名都请求一遍(一个 404、一个 200)才确定的。凡是涉及具体版本号、文件名的东西,下载一次比读十篇教程都可靠。

坑 5 的教训:搜索是静态博客最容易"看起来有、其实没有"的功能。判断它有没有用,不能看配置文件里写没写 enableSearch = true,而要实际搜几个中文词:连续词、带空格的多词、标签名、分类名。我是搜"标签名"时发现搜不到的——因为索引里根本没有这个字段。

上线之后才暴露的 3 个问题
#

“本地能跑"和"挂到自己的域名上一切正常"是两件事。下面这三个,都是绑定自定义域名之后才冒出来的。

问题 1:域名绑好了,页面却"没样式”(基地址没跟上域名)
#

现象:https://blog.wxgg.eu.cc/ 能打开、文字都在,但完全没有样式——SVG 图标撑成巨大的一坨、头像不显示、布局全乱。

排查:查看页面源代码,两个线索立刻暴露问题:

  • <link rel="canonical"> 指向的是 https://<用户名>.github.io/blog/,而不是我的域名;
  • 所有静态资源都是根相对路径 /blog/css/...——站点现在挂在域名根目录,浏览器于是去请求 https://blog.wxgg.eu.cc/blog/css/...,全部 404。

根因:Hugo 生成资源路径用的是 baseURL,而我在工作流里让它取 GitHub Pages 的自动地址:

hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"

这份 HTML 是在自定义域名生效之前构建的。那时 Pages 给出的地址还是 https://<用户名>.github.io/blog,Hugo 就把 /blog/ 写进了每一个资源路径;域名生效后站点换到根目录,路径全部对不上。

修法(两条路,我最后选了后者):

做法 换域名时要做什么 优缺点
把域名写死在 hugo.toml 的 baseURL 改 1 行配置 + 重新构建 确定、直观,但容易忘
继续用 Pages 的地址,但构建前统一规范成 https 只改 Pages 设置,代码不用动 自适应;地址永远跟着域名走,代价是构建时多一行处理

最关键的一句:不管走哪条路,绑域名/换域名之后都必须重新构建一次。因为 canonical、sitemap、RSS 这些绝对地址本来就是跟着域名走的,不重建就不会更新——我这次就是"域名先生效、构建是上一次的产物",才出现"能打开但没样式"。

问题 2:Pages 没启用,工作流直接报 HttpError
#

第一次运行 Actions 就红了,报错原文:

Setup Pages
HttpError: Not Found - https://docs.github.com/rest/pages/pages#get-a-apiname-pages-site
Get Pages site failed. Please verify that the repository has Pages enabled and configured to build using GitHub Actions

根因:仓库还没启用 Pages。Actions 想替你部署,但 Pages 功能没开,它去查 Pages 站点时拿到 404。

修法:仓库 Settings → Pages → Build and deployment → Source 选 GitHub Actions。报错其实写得很清楚,但如果没注意到那个 “Source” 下拉框,很容易误以为是自己工作流写错了。

问题 3:站点的绝对地址全是 http
#

修好前两个问题后,我去检查 sitemap.xml 和 RSS,发现里面的链接全是 http://(sitemap 4 处、RSS 7 处),og:url 也是 http。

根因:Pages 在没有启用 “Enforce HTTPS” 时,会把 http://域名 交给构建,Hugo 就照着生成了所有绝对地址。

有意思的是,那个开关我想勾也勾不上,GitHub 提示:

Unavailable for your site because your domain is not properly configured to support HTTPS

因为域名在 Cloudflare 上开着代理(橙色云),GitHub 无法为它签发自己的证书,所以这个开关一直不可用。但这不影响访客:HTTPS 由 Cloudflare 的证书提供(实测签发者是 Google Trust Services,覆盖 *.wxgg.eu.cc,有效期到 2026-10-28)。

修法:构建前把拿到的地址统一替换成 https:

BASE="${BASE/http:\/\//https:\/\/}"
hugo --minify --baseURL "$BASE/"

改完 sitemap / RSS / og:url 全部变成 https。(文件里剩下的 http://www.sitemaps.org/... 是 XML 命名空间,属于正常现象。)

我给自己加的两道保险
#

这些坑有个共同点:页面"看起来是好的",靠人眼很容易漏。所以我加了两道自动检查。

① 构建期断言:构建完成后、上传产物之前,直接检查刚生成的首页里引用的样式表路径,是否与本次构建用的地址一致。这一步纯本地、不碰网络:路径对不上,就让这次运行直接失败。这样一来,“地址不一致导致没样式"会变成 CI 上的红叉,而不是等我偶然发现。

这一条其实是我改过的第二版。最初写的是”部署后自检"——部署完成后去请求线上首页和它引用的样式表,任一不是 200 就判失败。它有两个坑,最后被我整个删掉了:

  • 必须带重试:我第一版只检查一次,结果部署刚结束、CDN 还没切到新内容,它抓到的是上一版 HTML,于是误报失败——站点其实已经好了;
  • 它依赖构建机能不能访问你的站点:我的站点在 Cloudflare 后面,GitHub 的构建机去请求首页,直接返回 403,这个自检在 CI 里根本跑不通(日志里写着"首页返回 403")。

教训是:能在本地判定的问题,就不要交给"线上探测"去判定。线上还隔着一层 CDN、一层防火墙,构建机能观测到的世界,和你的浏览器并不是同一个。

② 发布前检查:推送前跑一个脚本,扫内容里有没有真实密钥或内网 IP、有没有引用不存在的图片(坏图)、有没有超过 500KB 的大图。它分两档:.md 文档里出现 sk-、AKIA 这类格式说明只算"提示"(否则每篇教程都会报警),而配置文件里出现同样特征就直接算"高危"。

顺手的几个小设置
#

  • 文章模板:把 front matter 固化成模板,新建文章时不用手填;
  • 图片规范:统一 WebP、宽度 ≤1600px、和文章放同一目录;
  • 发布前检查:写个脚本,推送前扫一遍"有没有真实密钥、有没有坏图、有没有超过 500KB 的大图";
  • 版本记录:把 Hugo 版本、主题 commit 记进一个文件,以后样式坏了能快速回退。

小结
#

这套方案真正的好处不是"免费",而是整条链路都是纯文本 + 可迁移的:文章是 Markdown,仓库是 Git,构建靠云端。哪怕以后想换框架或换托管,content/ 目录里的东西一个字都不用改。

写这篇的时候,博客刚上线。上线之后我又陆续发现并修掉了几个问题(见上面那节),也算印证了一件事:静态博客的坑大多不在"搭",而在"域名、协议、缓存"这些边界上。

接下来就是慢慢往里填内容了——毕竟搭博客只是手段,持续记录才是目的。

更新记录
#

日期 变更
2026-09-23 初稿发布
2026-09-23 补充「上线之后才暴露的 3 个问题」与「两道保险」——绑定自定义域名后踩到的
2026-09-23 修正「两道保险」小节:部署后自检因构建机被 Cloudflare 拦(403)已删除,实际保留的是构建期断言

本文里的 IP、密钥、订阅链接等一律用占位符表示(如 192.168.x.x、YOUR_TOKEN),请按自己的环境替换。