我的博客搭建经历
在各种一站式写作平台、社交媒体和知识库产品层出不穷的 2026 年,为什么还要费尽心机去自己折腾一个独立博客?
对于我来说,答案很简单:拥有一个完全属于自己的数字花园。 在这里,我拥有对内容的 100% 控制权,没有算法推荐的裹挟,没有格式排版的限制,更重要的是,博客本身的设计、架构、自动化脚本的演进过程,就是一个极佳的程序员技术沙盒。
今天,我想把这个博客从搭建、优化、重构至今的完整历程和技术选型细节记录下来。
一、 核心技术栈:为什么选择 Hugo Extended
在建站之初,我对比了 Hexo、Gatsby、Astro 以及 Hugo。最终我选择了 Hugo Extended。
- 速度的绝对降维打击:Hugo 是用 Go 编写的,构建速度往往以毫秒计。对于一个需要频繁预览和调整的个人博客来说,保存即渲染的即时反馈体验极为畅快。
- SCSS 的原生处理能力:由于我选用了功能非常丰富的 DoIt 主题,它需要动态处理大量的样式表。Hugo 的 Extended 版本 内置了对 SCSS 和编译管道的支持,我可以非常轻松地在
assets/css/_override.scss中覆盖主题变量(例如我将全站的非代码字体覆盖为了LXGW Bright霞鹜文楷,而无需修改主题子模块的代码)。
二、 博客整体运行架构示意图
为了让大家有一个直观的宏观认识,我将整个博客的“本地写作、本地脚本安检、代码托管、云端 CI/CD 构建、CDN 分发及图床/评论闭环”的工作流,绘制成如下拓扑图。本博客主题原生支持 Mermaid,无需任何多余配置即可直接渲染:
graph TD
subgraph Local ["1. 本地写作与开发 - WSL / Windows"]
A["Zed 编辑器"] -->|"1. 新建并编写博文"| B("Markdown 源文件")
B -->|"2. 本地热预览"| C["Hugo 本地 Server"]
C -->|"3. 浏览器实时调试 localhost:1313"| D(("本地预览"))
end
subgraph Deploy ["2. 自动化部署 - 双端脚本"]
B -->|"4. 一键部署指令 deploy.sh / deploy.ps1"| E{"五重安全雷达检测"}
E -->|"未拉取主题"| F["自愈子模块"]
E -->|"落后远程"| G["拦截并提示 git pull"]
E -->|"草稿检测"| H["显示 draft: true 列表并警告"]
E -->|"检测通过"| I["暂存并生成本地 Backup 提交"]
I -->|"5. 安全推送 Git Push"| J["GitHub 源码私有仓库"]
end
subgraph Cloud ["3. 云端构建与 CDN 分发"]
J -->|"6. Webhook 自动触发"| K["Cloudflare Pages 构建管道"]
K -->|"7. 拉取子模块并运行 Hugo Build"| L["全量静态 HTML/CSS/JS"]
L -->|"8. 部署至全球边缘网络"| M["Cloudflare CDN 边缘节点"]
end
subgraph User ["4. 读者端访问与交互"]
N(("读者浏览器")) -->|"9. 访问你的域名"| M
N -->|"10. 匿名请求无 Referrer 图片"| O["Cloudflare Workers 图床"]
N -->|"11. 读写文章评论"| P["Utterances 评论 / GitHub Issues"]
end
三、 博客基建的三个“痛点”与折腾方案
独立博客好看不好用,往往是因为“图片难存、双语难翻、评论难搭”。为了解决这三个痛点,我逐步搭建了属于自己的解决方案。
1. 干净的双语文章管理:从后缀制到目录制
我的博客是一套中英文双语系统(中文默认,英文为辅)。
最初,我使用的是 Hugo 默认的“文件名后缀模式”:
content/posts/my-post.md # 中文文章
content/posts/my-post.en.md # 英文文章
当文章数量少时还好,一旦变多,两类语言的文件就会在同一个文件夹下乱成一团,且编辑器的侧边栏看起来杂乱无章。
为了彻底重构为 “基于目录的多语言管理模式” (Content Directory Multilingual),我把中英文文章物理隔离到独立的语言目录下:
- 将中文文章全部隔离到
content/zh/posts/ - 将英文文章全部隔离到
content/en/posts/
并在 hugo.toml 中做了如下的完整映射配置(已脱敏):
# hugo.toml
defaultContentLanguage = "zh-cn"
[languages]
[languages.zh-cn]
contentDir = "content/zh" # 中文内容源目录
weight = 1
title = "我的个人博客"
locale = "zh-CN"
label = "简体中文"
hasCJKLanguage = true # 开启中日韩字数精确统计
[languages.en]
contentDir = "content/en" # 英文内容源目录
weight = 2
title = "My Blog"
locale = "en"
label = "English"
hasCJKLanguage = false
现在,中英文文章的文件名完全相同(比如都叫 my-post.md),Hugo 会自动通过同名相对路径在渲染时把它们关联为互为翻译。我的工作区瞬间变得极度清爽!
2. 图片图床:自建 Cloudflare Workers 防 403
为了随处写作且保证图片能在全球范围内高速、稳定地访问,我没有选择把图片存在博客 Git 仓库里(会拖慢 clone 速度),而是使用了自建的 blog-images.your-domain.workers.dev(基于 Cloudflare Workers 代理的对象存储图床)。
防盗链与本地预览 403 问题的终极解决方案:
由于很多第三方图床会识别 Referer。当你在本地 http://localhost:1313 预览时,浏览器发送的本地 Referer 会导致图床直接返回 403 Forbidden。
我们可以通过重写 Hugo 的 Markdown 图片渲染器,强制图片在加载时不发送 Referer。在项目根目录下新建 layouts/_markup/render-image.html:
<!-- layouts/_markup/render-image.html -->
{{- $lightgallery := .Page.Params.lightgallery | default site.Params.Page.lightgallery | default false -}}
{{- $destination := replaceRE "^/static" "" .Destination -}}
{{- if .Title -}}
<figure>
{{- if $lightgallery -}}
<a class="lightgallery" href="{{ $destination }}" title="{{ .Text }}" data-thumbnail="{{ $destination }}" data-sub-html="<h2>{{ .Title }}</h2>">
{{- end -}}
<!-- 关键点:注入 referrerpolicy="no-referrer" -->
<img loading="lazy" referrerpolicy="no-referrer" src="{{ $destination }}" alt="{{ .Text }}">
{{- if $lightgallery -}}
</a>
{{- end -}}
<figcaption class="image-caption">{{ .Title | safeHTML }}</figcaption>
</figure>
{{- else -}}
<!-- 关键点:注入 referrerpolicy="no-referrer" -->
<img class="tw:inline" loading="lazy" referrerpolicy="no-referrer" src="{{ $destination }}" alt="{{ .Text }}">
{{- end -}}
这一行小小的 HTML 属性 referrerpolicy="no-referrer",完美根治了本地预览图床图片报 403 的顽疾!
3. 自定义样式变量覆盖
如果你用的是 Git 子模块安装的主题,直接改主题里的 CSS 会导致升级困难。最好的做法是在项目级重写样式。在 assets/css/_override.scss 中覆盖全局字体(以引入霞鹜文楷字体为例):
/* assets/css/_override.scss */
@font-face {
font-family: 'LXGW Bright';
src: url('/fonts/LXGWBright.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root {
/* 覆盖 DoIt 主题内置的全局字体变量 */
--global-font-family: 'LXGW Bright', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
4. 评论系统:优雅且免维护的 Utterances
我不希望引入多余的第三方臃肿评论脚本,更不希望为此去维护一个后端的数据库。因此,我选用了基于 GitHub Issues 的 Utterances。
其在 hugo.toml 中的配置非常轻量:
# hugo.toml
[params.page.comment]
enable = true
[params.page.comment.utterances]
enable = true
repo = "your-github-username/your-blog-comments-repo" # 绑定的公共 GitHub 仓库
issueTerm = "title" # 用博文标题作为 issue 的匹配项
label = "comments" # 自动打上 comments 标签
lightTheme = "github-light"
darkTheme = "github-dark"
不仅加载速度极快,而且非常契合技术博客的格调。
四、 DevOps 自动化部署:双端脚本护航
如果每一次写完文章,都需要手动去清理缓存、全量构建、暂存提交、然后安全推送,写博客的积极性很快就会被这些重复机械的劳动磨灭。
为此,我编写了两套功能对齐的部署脚本(Linux/WSL 下的 ./deploy.sh 与 Windows 下的 deploy.ps1)。以下是脚本中最核心、最有技术含量的三大防护模块的脱敏代码实现:
1. 智能 Git 冲突防御(拦截未拉取代码直接 Push)
脚本会在执行部署前拉取远程状态,比对本地与远程的提交差异。如果发现本地落后,立即强行终止,防御代码被强制覆盖的悲剧:
# deploy.sh 片段
local remote_ref=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo "")
if [ -n "$remote_ref" ]; then
echo "拉取远程仓库状态 (git fetch)..."
git fetch --quiet 2>/dev/null
# 统计本地落后于远程的 commit 数量
local behind_commits=$(git rev-list --count HEAD.."$remote_ref" 2>/dev/null || echo "0")
if [ "$behind_commits" -gt 0 ]; then
echo "❌ 部署被拦截:本地分支落后远程分支 $behind_commits 个提交!"
echo "请先执行 git pull,解决可能存在的冲突后再尝试部署。"
exit 1
fi
fi
2. 自动化草稿(Draft)检测雷达
利用 Hugo 内置指令列出所有带有 draft: true 的文件并优雅高亮,避免你把半成品文章不小心发到线上:
# deploy.sh 片段
check_drafts() {
if command -v hugo &> /dev/null; then
# hugo list drafts 会输出草稿列表,用 tail 剥离掉首行表头
local drafts=$(hugo list drafts 2>/dev/null | tail -n +2 || true)
if [ -n "$drafts" ]; then
echo "═══════════════════════════════════════"
echo " 📝 提醒:以下文章当前仍为草稿 (draft: true)"
echo " 它们不会被部署到线上版本:"
echo "$drafts" | while read -r line; do
local filename=$(echo "$line" | cut -d',' -f1)
local title=$(echo "$line" | cut -d',' -f2)
echo " - $filename ($title)"
done
echo "═══════════════════════════════════════"
fi
fi
}
3. TTY 交互式终端感知
当在常规终端里双击或运行时,它会等待用户敲击键盘确认;若被 VS Code Task、后台定时器或 CI/CD pipeline 自动触发,它能自动感知并直接跳过阻碍性的 TTY 确认,防止任务永久挂起死锁:
# deploy.sh 片段
if [ "$confirm" = true ]; then
# [ -t 0 ] 用来判断标准输入是否来源于真实的交互式终端
if [ -t 0 ]; then
read -p "是否继续部署? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "❌ 已取消部署"
exit 0
fi
else
echo "ℹ 检测到非交互式环境,自动跳过人工确认步骤"
fi
fi
四、 托管平台与云端渲染
博客的源文件被推送到 GitHub 后,会由 Cloudflare Pages 自动接管。
- 它的构建容器会自动检测博客的更新,全自动下载 Hugo 依赖并拉取子模块。
- 重新编译后,把静态文件全球分发。静态文件不需要我推送到第二个仓库,我的 Git 源码仓库就是唯一的真理来源(Source of Truth)。
总结
一个看似简单的博客,其背后蕴含着多语言架构设计、CDN 图床缓存、Git 子模块管理、双系统 shell/PowerShell 脚本编写、无服务器计算 (Serverless Workers) 以及 CI/CD 管道构建等一系列工程实践。
生命不息,折腾不止。这个博客就是我最好的代码试验田,希望未来的日子里,它能记录下我更多、更有价值的思考与探索。