《我的 Hugo + Cloudflare 自动化博客搭建手册》
一套属于自己的博客系统,不仅仅是一个网站,更是一份可以陪伴很多年的数字资产。
第一章 为什么重建博客
1.1 写在前面
从学生时代开始,我就一直有写博客的习惯。
最早接触的是 Hexo + GitHub Pages,那时候更多的是为了记录一些折腾电脑、网络和编程的过程。后来由于域名到期、工作繁忙以及博客长期没有维护,整个博客逐渐停止更新,最终也无法继续访问。
多年以后,我重新开始思考一个问题:
互联网上那么多内容,真正属于自己的还有多少?
朋友圈会被时间淹没,微信公众号受平台限制,知乎、微博等平台都有自己的规则。只有自己拥有的博客,数据、域名、内容、排版、访问方式,全部掌握在自己手里。
因此,我决定重新搭建一个属于自己的博客。
这一次,我希望它不仅仅是一个写文章的网站,而是一套能够长期稳定运行、几乎无需维护、可以陪伴自己很多年的知识管理系统。
1.2 我的目标
重建博客时,我给自己定下了几个目标。
第一,坚持简单。
博客应该把精力放在内容,而不是维护网站本身。
因此,我希望整个发布流程能够尽可能自动化,让写作成为唯一需要关注的事情。
第二,数据完全属于自己。
所有文章都以 Markdown 保存。
所有图片保存在自己的仓库。
所有源码托管在 GitHub。
网站部署在 Cloudflare Pages。
域名使用自己的 leesy.cc。
任何时候,即使更换电脑、更换服务器,也可以快速恢复整个博客。
第三,速度快、访问稳定。
选择 Cloudflare Pages 的原因并不仅仅是免费。
更重要的是:
- 全球 CDN 加速
- 自动 HTTPS
- 自动部署
- 与 GitHub 无缝集成
- 不需要维护服务器
整个博客真正实现了静态化部署,没有数据库,没有后台,没有运维压力。
第四,让博客成为长期积累的平台。
博客未来主要记录以下几个方面:
- 电网技术学习笔记
- NAS 与家庭服务器折腾记录
- Docker、Cloudflare、Linux 等技术实践
- AI 工具使用心得
- 阅读笔记
- 健康与运动记录
- 日常思考与生活随笔
希望很多年以后,再回来看这些文章,依然能够知道当时为什么这样思考、为什么这样做。
1.3 为什么选择 Hugo
目前主流静态博客框架有很多,例如:
- Hexo
- Hugo
- Jekyll
- Astro
- Next.js
最终我选择了 Hugo,主要有几个原因。
首先,Hugo 的构建速度非常快,即使几百篇文章,也能在几秒钟内完成生成。
其次,Hugo 使用 Markdown 作为内容格式,没有数据库依赖,迁移非常方便。
第三,社区活跃,主题丰富,生态成熟,适合长期维护。
最后,也是最重要的一点:
Hugo 非常符合我"内容优先"的理念。
写作就是写作,而不是折腾网站。
1.4 为什么选择 Cloudflare Pages
以前我一直使用 GitHub Pages。
GitHub Pages 非常优秀,但随着博客逐渐完善,我希望网站能够拥有更好的访问速度以及更加简单的部署流程。
Cloudflare Pages 提供了:
- 自动 HTTPS
- 全球 CDN
- GitHub 自动部署
- 免费额度充足
- 自定义域名
- 与 Cloudflare DNS 深度集成
我的域名本身就在 Cloudflare,因此迁移之后,整个网站的部署和域名管理都集中到了同一个平台,维护成本大幅降低。
最终形成了下面这一套工作流:
Typora → Hugo → Git → GitHub → Cloudflare Pages → leesy.cc
整个发布过程只需要一次 git push 即可完成。
1.5 希望这本手册能够一直更新
这份文档不是一篇一次性教程。
它更像是整个博客项目的运维手册。
未来每增加一个功能、解决一个问题、踩过一个坑,我都会把过程记录下来。
希望几年以后,当我再次打开这份文档时,依然能够快速恢复整个博客,也能够看到自己一路走来的成长轨迹。
第二章 环境准备
博客能够长期稳定运行,很大程度上取决于环境是否统一。
因此,我希望整个博客系统尽量遵循以下原则:
- 开发环境简单。
- 所有配置尽量使用默认方式。
- 所有数据都可以通过 Git 恢复。
- 更换电脑后,能够在较短时间内恢复整个开发环境。
2.1 开发环境
当前博客开发环境如下。
| 项目 | 软件 |
|---|---|
| 操作系统 | macOS |
| 包管理器 | Homebrew |
| 静态博客 | Hugo Extended |
| 编辑器 | Typora |
| 代码编辑器 | VS Code(辅助) |
| 版本管理 | Git |
| 代码托管 | GitHub |
| 网站部署 | Cloudflare Pages |
| 域名解析 | Cloudflare DNS |
所有软件均尽量通过官方渠道安装,不依赖第三方修改版本。
2.2 博客目录结构
博客根目录如下:
blog/
├── archetypes/
├── assets/
├── content/
├── layouts/
├── scripts/
├── static/
├── themes/
├── hugo.toml
└── README.md
其中几个目录最重要。
content/
保存所有 Markdown 文章。
以后所有文章都放在:
content/posts/
不直接修改 public 目录中的内容。
static/
保存所有静态资源。
例如:
static/images/
所有图片最终都会放在这里。
浏览器访问时,对应:
/images/
scripts/
保存所有自动化脚本。
例如:
- organize_images.py
以后新的自动化工具,也统一放在这里。
themes/
博客主题。
当前使用:
PaperMod。
如果以后升级主题,也只需要替换这一部分。
2.3 为什么选择 Markdown
所有文章全部采用 Markdown 编写。
原因主要有以下几点:
- 可读性高。
- 与平台无关。
- Git 可以直接管理。
- 未来迁移成本极低。
- Typora 编辑体验优秀。
相比富文本编辑器,Markdown 更适合长期保存。
十年以后,即使 Hugo 不存在了,Markdown 文件依然能够正常打开。
2.4 为什么选择 Git
Git 不仅用于同步代码。
对于博客来说,它更像是一套完整的版本管理系统。
每一次修改:
- 都有历史记录。
- 可以查看差异。
- 可以回滚。
- 可以恢复误删内容。
因此,本博客所有配置、脚本、文章都纳入 Git 管理。
2.5 为什么选择 Cloudflare Pages
博客源码保存在 GitHub。
真正的网站部署交给 Cloudflare Pages。
这样做有几个优点:
- GitHub 专注于源码管理。
- Cloudflare 专注于部署和 CDN。
- 每次执行
git push后自动完成构建。 - 无需手动上传文件。
- 自动生成 HTTPS。
- 全球 CDN 自动缓存。
整个网站没有数据库,没有后台,没有服务器维护成本。
真正实现:
Git 即发布。
2.6 当前工作流
整个博客采用如下工作流程:
Typora
│
▼
Markdown
│
▼
Python 自动整理图片
│
▼
Git Commit
│
▼
GitHub
│
▼
Cloudflare Pages
│
▼
https://leesy.cc
整个过程中,除了写文章之外,其余步骤均可自动完成。
博客维护者需要关注的,只有内容本身。
第三章 Hugo 建站实践
这一章记录整个博客从零开始搭建的全过程。
网上关于 Hugo 的教程很多,但真正从零搭建并长期维护之后,会发现真正耗费时间的并不是安装 Hugo,而是后面的各种配置、迁移以及踩坑。
本章不仅记录最终方案,也记录整个选择过程。
3.1 为什么放弃 Hexo
最早接触博客时,我使用的是 Hexo。
Hexo 陪伴了我很多年,也留下了不少文章。
但是随着时间推移,它逐渐暴露出几个问题:
- Node.js 环境依赖较重;
- 每次升级都可能遇到插件兼容问题;
- 主题维护程度参差不齐;
- 长时间不维护后,恢复环境比较困难。
当重新搭建博客时,我希望:
不需要记住大量命令,也不需要担心几年以后环境无法恢复。
因此最终放弃了 Hexo。
3.2 为什么选择 Hugo
经过比较后,我最终选择 Hugo。
原因主要有以下几点:
第一,构建速度非常快。
即使未来博客拥有几百篇文章,Hugo 仍然可以在几秒钟内完成构建。
第二,没有 Node.js 依赖。
整个博客只依赖 Hugo 一个程序。
升级更加简单。
第三,Markdown 原生支持。
所有文章都是 Markdown。
未来如果需要迁移到其它平台,也几乎没有成本。
第四,生态成熟。
PaperMod、LoveIt 等优秀主题都非常成熟。
搜索、归档、RSS、标签等功能基本开箱即用。
3.3 建站流程
整个建站过程如下:
安装 Hugo
│
▼
创建博客目录
│
▼
安装 PaperMod
│
▼
配置 hugo.toml
│
▼
本地运行 hugo server
│
▼
上传 GitHub
│
▼
Cloudflare Pages 自动部署
真正复杂的,并不是这个流程。
真正复杂的是下面这些问题。
3.4 我踩过的坑
整个博客搭建过程中,真正花费时间的地方主要集中在下面几个问题。
坑一:GitHub Pages 配置反复失败
最开始使用 GitHub Pages 部署。
虽然最终能够成功,但是期间遇到了:
- GitHub Actions Workflow 冲突;
- Pages 与 Deploy Action 混用;
- 权限(403)问题;
- Node.js 版本弃用;
- Submodule 清理失败。
虽然最后都解决了,但整个过程让我意识到:
GitHub 更适合作为源码仓库,而不是最终的网站托管平台。
这也为后面迁移 Cloudflare Pages 埋下了伏笔。
坑二:主题安装方式
刚开始准备使用 Git Submodule。
后来发现:
虽然升级方便,但也增加了部署复杂度。
最终决定:
直接把 PaperMod 放进仓库。
优点:
- 不依赖 Submodule;
- Cloudflare Pages 无需额外配置;
- 换电脑直接 Git Clone 即可。
后来的实践证明,这是一个非常正确的决定。
坑三:文章命名
最开始直接使用中文文件名。
例如:
我的 Hugo + Cloudflare 自动化博客搭建手册.md
后来发现:
- Shell 需要处理空格;
- URL 会自动编码;
- 分享链接不够美观。
最终统一规范:
文件名全部使用英文 slug。
例如:
hugo-cloudflare-handbook.md
真正显示给读者看的标题,仍然使用中文。
这样兼顾了维护性与阅读体验。
坑四:图片管理
这是整个博客搭建过程中耗时最长的问题。
Typora 默认插入图片以后:
路径会出现:
- macOS 本地路径;
- 图片全部堆积在一个目录;
- 更换电脑后容易失效。
经过多次尝试,最终设计出目前的方案:
Typora ↓
统一复制到
static/images/未整理/
↓
Python 自动整理图片
↓
按文章分类移动
↓
自动修改 Markdown 图片链接
整个过程无需人工处理。
这是目前博客自动化程度最高的一部分。
后面将单独用一章详细介绍。
坑五:GitHub Pages 迁移 Cloudflare Pages
博客最终没有继续使用 GitHub Pages。
原因主要有:
- 域名本身就在 Cloudflare;
- Cloudflare CDN 更快;
- 自动 HTTPS;
- 自动部署更加简单;
- 后续可以直接接入 Analytics。
最终博客部署架构调整为:
Git
↓
GitHub
↓
Cloudflare Pages
↓
https://leesy.cc
实践证明,这套方案明显比 GitHub Pages 更加省心。
3.5 最终目录结构
博客目前采用如下目录:
blog/
├── content/
├── static/
│ └── images/
├── scripts/
├── themes/
├── assets/
├── hugo.toml
└── README.md
目录尽量保持简单。
所有自动化脚本统一放入 scripts/。
所有图片统一放入 static/images/。
所有文章统一放入 content/posts/。
长期维护时,这样的结构最容易理解。
3.6 我的经验
回顾整个建站过程,我最大的体会其实不是某一条命令,而是:
一开始就应该把博客当成一个长期维护的项目,而不是一次性的网页。
真正值得投入时间的,不是折腾主题,不是追求炫酷动画,而是建立一套稳定、可靠、自动化的写作与发布流程。
博客最终的价值,不在于它使用了什么框架,而在于它能否陪伴自己很多年,持续记录、持续积累、持续成长。
第四章 自动化发布流程的演进
前三章更多是在讲选择:为什么重建博客,为什么选择 Hugo,为什么最终把网站部署到 Cloudflare Pages。
但真正让我觉得这套博客系统开始“站起来”的,其实是自动化发布流程逐渐成型的过程。
最开始,我想解决的问题很简单:
在 Typora 里写文章时,本地能看到图片,发布到网站后也能看到图片。
这个问题看起来不大,但真正做起来以后才发现,它牵扯到图片路径、Hugo 静态资源目录、Git 提交范围、Typora 本地预览、文章备份、构建校验、自动推送等一整套流程。
也正是因为这个问题,博客从“能发布”慢慢变成了“可以放心长期使用”。
4.1 第一版:先把一键发布跑起来
最早的思路非常直接:
写一个 deploy.py,把所有事情串起来。
大致流程是:
整理图片
↓
修改 Markdown 图片链接
↓
运行 Hugo 构建
↓
Git 提交
↓
Git 推送
这个方向是对的。
因为对于个人博客来说,最重要的不是脚本写得多复杂,而是能不能把“写完文章以后还要做的一堆杂事”交给程序。
但第一版很快暴露出问题:
- 它倾向于扫描所有文章;
- 图片处理、Markdown 改写、Git 操作混在一起;
- 一旦某个环节出错,很难判断问题在哪里;
- 对历史文章不够友好;
- 对“不要误伤原文”这件事保护还不够强。
所以第一版解决的是“能不能自动发布”的问题。
但它还没有解决“能不能长期放心使用”的问题。
4.2 第二版:把脚本拆成各管一件事
后来我逐渐意识到,自动化脚本不能写成一个大杂烩。
如果所有逻辑都塞进 deploy.py,短期看起来方便,长期一定会越来越难维护。
于是发布流程被拆成几个模块:
deploy.py
scripts/
├── organize_images.py
├── markdown.py
├── git_tools.py
├── hugo_tools.py
├── config.py
└── utils.py
每个文件只负责一件事。
deploy.py 只做调度。
organize_images.py 只处理图片。
markdown.py 只修改文章头信息和标签。
git_tools.py 只判断哪些文章变了,以及提交哪些文件。
hugo_tools.py 只负责构建。
这样的好处是,流程一下子清楚了:
changed_markdown_files()
↓
backup_markdown()
↓
organize_images()
↓
fix_markdown()
↓
build_hugo()
↓
commit_and_push()
这一步是整个自动化流程的关键转折。
从这一刻开始,博客发布不再是一个“能跑就行”的脚本,而是一套可以继续迭代的工具。
4.3 第三版:只处理改过的文章
刚开始做图片整理时,很自然地会想到:
把 content/posts/ 下面所有 Markdown 全部扫描一遍。
这样最简单,也最不容易漏。
但很快就发现,这不是一个好习惯。
历史文章越多,全部扫描的风险越高。尤其是旧文章里可能会有教程示例、历史图片路径、外链图片、代码块里的图片写法。如果每次都全量改写,就很容易产生不必要的变更。
所以后来改成:
只处理相对 Git
HEAD新增、修改或删除的 Markdown。
也就是说,Git 成了整个自动化流程的边界。
我今天只改了一篇文章,脚本就只处理这一篇。
我删除了一篇文章,脚本就只提交这篇文章的删除记录。
没有变动的历史文章,不碰。
这个原则非常重要:
自动化不是越积极越好,而是越克制越可靠。
4.4 第四版:先备份,再改写
在修改文章内容之前,还有一个更重要的问题:
文章不能丢。
这是整个流程的底线。
所以自动化脚本后来增加了备份机制。
只要检测到本轮有 Markdown 需要处理,真正改写之前,先把原文备份到:
.backups/<时间>/markdown/
这样即使图片整理或 Markdown 改写出现问题,也能找回改写前的版本。
这个设计给了我很大的安全感。
因为博客不是测试数据。
每篇文章都有当时的想法、情绪和记录。脚本可以重写,文章不能随便丢。
所以后来的原则就变成了:
- 改写前先备份;
- 图片只复制,不随便移动原图;
- 本地图片找不到时取消提交;
- Hugo 构建失败时取消发布;
- 只有检查通过后才提交和推送。
这套流程并不复杂,但它让自动化从“方便”变成了“可靠”。
4.5 第五版:解决 Typora 本地预览
这套流程里最容易忽视的,其实是 Typora。
Hugo 网站上图片能显示,不代表 Typora 本地能显示。
网站里使用的是:
/images/文章名/1.jpg
这是浏览器访问 Hugo 静态资源时的路径。
但 Typora 在本地打开 Markdown 时,并不知道 /images/ 对应的是博客根目录下的 static/images/。
所以后来脚本会在文章头信息里自动加入:
typora-root-url: /Users/leesdove/Documents/blog/static
这样同一个图片链接:

