如何使用 Hexo 和 Github Actions 搭建个人博客

hexo_github_actions

缘起

5 月 13 日晚间,一位 IT 互联网行业的前辈——陈皓(网名:左耳朵耗子)去逝。2004 年,从 Live Space 自定义脚本开始,我玩起了个人博客空间,到 2008 年正式建立自己的独立博客,很快就注意到了 CoolShell.cn 的个人博客,正是左耳朵耗子的博客。

他的博客内容原创性好,完整性强,有独到见解,是整个中文博客圈,数一数二的高质量博客。从他的博客我学习了很多的技术知识,也了解了他的个人风格和思想。

这些年,我也在认真经营自己的独立博客。当我知道左耳朵耗子逝世的消息后,我第一时间就打开了他的博客,这是一个下意识的行为,因为我认识他在此,验证消息也是在此,缅怀同样在此。我发现,他的博客打不开了(后来恢复了)。可以看到,他的博客托管在 CloudFlare 平台上,出现了欠费的提示页面。

这一瞬间,我就涌起一种凄凉和恐慌的感觉,因为左耳朵耗子逝世的消息,离事情发生才一两天,他的博客竟然就因为欠费出现了停服的情况。他作为一名行业前辈,写下的文字,怎么说也对广大读者有着重要的意义,但是在这个”互联网有记忆“的时代,竟然因为欠费瞬间就被抹去。

这使我不得不反思,撰写独立博客的意义;以及,我是否希望我的博客在我去世后,还能长期存续的问题。至此,静态网站生成器,才重新纳入我的视野。

静态博客的优点

选择静态化有很多的原因,简单说说。

希望博客更长久存续,第一,就需要免费且持续托管的服务。基于这个目的,可以选择的地方有很多,比如早些年,有 QQ 空间,Live Space,然后,新浪博客,网易博客,博客园,CSDN.net,Tumblr,WordPress.com,Blogger.com,medium.com 等等,其实还有很多很多,这些都是 UGC 时代以及当代最优秀的一些平台,他们以用户撰文并发表作为自己的核心服务,即使作者不再更新,他们创建的内容,还会随着公司的存续而继续存续。

第二,有充分的自主性,可以高度自定义。上述的那么多服务和平台,符合这个要求的,就并不多了。大多数内容平台,更多专注于内容的创建和流量的运营,在允许客户自定义博客的功能和外观方面,都有很强的限制。其实,自定义程度最高的平台,就是自己运维一个独立博客,使用 WordPress 这样的博客应用,通过编写代码来高度定制自己的想要的功能。而缺点就是需要动态服务器,而动态服务器则很少有免费且稳定的服务提供商。

第三,维护方便,可以在线撰写。这毫无疑问要求博客的后台,必须是一个动态的网站才可以实现在线撰写。而前面也说了,动态的服务器,少有免费且稳定的。

所以,我很容易想到了世界最大的程序员社区 GitHub + 静态博客生成器。因为这是一种我早就听说过,也明白其原理的解决方案。GitHub 也是一个比较理想且成熟的社区。利用代码管理系统,实现内容的管理,利用持续集成的系统,完成内容的构建,利用免费的 Pages 服务,提供博客的展示。而微软又比较有钱,维护好程序员社群也符合他们的长期利益,所以,有望长久免费运营下去。

Hexo 博客

前两天,试用了 Hugo 静态网站生成器来搭建博客,安装部署的体验不错。不过,当全部系统搭建完毕后,想找一个功能完善,又符合我偏爱的老派审美的主题时,我犯难了。因为 Hugo 的主题实在是太难找了,虽然主题看起来很多,但是,就像它官网宣传的那样,它主打的是构建通用的静态网站,博客只是其中一个功能,所以专门为博客定制的主题数量就不多。

此外,官方皮肤主题目录,竟然不能按照热门程度、更新频度等关键评估指标进行排序,就算是想知道每个皮肤的全部特性,也显得万分困难。我得去知乎帖子,V2EX 帖子等等网友推荐列表里寻找主题,而找到的主题多数良莠不齐,比起 WordPress 世界强大完善的主题和插件生态,真是令人失望。

