Pages CMS + Hexo 8 实战:像发 QQ 空间一样写博客,并用 GitHub Actions 自动发布

Chen Xi
Chen Xi

我一直想把写博客这件事变简单一点。

Hexo 本身很好用,但“写一篇随笔”通常意味着打开编辑器、找到 Markdown 文件、补全 frontmatter,再进终端执行命令。电脑在手边还好,换成手机就很麻烦。写着写着,人会开始惦记构建有没有报错、分支有没有推对,原本的一点表达欲也被消耗掉了。

我想要的其实很朴素:像以前发 QQ 空间一样,打开网页,写完,点保存。至于 Hexo、Git 和部署,让它们在后面自己跑。

金猪博客最后接入了 Pages CMS。它不是把 Hexo 改造成动态网站,也没有另外建一套内容数据库。文章仍然是 GitHub 仓库里的 Markdown,Pages CMS 只是给这些文件套了一层在线编辑器。

本文记录本站实际使用的配置,也把刚刚遇到的一次“CMS 明明保存了,怎么没看见 Actions”的排查过程写下来。

本文于 2026-07-30 使用 Hexo 8.1.2、Keep 4.3.0、Node.js 22 和 Pages CMS 托管版验证。Pages CMS 仍在更新,配置项应以文末官方文档为准。

最终的发布链路

先看全貌。本站不是让 Cloudflare 直接构建 Hexo,而是保留两个分支:

  • source 保存 Markdown、主题配置、脚本和依赖;
  • main 只保存 Hexo 生成后的静态网站。

image

一次正常保存会经过下面几步:

  1. 在 Pages CMS 编辑文章;
  2. Pages CMS 把 Markdown 提交到 source
  3. sourcepush 触发 GitHub Actions;
  4. Actions 安装依赖,执行清理、构建和检查;
  5. 检查通过后,把 public/ 发布到 main
  6. Cloudflare Pages 监听 main,把新版本部署到 dajinzhu.cn

这里有一个好处:CMS 没有直接碰正式网站。即使某篇文章的日期、链接或公式写坏了,也要先经过构建检查。检查不通过,网站继续保留上一个正常版本。

如果还没有搭好 Hexo 和 Cloudflare Pages,可以先看本站的 Hexo 8 + Keep 4 部署记录。本文从源码仓库已经可以正常构建开始。

1. 让 Pages CMS 认识仓库

最省事的方式是使用官方托管版:

  1. 打开 app.pagescms.org
  2. 使用 GitHub 登录;
  3. 安装 Pages CMS GitHub App;
  4. 授权需要编辑的仓库;
  5. 打开仓库的 source 分支。

这里一定要确认分支。本站默认分支就是 source,CMS 保存的也是 source。如果误开 main,看到的只会是生成后的 HTML,既不适合编辑,也不会触发源码构建。

Pages CMS 会读取仓库根目录的 .pages.yml。没有这个文件时,它不知道文章放在哪里,也不知道 titledatetags 应该用什么控件编辑。

2. 先配图片目录

本站把文章图片放在:

1
source/images/posts/

对应的 Pages CMS 配置是:

1
2
3
4
5
6
7
8
media:
- name: post_images
label: 文章图片
input: source/images/posts
output: /images/posts
categories: [image]
extensions: [jpg, jpeg, png, gif, webp, svg]
rename: safe

input 是文件在 GitHub 仓库中的位置,output 是写进 Markdown 的公开地址。两者不是一回事。

例如,在 CMS 中上传:

1
source/images/posts/pages-cms-hexo/publishing-flow.svg

文章里应该引用:

1
![发布流程](/images/posts/pages-cms-hexo/publishing-flow.svg)

rename: safe 会把不适合作为 URL 的文件名处理得规整一些。我仍然习惯在上传前使用简短的英文文件名。以后迁移服务器或批量处理图片时,会少很多麻烦。

3. 把 Hexo 文章声明成一个集合

Hexo 的文章集中在 source/_posts,每篇文章都有相同的 frontmatter 结构,因此适合使用 Pages CMS 的 collection

1
2
3
4
5
6
7
8
9
content:
- name: posts
label: 博客文章
type: collection
path: source/_posts
format: yaml-frontmatter
filename:
template: "{primary}.md"
field: create

format: yaml-frontmatter 很关键。它告诉 CMS:两个 --- 之间是结构化字段,下面才是 Markdown 正文。

为了让文章列表更好找,我还配置了排序和搜索:

1
2
3
4
5
6
7
8
view:
fields: [title, published, date, categories]
primary: title
sort: [date, title]
search: [title, description, tags, categories]
default:
sort: date
order: desc

文章多起来后,这一段比想象中有用。否则几十个 Markdown 文件堆在一起,在线编辑器和文件管理器没有本质区别。

