回顾这些年折腾 Hexo 博客的经历,从初建到复活再到各种功能配置,踩了不少坑也积累了一些经验。这篇文章把整个过程串联起来,既是记录也是分享。
建站之初
从 2021 年 10 月中旬开始,陆陆续续搭建这个自己的网站。由于不会用 GitHub,也不了解 Hexo、Node.js 这一类东西,所以花的时间比大部分人长。
建站路上遇到最坑的点就是下载的 NexT 主题有问题——需要修改主题目录下的 _config.yml 文件,否则会多一个 %20,删去 || 之间的空格即可。后来才发现原来我用的这个 NexT 已经停止维护了,于是换成了最新的 NexT。
反反复复换了很多次主题,但都不尽如人意,所以最后还是用了 NexT。
第一次复活
2023 年 2 月,寒假结束前,我复活了博客。
这个寒假过得有些混乱,但也有些收获。从家里急事回老家开始,在车上看完了《四月是你的谎言》——本来就弹钢琴的我被触动,买了一台雅马哈 PSR-e373 电子琴。发现电子琴可以作为 MIDI 输入器后,还想写一个 Minecraft mod 做实时 MIDI 转换,可惜老电脑带不动。
后来兴趣又回到了编程上,学习了 Python 的 OpenCV 和 PyQt 写了学校科技创新比赛的作品,又学了一点 Lua 语法准备做饥荒 mod。
一个寒假过去,学习没怎么搞,博客倒是又捡起来了。
第二次复活:全面功能配置
2024 年 2 月,再次清空电脑后,一切又重新开始。惊喜地发现上次复活博客也正好是 2 月 2 日——整整一年前的同一天。
这次趁着重新搭建,把博客的各种功能系统地配置了一遍。
LaTeX 支持
博客中经常需要数学公式(尤其是机器学习相关的笔记),LaTeX 支持是刚需。
方案一:hexo-renderer-kramed(适用于 matery 主题)
Hexo 默认的 hexo-renderer-marked 渲染引擎会将 LaTeX 中的下划线 _ 解析为 HTML 的 <i> 标签。hexo-renderer-kramed 修复了这个问题:
1 | npm uninstall hexo-renderer-marked --save |
还需要修改 node_modules/kramed/lib/rules/inline.js 解决语义冲突:
1 | // 第 11 行 escape 变量 |
方案二:hexo-renderer-markdown-it-plus(推荐)
1 | npm uninstall hexo-renderer-marked --save |
然后在主题 _config.yml 中启用 KaTeX:
1 | math: |
踩坑记录:渲染器冲突
配置完成后发现 LaTeX 还是不生效,排查发现是因为同时安装了三个 Markdown 渲染器,它们互相冲突:
| 插件 | 支持 $...$ |
最终状态 |
|---|---|---|
hexo-renderer-marked |
❌ | ❌ 卸载 |
hexo-renderer-kramed |
✅,但修改 | ❌ 卸载 |
hexo-renderer-markdown-it-plus |
✅ 内置 KaTeX | ✅ 保留 |
Hexo 7 中,多个渲染器注册同一扩展名(.md)时,最后注册的胜出。按加载顺序,hexo-renderer-marked 总是覆盖前面的,而它根本不处理 $...$ 数学公式。
修复方法:
1 | npm uninstall hexo-renderer-marked hexo-renderer-kramed --save |
同时在站点 _config.yml 中配置 markdown_it_plus 插件并显式启用 KaTeX:
1 | markdown_it_plus: |
另外,NexT 主题默认 math.per_page: true,只有 front-matter 中声明了 katex: true 的文章才会加载 KaTeX 的 CSS/JS。所以每篇用到数学公式的文章都需要加上:
1 |
|
做完这三步,$E=mc^2$ 和 $$\frac{1}{x^2-1}$$ 就能正常渲染了。
需要注意的是,KaTeX 不支持 \begin{equation} 环境,如果文章中用到了需要改写成 \begin{aligned} 等 KaTeX 支持的写法,或者换用 MathJax 引擎。
SSH Deploy Key 配置
GitHub 在国内连接不稳定,终端用 git 命令常失败,所以配置 SSH 密钥来提升部署稳定性。
生成 SSH 密钥对
1 | ssh-keygen -t rsa -N '' -f /home/${USER}/.ssh/id_rsa_github_blog -q |
.pub 结尾的是公钥。
添加 Deploy Key
进入 GitHub 仓库设置页,填写标题,粘贴公钥内容,勾选 “Allow write access”,点击 Add key。
配置 SSH Config
Linux(~/.ssh/config):
1 | Host github.com |
Windows(C:\Users\Administrator\.ssh\config):
配置方法大体相同,如果遇到端口 22 被屏蔽:
1 | Host github.com |
出现以下提示时键入 yes 即可:
1 | The authenticity of host '[ssh.github.com]:443' can't be established. |
LeanCloud 评论与阅读量
评论功能(Valine)
在 LeanCloud 仪表盘的应用凭证中找到 AppID 和 AppKey,配置 NexT 主题的 _config.yml:
1 | # Valine |
阅读量功能
在 LeanCloud 中新建应用,数据存储 → 结构化数据 → 新建 Class,命名为 Counter,选择无限制。然后在主题 _config.yml 中配置:
1 | leancloud_visitors: |
Gitee / GitHub 镜像同步
虽然 Gitee 的实名认证没通过,但可以设置为从 GitHub 自动同步。
在 Gitee 仓库 → 管理 → 仓库镜像管理 → 添加镜像,设为 Pull 方向,选择镜像仓库和 GitHub 私人令牌。
申请 GitHub Token:用户头像 → Settings → Developer setting → Personal access tokens → Generate new token,勾选 repo 和 admin:repo_hook 字段。
详细教程参考 Gitee 官方文档。
SEO 优化
生成 Sitemap
1 | npm install hexo-generator-sitemap --save |
在 _config.yml 中配置:
1 | sitemap: |
推送到必应
进入 Bing Webmasters 进行配置。
文件目录管理
随着文章增多以及 Obsidian 记录习惯的养成,决定在 _posts 目录下按类别建立文件夹,每篇文章单独建文件夹,图片和文章放在同一级。
移动后发现 {% post_link %} 标签会报错,改用替代方案后解决。之前配置的拼音地址在分类目录上起了作用——提前铺路总是好事。
适配 Obsidian 双链引用
使用 hexo-backlink 插件将 Obsidian 的 [[WikiLink]] 转换为 Hexo 站内文章链接:
1 | npm install hexo-backlink |
在 _config.yml 中配置:backlink: true
Obsidian 中的设置:设置 → 文件和链接 → 新链接格式选择"文件的相对路径",并开启"使用 [[维基链接]]"。
字数统计
使用 hexo-word-counter 统计文章字数及阅读时间:
1 | npm install hexo-word-counter |
在站点 _config.yml 中添加:
1 | symbols_count_time: |
在 NexT 主题 _config.yml 中设置 item_text_total: true。
参数说明:中文为主的博客推荐设置 awl: 2、wpm: 300。
文章短链接(abbrlink)
Hexo 默认生成的文章地址是类似 YEAR/MONTH/DAY/TITLE 的长路径,既不利于 SEO 也不利于链接持久化。
方案一:hexo-permalink-pinyin(拼音转写)
1 | npm install hexo-permalink-pinyin --save |
1 | permalink_pinyin: |
但这个方法仍然依赖文章标题,改了标题链接也会变。
方案二:hexo-abbrlink(短链接,推荐)
hexo-abbrlink 基于文章标题生成固定短链接(CRC16 或 CRC32),文章内容不变链接就不变,不依赖分类和标题。
1 | npm install hexo-abbrlink --save |
在 _config.yml 中配置:
1 | # abbrlink config |
踩坑记录
之前安装了这个插件但一直没生效,排查后发现原因很简单:permalink 没有使用 :abbrlink。
插件在 before_post_render 阶段工作,它会读取(或生成)文章的 abbrlink 值并注入到数据中。但 Hexo 最终生成链接时,只用 permalink 模式中声明的变量。如果 permalink 里只有 :category 和 :title,那 :abbrlink 再好看也不会出现在 URL 里。
所以配置 hexo-abbrlink 的关键就两步:
- 安装插件 + 写好
abbrlink配置段 - 修改
permalink为posts/:abbrlink/(或其他含:abbrlink的模式)
这样生成的链接就是稳定的短链接了:
1 | https://mxhanks.github.io/posts/48721/ # 而不是 /技术踩坑/hexopostassetfix/ |
旧链接兼容
开启 abbrlink 后,之前以旧格式发布的所有页面链接都会变化。被搜索引擎收录或外部引用的旧链接会 404。建议:
- 更新 sitemap 让搜索引擎重新抓取
- 如果使用 GitHub Pages 或 Nginx,可配置 301 重定向将旧路径永久指向新路径
post_asset_folder 图片路径修复
问题背景
我的博客文章组织和大多数人不太一样——习惯用 Obsidian 写文章,并且让图片和 markdown 文件保持在同一级目录。
目录结构类似:
1 | source/_posts/ |
在 Hexo 开启 post_asset_folder: true 的情况下,图片却一直无法正常显示——<img> 的 src 指向了根目录 /output_6_1.png,自然是 404。
原因分析
当 _config.yml 启用以下配置时:
1 | post_asset_folder: true |
Hexo 会为每篇文章创建一个同名子文件夹作为资源目录。例如文章 my-post.md 的预期资源目录是 my-post/。
但我的文章本身就在文件夹中,且文件夹名和文章名相同(Regression/Regression.md)。Hexo 计算的 asset_dir 变成了:
1 | source/_posts/人工智能/Regression/Regression/ |
注意出现了两层 Regression/——第一层是文章所在的目录,第二层是 Hexo 自动追加的同名子文件夹。而我的图片放在第一层(与 Regression.md 同级),不在这个二层子文件夹里。
所以在构建流程中:
- Processing 阶段:
scanAssetDir扫描.../Regression/Regression/→ 目录不存在 → 没注册任何PostAsset hexo-renderer-marked渲染:postAsset: true查找PostAsset.findById("...output_6_1.png")→ 没找到 → 回退到url_for("output_6_1.png")→ 输出<img src="/posts/688035c/output_6_1.png" >→ 404
解决方案
核心思路是在渲染之前纠正图片 URL,在生成阶段把图片复制到正确位置。用了一个 Hexo 脚本(scripts/fix_asset_dir.js)实现两个过滤器:
Phase 1:注册 PostAsset(before_generate,优先级 1)
在 render_post 之前运行,扫描符合"同名目录"模式的文章目录,为同目录下的图片手动注册 PostAsset,让资源生成器把图片复制到 public 目录中。
Phase 2:修复 HTML 中的 <img> 路径(after_post_render)
文章渲染完成后,将 <img src="/posts/688035c/output_6_1.png" > 替换为正确的绝对路径。
最终效果
| 项目 | 修复前 | 修复后 |
|---|---|---|
<img src> |
/output_6_1.png ❌ |
/posts/28955/output_6_1.png ✅ |
图片文件在 public/ |
不存在 ❌ | 存在 ✅ |
多篇受影响的文章(Regression、Classification、Regularization、STM32 等)均自动修复,无需修改 markdown 源文件。
为什么不改源文件?
Obsidian 和 Hexo 共享同一套源文件。Obsidian 中图片就是和 md 同级的相对路径,改了 Hexo 的构建逻辑去适配它,比反过来改 Obsidian 配置更可持续。
自建服务器部署
博客一直部署在 GitHub Pages 上,国内偶有不稳定。手头有一台云服务器(CentOS + 宝塔面板),就把博客也同步部署上去,实现双线访问。
需求
- 保留 GitHub Pages 部署不变
- 增加自建服务器同步
hexo d一条命令搞定两边- 不需要手动上传文件
方案对比
| 方案 | 原理 | Windows 兼容性 |
|---|---|---|
hexo-deployer-rsync |
本地 rsync → 远程 rsync over SSH | ❌ Windows 无 rsync |
hexo-deployer-sftp |
Node.js ssh2 库直连 SFTP | ⚠️ 密钥格式问题 |
| 自定义 tar+ssh | tar 打包 → SSH pipe → 远端解压 | ✅ |
前两个方案在 Windows 上都遇到了问题,最终选择了第三种。
SSH 密钥配置
1 | ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_blog_server -N "" -C "blog-server" |
配置 ~/.ssh/config:
1 | Host 47.115.49.75 |
Nginx 配置
1 | server { |
核心:tar + SSH 管道
1 | cd public && tar cf - . | ssh root@47.115.49.75 "tar xf - -C /www/wwwroot/default" |
原理:tar cf - . 把当前目录打包输出到 stdout,通过 SSH 管道发送到远端解压。
优点:不需要 rsync、tar 和 ssh 在 Git Bash 中都是原生可用、一条管道效率高。
踩坑:Hexo 7 的事件 vs 过滤器
第一次尝试用 hexo.extend.filter.register('after_deploy', ...) 来挂载服务器部署,但脚本加载了却从不执行。查了 Hexo 7 源码才发现问题——部署完成时触发的是事件而不是过滤器:
1 | // node_modules/hexo/dist/plugins/console/deploy.js |
所以应该用:
1 | // ❌ 没用——Hexo 没有 'after_deploy' 过滤器 |
最终脚本
完整脚本见 scripts/deploy-to-server.js,Hexo 会自动加载 scripts/ 目录下的 .js 文件,无需额外配置。部署时执行顺序:
hexo-deployer-git推送到 GitHub Pages- Hexo 触发
deployAfter事件 - 脚本执行 tar+ssh 同步到私有服务器
密钥格式教训
Windows 上用 ssh-keygen 默认生成 OpenSSH 格式,但很多 Node.js 的 SSH 库(ssh2、node-ssh 等)只支持 PEM 格式。如果需要转换:
1 | ssh-keygen -p -m PEM -f ~/.ssh/id_rsa_xxx -P "" -N "" |
注意转换后公钥不变,不需要重新添加到服务器。
最终效果
| 项目 | 说明 |
|---|---|
| GitHub Pages | https://mxhanks.github.io ✅ |
| 自建服务器 | http://47.115.49.75 ✅ |
| 部署命令 | hexo g && hexo d 两边同步 |
LeanCloud 替代:自建阅读量计数 API
背景
博客一直用 LeanCloud 做文章阅读量统计,NexT 主题内置了 leancloud_visitors 支持,配合前端的 lean-analytics.swig 模板工作得很好。
但 LeanCloud 宣布将在 2027 年 1 月 停止服务,需要找一个替代方案。刚好手头有自建服务器,决定写一个兼容 LeanCloud API 的计数服务。
方案选型
NexT 主题其实内置了三个统计方案:
| 方案 | 维护成本 | 可控性 | 备注 |
|---|---|---|---|
| LeanCloud | 零 | ❌ | 即将关停 |
| 不蒜子 (Busuanzi) | 零 | ❌ | 第三方服务,历史数据无法迁移 |
| Firebase Firestore | 低 | ⚠️ | Google 服务,国内访问慢 |
| 自建 API(最终选择) | 低 | ✅ | 已有服务器,数据完全可控 |
自建服务架构
1 | 浏览器 (fetch) → Nginx (/counter-api/) → Node.js (Express) → SQLite (counter.db) |
NexT 主题的前端 JS(lean-analytics.swig)会调用三个接口:
| 操作 | HTTP 请求 |
|---|---|
| 查询某篇文章的计数 | GET /1.1/classes/Counter?where={"url":"..."} |
| 首次访问创建记录 | POST /1.1/classes/Counter |
| 递增计数 | PUT /1.1/classes/Counter/:id {"time":{"__op":"Increment","amount":1}} |
用 Express + better-sqlite3 写了个约 100 行的服务,完整模拟了这三个接口。前端代码不需要改一行——只要在主题配置中把 server_url 指向自建服务即可。
数据迁移
LeanCloud 导出的 JSON 中有 44 条计数记录,但因为改过文章链接(从日期路径 /2024/02/12/title/ 和拼音分类路径 /人工智能/classification/ 迁移到 /posts/:abbrlink/),大部分旧 URL 已经失效。
写了一个迁移脚本做三件事:
- 删除 time ≤ 1 的记录(只有自己点过一次的无效数据)
- 通过 URL 中的拼音关键词匹配现有文章(如
regularzation→/posts/9044/) - 合并同篇文章的多个旧 URL 计数(同一篇文章可能有过日期路径和拼音路径两条记录)
匹配结果示例:
1 | 302 /posts/57901/ ← Windows mklink 使用 |
部署
服务部署在 CentOS Stream 10 上,用 systemd 管理进程:
1 | # 安装 Node.js |
Nginx 反代配置:
1 | location /counter-api/ { |
主题配置只需改 server_url(themes/next/_config.yml):
1 | leancloud_visitors: |
踩坑记录
迁移脚本的 Bug:第一次跑迁移时,匹配到的旧记录虽然插入了新 URL,但旧记录本身没有删除。结果数据库里同时存在新旧两条记录,计数翻倍。排查后发现是 toDelete 数组漏加了匹配记录的 rowid,补上后重新跑就正常了。
SSH 连接偶发退出码 255:连续执行多条 SSH 命令时,偶尔会出现 Exit code 255。原因可能是服务器资源紧张或 SSH 会话被复用。解决方法:加 sleep 间隔或将多个命令合并到一次 SSH 连接中执行。
NexT 主题的 hostname 校验导致计数不显示:部署完成后发现页面阅读数始终显示为 0,API 返回数据却是正常的。排查发现 NexT 的 lean-analytics.swig 模板中有这么一行:
1 | if (CONFIG.hostname !== location.hostname) return; |
网站 _config.yml 中配置的 url: http://mxhanks.github.io/ 决定了 CONFIG.hostname 的值。但自建服务器部署在 47.115.49.75 上,location.hostname 是 IP 地址,两者不相等,计数脚本直接 return 了。
修复方法很简单——当使用自定义 server_url(自建 API)时,跳过这个 hostname 校验:
1 | if (!server_url && CONFIG.hostname !== location.hostname) return; |
这样既保留了原版在 LeanCloud 下的保护行为(防止开发环境污染计数),又让自建 API 场景下能够正常工作。
HTTPS 问题
计数器 API 目前通过 HTTP 提供服务。博客在 GitHub Pages(HTTPS)上访问时,浏览器会因混合内容策略阻止 HTTP 请求,所以计数只在自建服务器版本上生效。
解决方案是申请一个域名配 Let’s Encrypt 证书,备案完成后将 stats.你的域名.com 解析到服务器,即可同时服务两个站点。
完整代码
服务端代码不到 100 行,放在博客项目的 server/ 目录下:
1 | server/ |
写在最后
从 2021 年初建到现在,博客的形态变了很多次。从一开始连 GitHub 都不会用,到后来自己写 Hexo 脚本、配服务器,每一步踩坑都成了经验。这些折腾的价值或许不只是技术本身——它让我习惯了记录、整理和分享,而这一点大概是写博客最大的收获。
如果你也在折腾 Hexo 博客,希望这篇文章能帮你少走一些弯路。