另外,也不知道是否是错觉,我总觉得,Hugo 配套的最好的主题,也都显得稀松平常,甚至跟一些网友怀疑的一样,我也觉得是不是搞后台 Go 语言开发的这帮人,做网页终究还是不大行。于是,我萌生了尝试一下 Hexo 的想法,Hexo 同样也是一个静态网站生成器,但官网主打的就是”博客框架“,主要区别在于,其使用 nodeJS 开发,生态里也多数都是前端程序员。预览了一些博客,发现确实更加美观,功能也齐全,这点要平均优于 Hugo 的各种主题。

安装搭建

找了一个教程,按着操作,发现本地安装和启动过程,还是非常流畅的。

1
2
3
4
5
6
7
8
9
10
11
12
# 安装 node
brew install node
# 更新 npm
npm install -g npm@9.8.1
# 安装 hexo 命令行
npm install hexo-cli -g
# 初始化一个博客站点,blog 可以换成你自己的文件夹名字
hexo init blog
# 启动博客
cd blog
npm install # 安装依赖
hexo server

按照上面命令顺序执行,基本就可以顺利在本地启动一个 Hexo 的博客,通过 http://localhost:4000 默认地址访问,就可以看到一个只有一篇 Hello World 文章的博客,默认的主题叫 landscape 看起来还挺好看,整洁大方,美观舒适,瞬间感觉这东西完爆 Hugo 啊……

构建、部署自动化

能够在本地启动,还不足够,咱们的需求是,能够完全脱离本地环境运行,在本地环境编辑、编译,只是一个可选项,如果没有本地环境,就算直接在 Github 版本库,也要能直接发布博客文章,这是一个核心的需求点,所以第二步,咱们就来实现这个。

这个任务本质上是 CI/CD 这个范畴的一个任务,无非就是,第一步,准备运行环境,第二步,构建所有静态页面,第三步,部署到指定位置。在使用 Hugo 平台的时候,将 hugo 生成的站点文件推送到 GitHub 后,GitHub Actions 频道会自动推荐一个 workflow 给我,是其他大神或者组织业已写好的配置文件。而当我把 Hexo 生成的站点目录推送到 GitHub 后,发现没有对应的 Actions 推荐。后来,我研究了一下,可能 Hugo 的那个 workflow 是官方研发团队编写的,经过认证,所以会进入 Github 的自动推荐。

Hexo 其实也有很多大神编写好的 workflow,但是因为都是个人做的,所以没被系统推荐。大家可以去 Github 上的 Marketplace,然后在导航菜单中,Types 选定 Actions,直接搜索 hexo 关键字,就能找出很多人发布的,将 hexo 站点构建并部署的现成的工作流。简直太方便了,给我打开了一扇新的大门。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Deploy Hexo site to Pages

on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false

# Default to bash
defaults:
run:
shell: bash

jobs:
# Build job
build:
runs-on: ubuntu-latest
#env:
# HUGO_VERSION: 0.108.0
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
submodules: recursive

- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-cache
restore-keys:
${{ runner.os }}-npm-cache

- name: Install Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 'latest'
- run: npm install
- run: npm run build
# https://github.com/marketplace/actions/configure-github-pages
- name: Setup Pages
id: pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./public

# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2

我自己也来 share 一个,这个是我根据 Hugo 的那个官方发布的工作流改的,改成了适合于 Hexo 的。我找了一个现成的 Hexo 的工作流,很短小精悍,确实可用,不过我觉得它写得不好,没有注释,未来不好扩展和维护。正好也需要学习一下 GitHub Actions,我就把大神写的 workflow 参考 Hugo 发布的自动化脚本,逐行逐行改过去,没想到我只写了一遍,就完全正确运行了。可见 GitHub Actions 功能强大,使用简便,用户体验非常好。

内容迁移

说实话,Hexo 的入门门槛并不高,你从了解到这个东西,到把它部署起来,速度是很快的。就我上面的这些步骤,到能在 GitHub Pages 上自动发布,半天内足够了,甚至可以基本搞清原理。不过,一般来说,你不是首次假设博客的话,都面临一个原来博客搬家的问题。比如我从 2008 年起,使用 WordPress 博客,我想搬家的话,需要把所有的文章搬迁到 Hexo 上。这个过程就并不那么友好了。

1. 文章

根据文档,Hexo 有一个 WordPress 博客迁移的工具插件,使用:

1
npm install hexo-migrator-wordpress

即可安装成功,然后,在 WordPress 博客选择导出功能,将全部内容导出成一个 XML 文件,然后,根据文档,使用命令:

1
hexo migrate wordpress <source> --save

就可以将博客所有文章导入到 hexo。

当然,故事不会那么圆满。我就碰到相当多的坑,在网上,你也轻易可以搜到各种关于坑的讲述,以及解决办法。比如,插件 Taxopress 这种处理 Tags 的插件,会在日常写文章的时候,生成一些日志,会储存在 posts 表里,这些日志的内容和文章几乎一样,但是 post_type 是 taxopress 而不是 post,可是无论是 WordPress 自己的导出工具,还是 hexo 的迁移工具,都不能正确过滤掉这个类型。导致同一篇文章被导入了多次,夸张的有连续导入 5 遍的情况。

后来,我不得不将数据库备份导入一个临时安装的 WordPress,然后从数据库里硬删除 post_type 为 taxopress,link,menu 等等的 post,此外还要删除 custom_style 类型的记录。然后再执行导出,得到的 xml 里才混入了比较少的杂质,方便导入。

就算如此,也免不了大量的手动清洗脏数据的工作,就不赘述了,简而言之就是没有好办法,博客迁移绝对是一个伤筋动骨的过程。谨慎为之!

2. 图片

第二个问题,是 WordPress 中的管理的图片,是不会被自动导入的,首先你必须手动从服务器上现在图片文件夹到本地。然后,你必须修复每篇文章中,对图片的引用链接。不然,所有的文章里引用的图片都会用绝对网址链接到你原来的博客。而不会用相对路径链接到本地服务器。为了做这件事情,我专门编写了一个工具。放在本博客的根目录下。首先把图片全部下载,安排好放置的目录,然后用脚本将所有文章的引用,都改为从本地引用。

WordPress 原来管理图片的系统,叫 Media Library,所有的媒体文件都会使用年月作为 key,分文件夹归档在一个目录树上,比较简单的方式是,继续保持这个目录树的结构基本不变,只是在文章中修复将图片引用的路径,统一更换一下前缀,完成链接的修复。

另一个问题是,WordPress 的文章里有时候插入了一些表格,在我的这次转换中,此类文章,表格在转换完,都破碎了。页面几乎都无法查看了。暂时,我还没找到特别好的方法。

3. 分类

第三个问题,是 WordPress 和 Hexo 对博客分类的使用和管理不同,比如在 Hexo 里,你不能像 WordPress 那样去设定分类,因为它默认分类只有一层,如果你想使用嵌套的分类,必须使用特殊的语法去设定,这样一来,你使用工具批量转换文章的时候,自动工具的分类设置很多都是错误的。

因为在 Hexo 里,父分类、子分类的表达方式是不一样的,而迁移工具基本不能正确处理分类的问题。我 400 多篇文章,迁移后,大概 150 篇文章的分类是错的。

主题、皮肤、模板

人靠衣装,佛靠金装。无论你是个性美观至上,还是功能至上,选定一个博客的主题,都是建立一个博客的过程中,最最重要的一项工作。

除了撰写,编译,发布这些由框架或者平台提供的功能,剩下的功能比如排版、布局、导航、归档、评论等等功能,都是主题提供的功能。

皮肤(Color Scheme)说的是,同一款主题有不同的配色方案,是主题(Theme)这个范畴下更精细的一个方面。而模板(Template)说的是,不同的文章类型,有不同的预设的排版样式。当然,你把这三个词混用,也没什么不可以,在各自的场景下,大体上都能明白。

选择主题的时候,一定要选择知名的,存在时间长的,使用广泛的,这样的主题至少可以保证功能上的质量,然而,这样势必就不会太有个性。因为大家都用这个,只能是牺牲个性来保证功能了。

如果需要个性,就得自己去找或者亲自写一些样式表,来定制皮肤,使得原有的主题呈现出全新的外观。

就主题来看,Hexo,Hugo 这种类型的平台,拍马屁也追不上 WordPress,WordPress 作为世界上最好的博客平台,有着完善的生态系统,庞大的用户群体,在博客主题方面,除了官方的目录,还有很多平台在贩卖收费版主题,可以说,选择之多,如过江之鲫,而质量之高也足堪使用,毕竟你还能选择付费皮肤让作者提供特殊支持。但是 Hexo 和 Hugo 都要差很多了。