4. 编辑器字段如何对应 Markdown

Pages CMS 的字段不是另一份数据。保存之后,它们仍然回到同一个 .md 文件。

image

本站目前使用这些字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
fields:
- name: title
label: 文章标题
type: string
required: true

- name: published
label: 公开发布
type: boolean
default: false

- name: date
label: 发布时间
type: date
required: true
options:
time: true
format: yyyy-MM-dd HH:mm:ss
step: 60

- name: description
label: SEO 搜索摘要
type: text
required: true
pattern:
regex: "^.{30,120}$"
message: 搜索摘要必须为 30–120 个字符。
options:
minlength: 30
maxlength: 120

- name: categories
label: 分类
type: string
required: true

- name: tags
label: 标签
type: string
list:
min: 1
max: 8
required: true

- name: body
label: 正文
type: rich-text
required: true
options:
format: markdown
switcher: true
media: post_images
path: ""

body 是一个特殊字段。对于 frontmatter 格式,它不会生成 body:,而是写到第二个 --- 后面。富文本编辑器可以直接排版,也可以切换到 Markdown 源码。遇到表格、代码块和复杂链接时,我更喜欢切到源码检查一遍。

分类在本站是一个字符串,标签则是列表。这样能避免同一篇文章挂三四个宽泛分类,同时保留更细的检索入口。

description 单独设置长度检查,是因为首页摘要不一定适合搜索结果。写它时我尽量回答两个问题:这篇文章解决什么问题,读者能得到什么。它不是关键词袋子。

5. 保存时保留未暴露的字段

Pages CMS 只认识 .pages.yml 中声明的字段。如果文章还有 reviewedcover 等暂时不准备放进编辑器的 frontmatter,保存时不应该把它们删掉。

本站开启了合并保存:

1
2
3
4
5
6
7
8
9
10
settings:
content:
merge: true
commit:
identity: user
templates:
create: "content: publish {filename}"
update: "content: update {filename}"
delete: "content: remove {filename}"
rename: "content: rename {oldFilename} to {newFilename}"

merge: true 会把编辑器提交的字段合并回原文件,未被编辑器管理的键得以保留。提交信息也做了统一,因此在 GitHub 历史里,一眼就能分出内容修改和程序修改。

6. GitHub Actions 才是真正的发布者

CMS 保存完成,只代表 source 多了一个 Commit。网站能不能更新,要看后面的工作流。

本站的触发条件是:

1
2
3
4
5
6
7
8
9
10
11
12
name: Build and deploy Hexo

on:
push:
branches:
- source
workflow_dispatch:
inputs:
payload:
description: Pages CMS action payload
required: false
type: string

正常保存使用 pushworkflow_dispatch 是手动重新部署入口,后面会讲。

构建步骤没有直接调用一条裸 hexo generate,而是把安装、构建和检查分开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
steps:
- name: Check out Hexo source
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Build and validate
run: |
npm run clean
npm run build
npm run check

本站的 check 会检查 description、canonical、JSON-LD、站点地图、图片和本地链接,也会检查 Tags、Categories 是否为空。前几天 Keep 主题曾偶发把一段 Stylus 源码当成页面局部模板,这类异常现在也会在部署前被拦住。

构建通过后,工作流把 public/ 同步到 main

1
rsync -a --delete --exclude ".git/" public/ "$RUNNER_TEMP/deploy/"

--delete 的意义是让 main 精确等于本次构建结果。文章改名或删除后,旧 HTML 不会永久残留。

7. 给 CMS 增加“重新部署”按钮

这里最容易误解。

Pages CMS 保存文章后,本来就会产生 GitHub push,不需要再点一次 Actions。Pages CMS 的 Actions 功能,是额外添加一个手动按钮,用在“内容没有变化,但我想重新构建”或者“刚修复了外部服务,想重跑一次”的场景。

本站在 .pages.yml 根级加入:

1
2
3
4
5
6
7
8
9
10
actions:
- name: deploy-site
label: 重新部署网站
workflow: deploy-hexo.yml
ref: current
cancelable: false
confirm:
title: 重新部署网站?
message: 这会运行 Hexo 构建、检查并发布 main 分支。
button: 开始部署

workflow.github/workflows/ 下的文件名;ref: current 表示使用 CMS 当前打开的分支。相应工作流必须声明 workflow_dispatch,并接受 Pages CMS 传来的 payload

这个按钮是备用入口,不应该成为每次发文的固定步骤。正常流程仍然是保存一次,等待自动构建。

8. 一次真实的“它到底执行没有”

配置完成后,我在 CMS 把一篇随笔的分类改了一次。页面没有给出我预想中的强烈反馈,于是第一反应是:“是不是没有交给 Actions?”

后来没有继续猜,而是把 GitHub 上的时间对齐:

1
2
3
19:26:32  CMS 提交到 source
19:26:35 GitHub Actions 由 push 触发
19:27:02 新的静态网站提交到 main

CMS 提交和 Actions 触发只差 3 秒。它确实执行了,只是 CMS 的“保存反馈”和 GitHub 的“工作流状态”分属两个界面。

再后来我又把分类从 Think 改成“随笔”,GitHub 出现了第二个内容提交。本地仓库当时还是旧版本,因此继续在本地写文章前,必须先:

1
git pull --ff-only origin source

这是在线 CMS 与本地编辑共存时最需要养成的习惯:开始本地工作前先同步,避免把手机上刚保存的内容覆盖掉。

9. 保存后网站没更新,按顺序查

image

第一站:source 有没有新提交

没有新 Commit,问题还在 CMS 这一侧:

  • 是否点了保存;
  • 是否选中了正确仓库;
  • 当前是不是 source
  • GitHub App 是否仍有仓库权限;
  • 字段校验是否没有通过。

此时 Actions 没有运行是正常的,因为 GitHub 根本没有收到新的 push

第二站:Actions 有没有同一个提交号

找到 CMS 提交的短 SHA,例如:

1
c2da94e

再到 Actions 中找 head_sha 相同的运行。不要只按“几分钟前”判断。连续保存两次时,两个运行可能非常接近,前一个还可能因为并发策略被取消。

如果完全没有运行,检查:

1
2
3
4
on:
push:
branches:
- source

分支名、缩进和工作流所在分支都要正确。

第三站:main 有没有新产物

Actions 变绿之后,再看 main。以本站为例,文章应该生成到:

1
2026/07/30/pages-cms-hexo-github-actions/index.html

分类页、标签页、首页和站点地图也应一起变化。不能只看到工作流成功,就默认每个页面内容都正确。

第四站:Cloudflare 是否部署了这个 main 提交

Cloudflare Pages 监听的是 main,不是 source。打开 Pages 的部署记录,比较它显示的提交号与 GitHub main 最新提交。

如果提交号落后,问题在 Cloudflare Git 集成;如果已经一致,才进入缓存排查。

第五站:最后再看缓存

前四站都正确,但正式域名仍是旧内容,可以依次尝试:

  • 浏览器强制刷新;
  • 无痕窗口;
  • 比较 pages.dev 与正式域名;
  • 查看响应头中的缓存状态;
  • 必要时清理对应 URL 的 Cloudflare 缓存。

一上来就全站清缓存,往往会掩盖真正的问题,也不能修复没有生成的 HTML。

10. 我现在怎么发一篇文章

日常发布已经很短:

  1. 用手机或电脑打开 Pages CMS;
  2. 新建文章,先关闭“公开发布”;
  3. 填标题、日期、description、分类和标签;
  4. 写正文并上传图片;
  5. 保存草稿;
  6. 预览和校对;
  7. 打开“公开发布”,再次保存;
  8. 到 Actions 看本次提交是否通过;
  9. 上线后检查文章页和分类页。

草稿也会提交到 source,但 Hexo 不会把 published: false 的文章放进正式网站。这样既保留了 Git 历史,也不用在 CMS 之外再维护一份草稿。

11. 几个容易留下后患的细节

不要直接编辑 main

main 是构建产物。下次 Actions 发布时,它会被 public/ 覆盖。所有正文修改都应该发生在 source/_posts

手机适合写,复杂排版仍要预览

普通段落、标题、列表和图片在 CMS 中很顺手;复杂表格、数学公式和嵌套代码块,我还是会切换 Markdown 源码。所见即所得不等于生成结果永远一致。

分类和标签要提前约定

技术Techtech 可能生成不同入口。本站约定分类尽量使用少量稳定名称,标签再承担细分主题。否则半年后分类页会像没有整理过的下载文件夹。

不要用修改日期假装内容更新

只有正文确实补充、修正或重新验证时才更新 updated。单纯为了让搜索结果显得新,既没有帮助,也会让读者误判文章的有效期。

CMS 不是备份,Git 才是

Pages CMS 只是编辑入口。真正可回退的历史仍在 GitHub Commit 中。改坏 frontmatter 时,可以比较前后版本,而不是靠回忆恢复。

结语

接入 Pages CMS 之后,Hexo 没有变成另一个系统。文章还是 Markdown,构建还是 Hexo,历史还是 Git,Cloudflare 也仍然只负责部署静态文件。

变化只发生在写作入口:过去必须坐到电脑前才能完成的操作,现在拿出手机也能做。保存之后到底发生了什么,又没有被藏进一个看不见的黑盒;从 source 到 Actions、main 和 Cloudflare,每一步都有提交号可以核对。

对我来说,这种状态比“完全无感的一键发布”更踏实:平时足够简单,出问题时又查得到。

参考资料