在网站上能显示,在 Typora 本地也能显示。
这一步解决了最开始那个核心痛点:
本地和网站看到的是同一套图片。
为了兼容历史文章,脚本还处理了一个细节:有些文章原来使用 Hugo 的 TOML 头信息,也就是 +++ 包起来的 front matter。Typora 对这类头信息支持并不好,所以流程里逐步把它们转换为更适合 Typora 的 YAML 格式。
这不是为了追求格式统一,而是为了让写作体验稳定。
4.6 第六版:图片按文章归档并编号
图片管理后来也经历了一次重要调整。
最开始只是把图片放进对应文章目录,文件名沿用原图名称。
这样虽然能用,但时间久了以后会有几个问题:
- 文件名可能很长;
- Typora、截图工具、微信保存的图片命名都不统一;
- 同一篇文章里图片顺序不直观;
- 以后人工检查不方便。
于是最终采用了按文章归档、按出现顺序编号的方式:
static/images/文章名/1.jpg
static/images/文章名/2.png
static/images/文章名/3.webp
文章中的引用统一变成:

这样做有几个好处。
第一,图片和文章天然对应。
第二,文件名短,便于排查。
第三,文章里第几张图,对应目录里第几个文件。
第四,迁移电脑或换部署平台时,图片路径仍然稳定。
后来外链图片也被纳入这套逻辑。能下载的外链图片,会被归档到本地 static/images/文章名/ 里,避免以后图床失效。
这一步让我真正觉得:
图片不再是文章的附属麻烦,而是博客数据的一部分。
4.7 第七版:保存后自动发布
有了 deploy.py 以后,发布已经很方便了。
但每次写完文章都要手动执行命令,还是有一点割裂。
于是后来增加了一个监听器:
python3 -m scripts.watch
它做的事情很简单:
- 启动后检查是否已经有未发布的 Markdown 改动;
- 监听
content/下 Markdown 文件的修改; - 发现保存后等待 15 秒;
- 确认编辑基本结束,再自动运行
deploy.py。
为什么要等 15 秒?
因为 Typora 写作时可能连续保存。如果每保存一次就立刻发布,反而容易打断写作,也会产生很多没必要的提交。
等待一小段安静时间,本质上是在尊重写作节奏。
最终体验变成:
打开监听器
↓
Typora 写文章
↓
保存
↓
自动整理图片
↓
自动构建
↓
自动提交推送
↓
Cloudflare Pages 自动部署
到这里,发布流程才真正接近我想要的状态:
写作即发布。
4.8 中间放弃过的方案:定时巡检
昨天还尝试过一个想法:
让 macOS 每天定时跑一次图片巡检。
这个想法听起来很美好。
每天自动检查一遍博客图片,如果发现问题就提醒。
但真正落地时遇到了 macOS 的权限限制。定时任务运行在后台,不一定拥有访问 ~/Documents/blog 的权限,尤其是 Documents 目录受系统隐私保护。结果就是:脚本本身没有问题,但后台任务可能没有权限读到博客目录。
后来我决定暂停这个方向。
原因有两个。
第一,它增加了系统层面的复杂度。
第二,它不符合我的实际写作习惯。
我的博客不是每天后台自动生产内容,而是我写文章时才需要检查和发布。因此,把检查放在手动启动监听器和发布流程里,比做一个常驻定时任务更自然。
这次取舍也提醒我:
自动化不是把所有事情都交给后台,而是把合适的事情放到合适的时机。
4.9 最终形成的发布流程
经过这些版本迭代后,现在的发布流程已经比较清晰。
手动启动监听器:
cd ~/Documents/blog
python3 -m scripts.watch
然后正常写文章。
保存文章以后,脚本会自动完成:
检测变更文章
↓
备份原文
↓
复制并归档图片
↓
标准化图片链接
↓
补充 Typora 本地图片根目录
↓
补充缺失标签
↓
运行 Hugo 构建
↓
提交本轮相关文件
↓
推送到 GitHub
↓
Cloudflare Pages 自动部署
这个流程有几个重要原则:
- 只处理本轮改动;
- 不碰无关历史文章;
- 改写前先备份;
- 图片统一收归本地;
- 构建失败就不发布;
- Git 记录每一次变化;
- Cloudflare Pages 只负责最终部署。
这正是我希望博客系统达到的状态:
平时足够简单,出问题时又能追溯。
4.10 这次迭代给我的启发
昨天这轮修改给我最大的启发是:
真正可靠的自动化,不是一开始就设计出来的。
它是在一个个具体问题里长出来的。
一开始只是图片本地看不到。
然后发现要统一图片目录。
接着发现不能全量改历史文章。
再后来发现必须先备份。
然后又补上 Hugo 构建校验、Git 精准提交、Typora 本地根目录、保存后自动发布、外链图片归档等细节。
每一步都不是为了炫技,而是为了解决真实写作中的小麻烦。
这些小麻烦如果靠人记,很快就会忘。
但把它们沉淀成脚本以后,博客系统就会越来越稳。
所以这一章真正想记录的,不只是几个 Python 文件,而是一种思路:
先让流程跑起来,再让它安全,再让它克制,最后让它自然融入写作。
这也是我希望这个博客长期保持的样子。
技术在背后安静工作,写作者只需要继续写。
第五章 自动化脚本功能说明
第四章讲的是自动化流程怎么一步步形成。
这一章则换一个角度:
不要求看懂每一行代码,但要知道每个脚本负责什么。
这样以后如果发布失败、图片异常、Git 没有提交,至少知道应该从哪里开始查。
5.1 deploy.py:总入口
deploy.py 是整个自动化发布流程的总入口。
它不负责具体细节,而是负责把各个模块按顺序串起来。
可以把它理解成一个发布指挥官。
它主要做几件事:
检测本轮改动的文章
↓
备份 Markdown 原文
↓
整理图片
↓
修正 Markdown
↓
运行 Hugo 构建
↓
提交并推送 Git
它有几个常用参数:
python3 deploy.py
正常发布。
python3 deploy.py --dry-run
只预演,不真正改文件、不提交。
python3 deploy.py --no-push
只提交,不推送。
python3 deploy.py --all-images
巡检全部文章图片。
平时真正需要记住的其实只有一个:
python3 deploy.py
但更多时候,我不会直接运行它,而是交给监听器自动调用。
5.2 scripts/watch.py:写作监听器
scripts/watch.py 是日常写作时最常用的脚本。
它启动后会一直看着 content/ 目录里的 Markdown 文件。
只要我在 Typora 里保存文章,它就会等待 15 秒,然后自动运行 deploy.py。
使用方式是:
cd ~/Documents/blog
python3 -m scripts.watch
为什么要有这个脚本?
因为它把发布动作从“写完后想起来执行命令”,变成了“保存后自动发布”。
它还处理一个细节:
如果启动监听器之前,已经有写完但没有发布的文章,它会先自动同步一次。
因此,日常使用时我只需要:
打开监听器
↓
打开 Typora
↓
写文章
↓
保存
剩下的事情交给脚本。
5.3 scripts/organize_images.py:图片整理核心
organize_images.py 是整套系统里最重要的脚本之一。
它负责解决图片问题。
它会扫描文章里的 Markdown 图片语法:

然后判断这张图片属于哪一种情况:
- 本地图片;
- Typora 临时图片;
- 已经归档的
/images/...图片; - 外链图片;
- 代码块里的示例图片。
真正需要整理的图片,会被复制到:
static/images/<文章名>/
并按文章中出现的顺序编号:
1.jpg
2.png
3.webp
文章里的图片链接会统一改成:

这个脚本有几个非常重要的原则:
- 只处理本轮传入的文章;
- 不处理代码块里的图片示例;
- 本地图片找不到时,不强行发布;
- 外链图片能下载就归档,不能下载就保留原链接;
- 原图尽量只复制,不随便移动;
- Typora 产生的根目录临时图片,会在确认安全后清理。
所以它不是简单的“替换路径”,而是在保护文章内容的前提下整理图片。
5.4 scripts/markdown.py:文章头信息与标签
markdown.py 主要处理 Markdown 本身。
它现在主要做两件事。
第一,补充 Typora 本地图片根目录:
typora-root-url: /Users/leesdove/Documents/blog/static
有了这一行,Typora 才知道:
/images/...
应该从本地的 static/images/ 里找。
第二,自动补充缺失标签。
如果一篇文章没有 tags,脚本会根据文章内容,从固定标签库里选择几个合适的标签。
但有一个重要原则:
如果我已经手动写了 tags,脚本不会覆盖。
这很重要。
自动化可以帮忙补漏,但不能替作者做最终判断。
5.5 scripts/git_tools.py:只提交该提交的内容
git_tools.py 负责和 Git 打交道。
它最重要的功能是判断:
本轮到底哪些 Markdown 变了?
它会通过 Git 对比当前工作区和 HEAD,找出新增、修改、删除的文章。
这样 deploy.py 才能只处理本轮真正变化的内容。
它还负责最终提交:
git add 本轮相关文件
git commit
git push
这里有一个细节非常关键:
它不是简单粗暴地 git add .。
而是只把本轮文章、相关图片、自动修改过的 Markdown 加入提交。
这样可以减少误提交,也让每一次提交更清楚。
5.6 scripts/hugo_tools.py:构建检查
hugo_tools.py 只做一件事:
运行 Hugo 构建。
目前实际执行的是:
hugo --minify
为什么提交前要先构建?
因为 Hugo 构建相当于发布前体检。
如果文章格式有问题,图片引用有问题,模板有问题,构建阶段就能提前发现。
只有构建通过,后面才会进入 Git 提交和推送。
这能避免把明显坏掉的版本发布出去。
5.7 scripts/config.py 和 utils.py:公共配置与工具
config.py 保存全局配置。
例如:
- 博客根目录;
- 文章目录;
- 图片目录;
- 支持的图片后缀;
- Typora 本地图片根目录;
- 自动标签规则。
utils.py 则保存一些公共工具。
例如:
- 打印日志;
- 运行命令;
- 生成安全目录名;
- 备份 Markdown;
- 统一抛出博客流程错误。
这两个文件平时不用碰。
但它们让其它脚本不用到处重复写同样的路径和工具函数。
5.8 scripts/audit_images.py:图片巡检
audit_images.py 用来检查文章里的图片是否都已经被正确归档。
它更像是一个体检工具,而不是发布工具。
它可以告诉我:
- 哪些文章还有未归档图片;
- 哪些图片链接可能失效;
- 哪些图片没有按规则放在文章目录下。
后来我尝试过把它做成 macOS 定时任务,但最终暂停了。
原因不是巡检本身没用,而是后台定时任务会遇到 macOS 文稿目录权限问题。
所以现在更适合的方式是:
需要检查时手动运行。
日常发布仍然交给 deploy.py 和 watch.py。
第六章 Git 在博客中的作用
很多人把 Git 理解成程序员用来管理代码的工具。
但对这个博客来说,Git 管理的不只是代码。
它管理的是整个博客的历史。
6.1 Git 管理哪些内容
这个博客里,Git 主要管理:
- Markdown 文章;
- Hugo 配置;
- 自动化脚本;
- 主题文件;
- 图片资源;
- 部署相关配置。
只要这些东西进了 Git,就意味着:
它们有历史,有记录,也能恢复。
这比单纯把文件放在电脑里安全很多。
6.2 为什么 public 不进 Git
public/ 是 Hugo 构建出来的网站成品。
它不是源文件。
源文件是:
content/
static/
themes/
hugo.toml
scripts/
只要源文件还在,public/ 可以随时重新生成。
所以 .gitignore 里忽略了:
public/
resources/
.backups/
__pycache__/
*.pyc
这样做的好处是:
- 仓库更干净;
- 不提交重复生成物;
- Cloudflare Pages 可以自己构建;
- 本地临时文件不会污染历史。
这是静态博客非常重要的原则:
Git 管源文件,不管生成物。
6.3 HEAD 是自动化流程的参照点
脚本判断文章有没有变化,依靠的是 Git 的 HEAD。
可以简单理解为:
HEAD 就是上一次正式提交的版本。
当前文件和 HEAD 对比,如果不同,就说明有新改动。
自动化脚本正是根据这个差异来判断:
- 哪些文章是新增的;
- 哪些文章是修改的;
- 哪些文章是删除的。
这样做比单纯看文件修改时间更可靠。
因为文件修改时间可能被系统影响,而 Git 差异是真正的内容差异。
6.4 一次发布对应一次提交
现在的理想状态是:
每一次自动发布,都对应一次 Git 提交。
这有几个好处。
第一,可以知道每次发布改了什么。
第二,如果某次发布有问题,可以定位到具体提交。
第三,可以恢复误删或误改的文章。
第四,GitHub 和 Cloudflare Pages 都能围绕提交记录工作。
所以博客发布不是把文件随便传上去。
它是一个清晰的链条:
本地文件修改
↓
Git commit
↓
Git push
↓
GitHub 保存历史
↓
Cloudflare Pages 构建网站
这条链路让博客有了可追溯性。
6.5 Git 也是回滚工具
如果某一天文章改坏了,最重要的不是慌,而是先看 Git。
常用思路是:
git status
看当前有没有未提交变更。
git log --oneline
看最近有哪些提交。
git diff
看当前文件和上次提交相比改了什么。
如果只是想找回某一篇文章的历史,可以查看对应文件的提交记录。
这就是 Git 对博客最大的价值:
它让写作有后悔药。
当然,真正回滚前要谨慎,因为回滚会改变文件内容。
但至少,历史在那里。
6.6 GitHub 在这里扮演什么角色
迁移到 Cloudflare Pages 以后,GitHub Pages 已经不再负责托管网站。
但 GitHub 仍然非常重要。
它现在负责:
- 保存博客源码;
- 保存提交历史;
- 作为 Cloudflare Pages 的构建来源;
- 在换电脑时恢复整个博客。
所以不能关掉 GitHub 仓库。
可以关闭 GitHub Pages。
但 GitHub 仓库仍然是整个博客系统的中心仓库。
第七章 从 GitHub Pages 迁移到 Cloudflare Pages
这次博客迁移里,一个很关键的变化是:
GitHub 不再负责托管网站,Cloudflare Pages 负责最终部署。
这不是把 GitHub 抛弃,而是重新分工。
7.1 迁移前的结构
以前的结构大致是:
本地博客
↓
GitHub 仓库
↓
GitHub Pages
↓
blog.leesy.cc
这种方式能用。
但在实际折腾中遇到过一些问题:
- GitHub Actions 权限配置;
- Pages 部署来源选择;
- Workflow 反复调整;
- 域名和 HTTPS 配置;
- 构建失败后排查不够直观。
这些问题不一定是 GitHub Pages 不好,而是对我这个博客来说,它不是最省心的方案。
7.2 迁移后的结构
迁移后变成:
本地博客
↓
GitHub 仓库
↓
Cloudflare Pages
↓
leesy.cc / www.leesy.cc
这个结构更清晰。
GitHub 只管源码。
Cloudflare 只管部署、CDN、HTTPS 和域名。
域名本身就在 Cloudflare 管理,因此部署和解析放在同一个平台里,维护起来更自然。
7.3 需要改哪些地方
迁移到 Cloudflare Pages 后,仓库里主要改了几类东西。
第一,更新 Hugo 的主站地址:
baseURL = "https://leesy.cc/"
这会影响 RSS、站点地图、canonical 地址以及页面里的绝对链接。
第二,删除 GitHub Pages 的 workflow。
原来的 .github/workflows/hugo.yml 是用来部署 GitHub Pages 的。
现在已经不需要。
如果继续保留,它可能会让 GitHub Pages 重新构建,造成混乱。
第三,在 GitHub 仓库设置里关闭 GitHub Pages。
这一步不会删除仓库,也不会影响 Cloudflare Pages。
它只是停止 GitHub Pages 继续提供旧网站。
7.4 Cloudflare Pages 怎么工作
Cloudflare Pages 连接 GitHub 仓库后,会监听指定分支。
这个博客使用的是:
main
每次本地执行:
git push
GitHub 收到新提交后,Cloudflare Pages 会自动开始构建。
构建逻辑可以理解为:
拉取 GitHub 仓库
↓
安装/使用 Hugo
↓
运行 hugo 构建
↓
生成 public/
↓
发布到 Cloudflare 全球网络
所以本地不需要手动上传 public/。
也不应该把 public/ 提交到 Git。
7.5 域名如何理解
现在主域名是:
https://leesy.cc
同时可以配置:
https://www.leesy.cc
旧域名:
https://blog.leesy.cc
如果还想保留,可以在 Cloudflare 里做跳转。
但从博客配置角度看,baseURL 应该使用最终希望被搜索引擎和读者记住的主域名。
因此现在写成:
https://leesy.cc/
这是博客的 canonical 地址。
7.6 迁移完成后的判断标准
迁移是否完成,可以看几个标志。
第一,GitHub Pages 已关闭。
第二,仓库里不再有 GitHub Pages 部署 workflow。
第三,hugo.toml 里的 baseURL 已经改为新主域名。
第四,Cloudflare Pages 能从 main 分支自动部署。
第五,访问:
https://leesy.cc
能看到最新文章。
第六,文章页面里的 canonical 地址指向 leesy.cc。
这些都满足以后,就说明迁移基本完成。
第八章 常见问题与排查
博客系统越自动化,越需要知道出问题时从哪里下手。
这一章记录一些常见情况。
8.1 Typora 看不到图片
先检查文章头里有没有:
typora-root-url: /Users/leesdove/Documents/blog/static
再检查图片链接是不是:

如果图片链接是 /images/...,但 Typora 没有配置 typora-root-url,本地就可能看不到。
解决办法通常是重新运行:
python3 deploy.py
或让监听器自动处理。
8.2 网站看不到图片
先确认图片文件是否真的存在:
static/images/<文章名>/1.jpg
再确认文章里引用的是:
/images/<文章名>/1.jpg
如果本地存在但网站没有,可能是还没有提交或 Cloudflare Pages 还没有部署完成。
这时看:
git status
git log --oneline
确认是否已经提交推送。
8.3 Hugo 构建失败
如果 hugo --minify 失败,自动发布会停止。
这是好事。
因为它阻止了坏版本发布。
排查时先看终端里的 Hugo 错误信息。
常见原因包括:
- front matter 格式错误;
- Markdown 语法异常;
- 图片路径写错;
- 主题模板问题;
- 文章里包含 Hugo 不允许的原始 HTML。
如果只是 Raw HTML 警告,不一定会阻止构建。
但如果 Hugo 报 error,就要先修好再发布。
8.4 Git push 失败
git push 失败通常有几类原因:
- 网络问题;
- GitHub 登录状态失效;
- 远端仓库地址错误;
- 本地和远端历史冲突;
- GitHub 权限问题。
排查时先看:
git remote -v
git status --branch --short
如果只是网络问题,等网络恢复后重新运行发布即可。
如果是认证问题,需要重新登录 GitHub。
8.5 Cloudflare Pages 没有更新
如果 GitHub 已经推送成功,但网站没更新,问题多半在 Cloudflare Pages。
可以检查:
- Cloudflare Pages 是否连接的是正确仓库;
- 构建分支是不是
main; - 最近一次部署是否成功;
- 构建命令是否正确;
- 输出目录是否是
public。
本地只要确认:
hugo --minify
git log -1 --oneline
就能判断内容有没有成功进入仓库。
8.6 文章误删怎么办
如果文章刚删,还没有提交,先看:
git status
如果 Git 显示这个文件被删除,说明历史里还在。
这时不要急着继续发布。
可以通过 Git 找回。
如果已经提交,也仍然可以从历史提交里恢复。
这就是为什么每次发布都要经过 Git。
8.7 自动化脚本出问题怎么办
先不要连续运行很多次。
第一步看:
git status
确认现在有没有未提交变更。
第二步看终端错误。
第三步根据错误类型判断:
- 图片问题,看
organize_images.py; - Markdown 头信息或标签问题,看
markdown.py; - Git 提交问题,看
git_tools.py; - Hugo 构建问题,看
hugo_tools.py; - 保存后自动触发问题,看
watch.py。
自动化脚本不是黑箱。
它们只是把原来手动做的事情拆成了几个清楚的步骤。
第九章 换电脑与恢复博客
一个博客系统是否可靠,不只看它平时能不能发布。
还要看换电脑时能不能恢复。
9.1 恢复博客需要哪些东西
理论上,只要 GitHub 仓库还在,就可以恢复大部分内容。
需要准备:
- Git;
- Hugo Extended;
- Python 3;
- Typora;
- GitHub 访问权限;
- Cloudflare 账号权限。
博客源码从 GitHub 克隆回来以后,文章、图片、脚本、主题都应该在仓库里。
这就是为什么图片必须进入 static/images/,而不能只留在本机某个临时目录。
9.2 新电脑恢复流程
新电脑大致按这个顺序:
安装 Homebrew
↓
安装 Hugo Extended
↓
安装 Git
↓
克隆博客仓库
↓
进入 ~/Documents/blog
↓
运行 hugo --minify
↓
打开 Typora 写作
↓
启动监听器
如果 hugo --minify 能通过,说明本地构建环境基本正常。
如果 Typora 能看到图片,说明本地图片根目录也正常。
9.3 为什么脚本不要依赖太多第三方库
现在这些 Python 脚本尽量只使用标准库。
这样做的目的很简单:
降低恢复成本。
如果脚本依赖很多第三方库,新电脑恢复时就还要安装依赖、处理版本兼容。
而标准库随 Python 自带,几年以后也更容易运行。
这个博客的原则一直是:
简单,稳定,少依赖。
9.4 备份的边界
GitHub 是主要备份。
本地 .backups/ 是临时保险。
Cloudflare Pages 不是源码备份。
它只是部署平台。
所以真正重要的是:
- GitHub 仓库;
- 本地 Git 历史;
- Markdown 原文;
- static/images 图片;
- 域名控制权。
只要这些还在,博客就能恢复。
第十章 长期维护原则
写到这里,这套博客系统已经不仅是一个网站。
它更像一个长期项目。
长期项目最重要的不是一次性折腾成功,而是几年以后还容易维护。
10.1 能不改结构就不改结构
现在的目录结构已经比较清楚:
content/posts/
static/images/
scripts/
themes/
hugo.toml
以后新增功能时,尽量不要随便打乱这个结构。
结构越稳定,迁移和排查越容易。
10.2 自动化要保持克制
自动化不是越多越好。
昨天尝试定时巡检后,我更加确认这一点。
真正适合自动化的是:
- 重复发生;
- 规则明确;
- 出错可回滚;
- 不影响写作节奏。
不适合自动化的是:
- 需要人工判断的内容;
- 系统权限复杂的后台任务;
- 失败后难以察觉的操作;
- 可能误删文章或图片的动作。
所以这套博客自动化应该继续保持一个原则:
帮我做杂事,但不要替我做判断。
10.3 发布前一定要能构建
无论以后脚本怎么变,有一个原则不能变:
Hugo 构建失败,就不发布。
构建是最低限度的质量检查。
哪怕只是个人博客,也不应该把明显坏掉的版本推到线上。
10.4 文章和图片都要当作资产
文章是资产。
图片也是资产。
以前容易只重视 Markdown,忽略图片。
但真正迁移博客时会发现,图片一旦散落在电脑各处,恢复成本非常高。
所以现在的规则是:
文章进入 content/posts/
图片进入 static/images/<文章名>/
只要文章和图片都在仓库里,博客才算真正属于自己。
10.5 文档要随着系统一起更新
这篇手册本身也应该持续更新。
以后每次调整脚本、迁移平台、修改部署方式、踩到新坑,都应该补进来。
因为人会忘。
但文档可以帮未来的自己快速恢复现场。
这也是写这篇手册最大的意义:
不是为了证明我搭过一个博客,而是为了让这个博客以后还搭得起来、修得明白、用得安心。
10.6 最后的目标
这套系统最终要达到的状态很简单:
打开 Typora,写文章。
保存。
等待自动发布。
然后继续生活。
技术不应该挡在写作前面。
它应该在背后安静地托住写作。
如果几年以后,我还能用同样简单的方式继续写文章,那这次折腾就值得。