我选定的皮肤是一款比较著名的皮肤,叫 NeXT,经调查,发布后维护的年头相当长了,各方面都非常完善,竟然也衍生出了自己的插件,十分丰富,我亲测觉得质量非常过硬。关于这款皮肤的中文资料非常多,很多都是万字长文,大家可以自己搜索看看。我这里介绍一些基本的功能。

1. 字数统计

1
npm install hexo-word-counter

安装字数统计,在每篇文章的头部展示“本文字数”,“阅读时长”信息,很迎合当今互联网上大家浮躁的情绪,对文章的内容做出一个预告。

2. 本地搜索

1
npm install hexo-generator-searchdb

没有了数据库,所有文章都是静态的,通过本地搜索功能,能将所有文章的索引,用一个大文件传输到前台,帮助读者快速检索整个博客,聊胜于无吧。

3. 评论

1
npm install hexo-next-giscus@1.0.3

动态博客上最基础的一个功能,发表评论,到了静态博客上,实现竟然十分困难。因为读者动态地创建评论内容并展示在你的博客上,本来就是纯动态功能。实现这样的功能,都需要另一个动态的系统配合。有很多三方评论系统可供选择,体验也都不错。我选择了这个插件 hexo-next-giscus,是利用 GitHub 推出的一款官方默认应用,Discussion,作为评论的后台,通过 giscus 这个三方 App 提供接口 API,然后再结合博客中的 js 客户端,共同完成了博客的评论功能。

上面的代码指定安装了 1.0.3 版本,因为我发现最新版本是无法正常工作的。请大家根据实际情况决定。

4. 优化

1
npm install hexo-optimize

这是一个优化博客引用的静态文件 CSS,JS 文件的插件,可以将静态文件进行压缩,优化站点的载入时间。

5. 数学公式

1
npm install hexo-filter-mathjax

这个插件是一个服务器端公式渲染的工具。是 Next 这款主题上推荐的插件

插件的官网上,有如何配置和使用的文档,在 matters 里增加一个 key 说明本页使用数学公式即可。如何配置的信息也有。

日常管理

在 WordPress 中,博客的文章保存在数据库的表里,磁盘上都是系统的源码文件。但是,Hexo 使用 Markdown 格式作为博客原文的格式,这种纯文本格式就直接以文件的形式存储在本地磁盘上,优势当然很多,方便书写,方便版本控制和管理。

缺点也同样明显,就是文件数量多,杂乱不堪,不便索引和管理。尤其是从 WordPress 导入后,你会发现,所有的文章,都在同一个文件夹 _posts 下面,以文章的 permalink 作为文件名,就那么平铺在单一目录下。

比如,我的博客,有 400 多篇文章,从 WordPress 导入后,都放在 source/_posts 文件夹下平铺着,我用英文给每篇文章设定了 post_slug,所以每篇文章都是 4-8 个不等的英文单词用中划线连接作为文件名,我那种崩溃,不知道你们能不能想象到。

日常,如果一个文件夹有 400 多个文件,大多数人就会觉得无从管理了。如果所有的文件名又是英文单词拼接成的 slug,就更加头晕了。因为时间日期信息又写在文件内部,排序也好,筛选也好,都很难做到。

对比一下 WordPress,采用数据库进行管理,可以进行简单的搜索,而已如果你懂一点点技术也可以轻易用 SQL 进行批量操作。你会感觉到一个从天到地的落差感。至少不要傻乎乎将所有文章都放到一个单个文件夹比较好吧。

利用 Hexo 生成静态网页的规律和原理,我设计了如下的解决方案:

  1. 设定一个规则,文件命名采用文章标题的中文,然后将原来的 post_slug 设定为每篇文章的网址,用 permalink 这个 front matter 字段来设定,写在文章头部。进行此项设定后,生成的静态站点,会用 front matter 中设定的 permalink 作为网址,不会用中文标题做网址。
  2. 文章都放入发表年份 4 位数字的文件夹里归档。
  3. 为了方便索引,文章的文件名,用两位数字的发表月份 + 26 进制后缀(小写字母)来做前缀。

这样按照我每年输出十几到几十篇文章的频度,完全够用了。每月写文章超过 26 篇,我这个编号体系就会爆掉。不过幸好我没那么高产。

这样,我一个文件夹里,最多就二三十篇,当然 2008 年我最积极,写了 100 多篇,也没什么,反正不会再去修改历史上的文档了。为了完成这个重命名工作,我又写了一个 Python 脚本,也提交到了本站的 repo 里了。

如果没有一点编码能力的非程序员,你会发现这种系统真的非常不友好,几乎等于完全不可用的一个系统了。

写作

全部收拾停当后,我可以安心写作了,然后我发现我遇到了难题。

因为在这个博客系统里,所有的内容,不是数据化的,也不是结构化线性存储的。而你在写作的时候,你的文章也是脱离系统孤立存在的,比如在 WordPress 中写作博客,分类信息,标签信息,你都可以进行下拉选择,或者输入一个字符,就会有大量的提示和建议出来。但是在 Hexo 博客里,不会有任何提示,甚至你写作了一点点,多加了个空格,都会导致分类信息和标签信息无法正确匹配,被识别为全新的分类或者标签。

这样一来,就要求你每次写作的时候,对自己的全部分类体系都有相当完整和精确的记忆,这就给人脑带来了巨大的心智负担。这点是让我最为痛苦的。不知道大家都是怎么解决的?我想到的当然是最后有个客户端,能在内存中把所有的东西都加载进去,然后写作的时候,可以很好的提供提示功能。而不是像现在,找一篇历史写过的文章,进去复制黏贴。

而经过搜索,比较推荐的客户端软件都已经停更至少 3 年了。这一点我非常遗憾。我想,如果有一个好的客户端,我会更加喜欢这个博客系统的。

工具

以前是登录 Web 后台撰写 blog,现在则是维护博客的源文件,经过编译后再发布成网站。在尝试了 Obsidian 和 VS Code 后,我现在更多使用的是 VS Code,主要在这里,我更熟悉一些,也有不错的预览功能。

我预感 Obsidian 也会比较好用,但是我之前没有解决图片预览的问题。

上面我提到了一个处理,就是文章都用中文作为文件名,然后用 permalink 字段来指定文章的永久链接。举个例子:

1
permalink: how-to-use-hexo-and-github-actions-to-build-a-personal-blog/

是本文的永久链接,文章的源文件是放在:

1
source/_posts/2023/06c-如何使用-Hexo-和Github-Actions搭建个人博客.md

路径下的。而本文的插图,是放在:

1
source/images/2023/06/hexo_github_actions.png

这个路径下的。这样一来,我引用图片的时候,要写成:

1
![hexo_github_actions](../../images/2023/06/hexo_github_actions.png)

但是我发现,这样做,在编译好之后,图片竟然就死链了。因为在编译后的网站里,图片的路径是:

1
/images/2023/06/hexo_github_actions.png

而文章的路径是:

1
/how-to-use-hexo-and-github-actions-to-build-a-personal-blog/

(注意,真实文章是上面那个路径后面的 /index.html 被隐去了,所以上面的路径实际上是一个文件夹)图片的相对路径里 ../ 只有一层。所以,要想在写作的时候和发布的时候,同时都能正确引用图片,就要保证在写作时候和发布后,文章与图片的相对路径不变。这是花了很多时间才体会到的问题和解决方案。

之前就是因为我只能保证发布后的展示效果,就导致我不能保证写作时的预览,害得我用各种工具都很不舒服。现在我想通了这个问题后,我预计 VS Code 也好,Obsidian 也好,应该会都比较好用了。

总结

搭建好 Hexo 的博客后,我发现,现在我写一篇文章的顾虑变多了。像处理写作时候预览,和发布后的展现的关系,以及处理路径这么基础的问题,在 WordPress 博客根本不会有。这种心智上的负担也不是一次性的。我写文章频率不高,但是每次一旦想写,就不得不找回所有的这些细枝末节,所以,整体来说,这个系统更适合运维操作,而不适合作者。

就不难想象为什么这么美观的一个网站框架流行不起来了。天下有几个人能像我这样搞定这一切呢?能搞定的都高度集中在程序员群体中。

就算对程序员来说,搞定技术问题不难,但是每次写作都要重复搞定相同的技术问题,也成为写作的巨大阻碍。现在我主要是为了享受静态网站可以长久不维护能正常运转这个优势。看来还要有个工具,能降低写作时候的心智负担,才更完美。

更新

对于写作时候造成的心智负担,我想到的解决方案就是,编写一个客户端软件,将分类信息、标签信息等读取出来,集成到文章的编辑界面上,实现分类和标签的提示。于是我构建了 HexoPress。我已经开源到了 GitHub。