Build Blog with Hexo
1. Git 安装
1.1 Git Bash 和 CMD
注意, Git Bash 和 CMD 不全相同
CMD是Windows自带的命令解释器, 是Windows系统内置的
Git Bash是一个在 Windows 上模拟 Linux/Unix shell 环境 的终端, 是基于 MSYS2/MinGW(第三方 Unix-like 兼容层)的.
简单来说, CMD是Windows系统使用的, Git Bash是在Windows上模拟Unix系统的终端. 由于我比较熟悉Linux环境下的操作, 所以我会尽量多的使用 Git Bash, 对Git Bash有更多的要求.
1.2 安装Git Bash
去Git官网下载安装包,开始安装程序(管理员模式).
勾选Add a Git Bash profile to Windows Terminal, 在 Windows Terminal 中自动添加一个“Git Bash”启动项(Profile),可以一键打开 Git Bash 终端.
其余选项不动, 安装
1.3 Git 配置
打开Git Bash
配置全局设置用户名和邮箱:
1 | git config --global user.name "Your Name" |
用下面命令查看配置:
1 | git config --global --list |
下面配置SSH公钥:
配置了ssh公钥之后, 本机push到github的时候就不需要登录了
检查是否已存在ssh key, 密钥不存在
1
2cd ~/.ssh
bash: cd: /c/Users/test/.ssh: No such file or directory生成密钥(执行后一直回车即可)
1
ssh-keygen -t rsa -C "xxx@xxx.com"
获取ssh key公钥内容(id_rsa.pub)
1
2cd ~/.ssh
cat id_rsa.pubGithub账号上添加公钥
头像->setting->SSH and GPG keys-> New SSH key
然后添加新的ssh密钥即可
使用
ssh -T git@github.com验证1
2
3test@ѩ▒ MINGW64 ~/.ssh
$ ssh -T git@github.com
Hi AskemiWang! You've successfully authenticated, but GitHub does not provide shell access.
1.4 题外话 CMD, Power Shell and Git Bash
ls是unix命令, Windows的CMD并不支持, power shell为了兼容所以能用. 而tree命令这种Windows原生自带的, 两者都可以使用, 但是git bash使用不了.
因为git bash只能使用unix的tree命令, 但是我们Windows上的git bash是基于 MSYS2(Minimal SYStem 2),提供一个类 Unix 环境。
Git for Windows(即 Git Bash)只打包了运行 Git 所需的最小工具集。这是一个轻量化的MSYS2, 并不完整, 所以很多Unix上的工具, 在git bash上无法使用.
2. Nodejs 安装
2.1 Nodejs 安装
Hexo是基于Node JS框架开发的, 所以先安装Node JS
下载完毕开始安装, 会自动安装nodejs和npm, 安装完后检查:
1 | test@▒ѩ▒ MINGW64 ~ |
可以看到版本信息, 已经安装好了.
2.2 环境变量配置
先打开nodejs文件夹的位置, 新建两个文件夹node_cache和node_global
1 | D:\softwares\nodejs |
用powershell可以查看到:
1 | ls | ? Name -like "*node*" |
1 | PS D:\softwares\nodejs> ls | ? Name -like "*node*" |
注意, ls是unix命令, Windows的CMD并不支持, power shell为了兼容所以能用. 而tree命令这种Windows原生自带的, 两者都可以使用, 但是git bash使用不了.
因为git bash只能使用unix的tree命令, 但是我们Windows上的git bash是基于 MSYS2(Minimal SYStem 2),提供一个类 Unix 环境。
Git for Windows(即 Git Bash)只打包了运行 Git 所需的最小工具集。这是一个轻量化的MSYS2, 并不完整, 所以很多Unix上的工具, 在git bash上无法使用.
用管理员权限配置:
1 | npm config set prefix "D:\softwares\nodejs\node_global" |
用get命令检查:
1 | test@▒ѩ▒ MINGW64 /d/softwares/nodejs |
打开系统的环境变量
系统变量->新建:
变量名: NODE_PATH
变量值:
D:\softwares\nodejs\node_global\node_modules注意, 变量值后面加了一级子目录 node_modules
用户变量中Path项->编辑->新建:
添加项:
D:\softwares\nodejs\node_global系统变量->编辑->新建:
添加项: %NODE_PATH%
检查(记得开管理员模式):
1 | test@▒ѩ▒ MINGW64 ~ |
3. Pandoc 安装(用于LaTex渲染)
Pandoc 是一个独立于 Hexo 之外的、安装在你的 Windows/Mac 电脑系统里的命令行文档转换工具。它的核心作用是将一种标记语言转换成另一种格式(比如把 Markdown 转成 HTML、PDF、Word 等)。 在 Hexo 博客中,它的作用是负责“翻译”你的 Markdown 文件。
下载x86_64版本的msi文件: Release pandoc 3.10 · jgm/pandoc
将pandoc.exe添加PATH:
编辑系统环境变量:
系统变量”区域,找到
Path点击 “新建”:
输入 Pandoc 的路径,例如:
D:\softwares\Pandoc\重启Git Bash确认:
1
2
3
4
5
6
7
8
9test@▒ѩ▒ MINGW64 /e/myBlog (main)
$ pandoc --version
pandoc 3.10
Features: +server +lua
Scripting engine: Lua 5.4
User data directory: C:\Users\test\AppData\Roaming\pandoc
Copyright (C) 2006-2025 John MacFarlane. Web: https://pandoc.org
This is free software; see the source for copying conditions. There is no
warranty, not even for merchantability or fitness for a particular purpose.
4. Hexo 安装
4.1 安装Hexo命令行工具
安装命令:
1 | npm install -g hexo-cli |
check:
1 | test@▒ѩ▒ MINGW64 ~ |
4.2 创建一个Blog项目
创建文件夹:
1 | mkdir myBlog && cd myBlog |
初始化hexo项目:
1 | hexo init |
根据 package.json 安装项目所需的所有依赖包:
1 | npm install |
生成测试博客并启动本地服务器:
hexo g
hexo s
1 | test@ѩ▒ MINGW64 /e/myBlog |
访问:
4.3 创建一个特殊的GitHub仓库
GitHub上创建一个新仓库, 名字为
1 | YourGitHubName.github.io |
比如:
1 | AskemiWang.github.io |
这个仓库为什么特殊?
GitHub 对名为 用户名.github.io
的仓库有特殊待遇,它被视为你的用户站点(User
Site)。GitHub可以自动自动启用 GitHub Pages服务,
简单来说方便提供网页服务.
一定要将可见性设置为Public
4.4 将Hexo部署到GitHub
修改_config.yml文件的deploy部分, 原来的样子是这样的:
1
2
3
4# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: ''修改为:
1
2
3
4
5
6# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo: git@github.com:AskemiWang/AskemiWang.github.io.git
branch: main修改URL:
1
2
3# URL
## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'
url: https://AskemiWang.github.io注意, 仓库地址最好用SSH的
4.5 安装hexo-deployer-git插件
这个插件的作用是帮我们更新博客, 推送到github上(相当于集成了git add, git push等命令)
1 | npm install hexo-deployer-git --save |
4.6 推送博客到GitHub
每次推送前需要的步骤:
- 清除前一次生成的文件
- 生成新的文件
- 部署
1 | hexo clean |
用一条命令解决:
1 | hexo clean && hexo g -d |
1 | test@ѩ▒ MINGW64 /e/myBlog |
4.7 通过网页访问
访问:
1 | https://yourname.github.io |
比如:
4.8 Hexo常用命令
1 | # 初始化 |
5. 绑定域名
5.1 购买域名
GitHub使用Git Pages服务实现了通过访问用户站点网页来访问博客.但是名字太长了也不好记, 可以通过更换域名来解决这个问题.
在阿里云上购买域名, 并在控制台里面查询
1 | https://home.console.aliyun.com/ |
阿里云真是扫码了, 开了代理后, 会自动跳到alibaba, 然而阿里巴巴没法用阿里云(国内)的邮箱登录.
5.2 添加解析
5.2.1 原理简述
下面解析域名, 也就是把我们的域名和IP关联起来,
一种可行的方案是使用CNAME的方式,
直接把我们的个人域名(xxx.cn)和主机记录(www)和刚刚的仓库域名yourname.github.io绑定,
但是这种方式下,
我们的个人域名访问只能是(www.xxx.cn)而不能访问xxx.cn.
所以我们使用A记录类型, 这种类型是绑定GitHub
Pages的IPV4地址.
实际上的访问流程是这样工作的:
\[ Personal Domain \xrightarrow{DNS} \text{GitHub Pages}\xrightarrow{通过DNS中的Host字段查询}CertainRepo \] 我们把域名和GitHub Pages绑定, 这样访问的时候会直接访问到GitHub Pages的IP地址, 那么如何从GitHub Pages的IP确定到我们个人的仓库呢?
当用户访问自定义域名时,DNS 解析会将请求指向 GitHub Pages 的共享 IP 地址,随后浏览器在 HTTP 请求头中携带该域名信息;GitHub 服务器接收到请求后,会读取仓库根目录下的 CNAME 文件来验证域名归属并匹配对应的仓库内容,最终在同一共享 IP 上直接返回该仓库生成的静态页面。
5.2.2 具体解析步骤
在域名管理处添加新的记录
添加记录使
根域名能直接访问:记录类型: A(指向IPV4地址)
主机记录: @(访问根域名)
记录值: 填写官方提供的四个IP: IP查询
1
2
3
4185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
万维网(www)访问
记录类型: CNAME
主机记录: www
记录值: 仓库的实际域名
1
根域名
然后在我们的blog文件夹的source路径下新建CNAME文件(无后缀), 并添加我们的域名
1 | test@ѩ▒ MINGW64 /e/myBlog |
5.3 重新部署
1 | hexo clean && hexo g -d |
5.4 GitHub Pages验证域名所有权(域名绑定旧仓库问题)
前面讲过, 我们的DNS请求是到GitHub Pages上的, 还需要一步是通过CNAME, 建立一个GitHub Pages到我们仓库的映射.
如果我们在GitHub上其他仓库中绑定了域名, 访问我们自己的域名时, 会提示GitHub Pages的404, 这说明我们从域名到GitHub Pages成功了, 但是从GitHub Pages到我们的仓库没成功. 这个时候需要我们验证这个域名是自己的.
- 点击头像
- 选择Settings
- 左边侧边栏找到Pages
- 点击 Add a domain, 添加我们的根域名
- 会看到一段提示,要求你添加一条 DNS TXT 记录来验证所有权。页面会显示一个 TXT record 和 TXT record value
- 在购买域名的网站, 添加新的记录:
- 记录类型: TXT
- 主机记录: TXT record
- 记录值: TXT record value
- 添加完后在GitHub上点击verify
此时我们需要在仓库的settings -> pages
->Custom domain这里,
填入我们自己的根域名xxx.cn
5.5 注意: 绑定域名后要额外修改配置
前面我们将_config.yml中的URL绑定到了我们的github域名上, 现在需要修改至我们的主域名.
6. Hexo全局配置
6.1 Hexo的_config.yaml配置
_config.yml文件是Hexo的全局配置文件.
配置项如下:
Site: 定义博客的全局“名片”信息。这些内容会被注入到网页的 HTML 头部(
<title>,<meta>标签),直接影响浏览器标签页的显示和搜索引擎的抓取(SEO)。- title: 网站主标题(显示在浏览器标签页和网站页头)。
- subtitle: 网站副标题。
- description: 网站描述(用于
<meta name="description">,搜索引擎摘要主要抓取这里)。 - keywords: 网站关键词(用于
<meta name="keywords">,多个词用逗号隔开)。 - author: 默认作者名称(当文章 Front-matter 中未指定作者时,使用此名称)。
- language: 网站语言(如
en,zh-CN。修改此项可让主题界面的按钮、提示语切换为对应语言)。 - timezone: 网站时区(如
Asia/Shanghai。影响文章发布时间的显示,留空则使用系统时区)。
URL: 控制网站的根地址以及文章永久链接(Permalink)的生成格式。
url: 网站的完整根网址。(注意:绑定自定义域名后,这里必须改为个人的域名比如https://wangyier.top,否则图片等资源的绝对路径会出错)。root: 网站的根目录(默认为/。如果部署在子目录下如/blog/,需改为/blog/)。permalink: 文章的 URL 格式, 也就是你生成某个文章时的那个链接格式。比如我新建一个文章叫
temp.md,那么这个时候他自动生成的地址就是http://yoursite.com/2018/09/05/temp。当前:year/:month/:day/:title/表示生成类似2026/06/25/hello-world/的链接,非常利于 SEO。permalink_defaults: 当 permalink 中的变量为空时,使用的默认值。pretty_urls: URL 美化设置。trailing_index: 设为false可隐藏 URL 末尾的index.html。trailing_html: 设为false可隐藏 URL 末尾的.html后缀。
Directory: 定义 Hexo 项目内部各个功能文件夹的物理存放路径。通常保持默认即可。
source_dir: 源文件目录(存放 Markdown 文章、图片等,默认source)。public_dir: 生成的静态网站输出目录(默认public,部署时就是把这个文件夹推送到 GitHub)。tag_dir/archive_dir/category_dir: 标签页、归档页、分类页的生成目录名。code_dir: 存放包含的代码片段的目录。i18n_dir: 多语言模板目录。skip_render: 指定不需要被 Hexo 渲染、直接原样复制到public目录的文件或文件夹。
Writing: 控制新建文章的默认行为、外部链接处理逻辑以及代码高亮渲染引擎。
new_post_name: 新建文章时的文件名格式(默认:title.md)。default_layout: 默认布局(post文章,page独立页面,draft草稿)。titlecase: 是否将标题自动转换为首字母大写(Titlecase)。external_link: 外部链接设置。enable: 是否在新标签页打开外部链接。field: 应用范围(site全站,post仅文章)。
filename_case: 文件名大小写转换(0: 不转换,1: 全小写,2: 全大写)。render_drafts: 是否渲染草稿(设为true则草稿也会生成静态页面)。post_asset_folder: 是否开启“文章资源文件夹”(开启后,新建文章会自动创建一个同名文件夹,方便管理文章内的图片)。relative_link: 是否使用相对链接(通常配合 CDN 或子目录部署使用,默认false)。future: 是否显示未来日期的文章(设为true则即使文章日期是明天,也会显示)。syntax_highlighter/highlight/prismjs: 代码高亮引擎及其详细配置(控制代码块是否显示行号、是否自动识别语言、缩进替换等)。
Home page setting: 专门控制博客首页(Index)的生成和展示逻辑
path: 首页的 URL 路径(默认为空,即根目录/)。per_page: 首页每页显示的文章数量(设为0则关闭分页,显示所有文章)。order_by: 文章排序方式(-date表示按日期倒序,即最新的文章排在最前面)。注意:安装hexo-generator-index-pin-top(见 7.4)后,首页排序由该插件接管,本项对首页失效(分类页/标签页/存档页等其他生成器不受影响)。
Category & Tag: 设置文章分类和标签的默认行为及 URL 映射。
default_category: 当文章未指定分类时,自动归入的默认分类名(默认uncategorized未分类)。category_map/tag_map: 分类/标签的别名映射字典。用于将中文分类名映射为英文 URL 路径(例如将前端映射为frontend,使 URL 变成/categories/frontend/)
Metadata elements: 控制网页
<head>中自动生成的 meta 标签。meta_generator: 是否在网页头部生成<meta name="generator" content="Hexo">(用于标识网站是用 Hexo 生成的,设为false可隐藏)。
Date / Time format: 自定义文章日期和时间的显示格式(基于 Moment.js 语法)。
date_format: 日期格式(如YYYY-MM-DD)。time_format: 时间格式(如HH:mm:ss)。updated_option: 当文章 Front-matter 中没有明确写updated字段时,如何获取更新时间(mtime表示使用文件的最后修改时间)。
Pagination: 控制全站(如归档页、分类页、标签页)的分页行为。
per_page: 全局每页显示的文章数(如果首页设置了per_page,则首页优先使用首页的设置)。pagination_dir: 分页的 URL 目录名(默认page,生成的 URL 类似/page/2/)。
Include / Exclude file(s): 精细控制
source目录下的文件在生成时是否被处理。include: 强制包含的文件(即使它们被 ignore 规则排除了)。exclude: 排除的文件(不会被复制到public目录)。ignore: 忽略的文件/目录(Hexo 完全不处理它们)。
Extensions: 配置博客使用的主题和插件。
theme: 当前使用的主题名称。当前为 Hexo 官方默认的landscape主题(如果要换主题,只需修改这里并安装对应主题即可)。
Deployment: 配置
hexo deploy命令的行为,定义如何将生成的静态文件推送到远程服务器。type: 部署类型。当前为git(需要安装hexo-deployer-git插件)。repo: 目标仓库地址。当前使用的是 SSH 协议 (git@github.com:...),这比 HTTPS 更稳定且免密。branch: 推送的目标分支。当前为main(对应 GitHub 仓库的默认分支)。
6.2 category_map和
tag_map
Category和Tag需要额外注意:
Category是文章分类, 比如可以分为: 学习, 生活, 娱乐 …
Tag是文章具体标签, 比如Python, Hash, C++, Transformer…
category_map是做一个映射. 可以实现大小写统一, 缩写统一, …
大小写统一: 比如配置
Tech: tech, 这样文章实际的category无论是大写还是小写, 都会映射成.../tech统一缩写, 比如
JS: JavaScript,JavaScript: javascript特殊符号美化, 像
GitHub Pages这种带空格的URL会转义成%20,非常不美观, 所以可以映射出一个不带空格的GitHub Pages: GitHub-Pages1
2
3
4category_map:
Tech: tech
Life: life
Entertaiment: entertaiment1
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
40tag_map:
#Hexo
HEXO: hexo
Hexo: hexo
# LLM
LLM: llm
Large Language Model: llm
large language model: llm
LargeLanguageModel: llm
# ML
ML: ml
Machine Learning: ml
machine learning: ml
MachineLearning: ml
# DL
DL: dl
Deep Learning: dl
deep learning: dl
DeepLearning: dl
# AI
AI: ai
Artificial Intelligence: ai
artificial intelligence: ai
ArtificialIntelligence: ai
# transformer & agent
Transformer: transformer
Agent: agent
# programming languages
Python: python
C: c
C++: cpp
c++: cpp
JavaScript: javascript
JS: javascript
# computer science
OS: os
Operating System: os
operating system: os
OperatingSystem: os
7. Hexo 优化
2026-10-26 已迁移到 Butterfly 5.7.0:本章是 NExT 时期的记录,保留作参考;迁移清单、配置对照和踩坑见第 18 章。
7.1 安装NexT主题
在blog根目录下, 下载主题:
1 | git clone https://github.com/next-theme/hexo-theme-next.git themes/next |
修改站点文件中的_config.yaml中的主题为next主题
1 | # Extensions |
7.2 修改新建文章时的模板
修改scaffolds/post.md 和 scaffolds/draft.md
如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14---
title: {{ title }}
date: {{ date }}
updated: {{ date }}
permalink:
top: 0
password:
comments:
copyright: true
tags:
categories:
keywords:
description:
---
7.3 Next主题
下面将对Next主题进行一系列设置,
对应的配置文件是:themes/next/_config.yaml
7.3.1 设置darkmode
修改主题配置文件themes/next/_config.yaml的
darkmode 部分, 现在默认是darkmode, 不用改:
1 | # Dark Mode |
7.3.2 修改网站图标
在 Hexo 的构建机制中,网站的根目录对应的是你本地项目中的
source 文件夹, 所以images文件夹的位置实际上是:
myBlog/source/images, 刚开始这四个图标是不存在的,
我们需要自备一张图片, 通过:Favicon
生成网站修改成favicon文件, 或者直接在线制作,
我是直接在网站上在线制作的:
1 | PS D:\chrome_download\favicon> ls . |
创建新site文件夹来存放这些图片:
1 | mkdir -p /e/myBlog/source/images/site |
1 | cp /d/chrome_download/favicon/* /e/myBlog/source/images/site/ |
修改配置文件的favicon部分:
1 | favicon: |
7.3.3 修改菜单栏
找到menu部分, 进行修改:
1 | menu: |
注意, 这里添加了categories, tags和 about页面的链接, 但是我们还没创建这三个页面.需要创建:
1 | hexo new page categories |
然后需要分别对三个文件做初始化:
分类页面 (Categories):
source/categories/index.md, 作用是告诉主题这里要渲染所有分类的列表。1
2
3
4
5---
title: Categories
type: categories
date: 2026-06-25 15:31:17
---标签页面 (Tags):
source/tags/index.md, 作用是告诉主题这里要渲染标签云或标签列表。1
2
3
4
5---
title: Tags
type: tags
date: 2026-06-25 15:31:14
---关于页面 (About):
source/about/index.md, 只需要正常的标题,然后在下方自由编写你的个人介绍(Markdown 正文)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20---
title: About Me
date: 2026-06-25 15:31:34
---
# Welcome to The Great Library
Welcome to my personal digital space! This blog serves as my knowledge base, where I document my learning journey, technical explorations, and daily reflections.
## Focus Areas
My studies and work primarily revolve around **Operating Systems** and **Large Language Models (LLMs)**. Consequently, the majority of the articles here are dedicated to these fields, along with related programming languages like C and Python.
## Connect with Me
Feel free to reach out, collaborate, or just say hi:
- 💻 **GitHub**: [AskemiWang](https://github.com/AskemiWang)
- 📧 **Email**: [wye18280075427@gmail.com](mailto:wye18280075427@gmail.com)
7.3.4 边栏显示图像和社交信息
修改主题配置文件的 avatar 和 social 字段,
注意要替换头像文件
先创建存放头像的文件夹, 并放入头像文件:
1 | mkdir -p source/images/avatar |
1 | # Sidebar Avatar |
7.3.5 修改页脚时间和作者之间的图标
footer字段用来修改页脚部分的内容, since可以打开, 记录从多久开始建站; icon的animated也可以打开, 这样红心就会跳动了
1 | footer: |
7.3.6 侧栏阅读进度提示
找到back2top字段, 修改scrollpercent为true, 这样会有阅读百分比提示:
1 | back2top: |
7.3.7 阅读进度条
找到reading_progress字段, 修改:
1 | # Reading progress bar |
7.3.8 中英文自动加空格(不要使用!)
本来通过安装插件npm install hexo-pangu,
并且开启服务pangu选项, 可以增加中英文的空格, 更加美观.
1 | pangu: true |
实际上有个严重的问题: pangu在处理超链接的时候对中括号处理有问题, 所以一定要禁用:
1 | pangu: false |
7.3.9 开启“图片点击放大”功能
开启fancybox后,读者点击文章里的任何图片,都会弹出一个优雅的黑色遮罩层进行放大查看,体验极佳。
1 | fancybox: true |
7.3.10 开启“图片懒加载”(提升加载速度)功能
如果文章图片多,开启懒加载可以让图片只在滚动到屏幕可视区域时才加载,并带有淡入动画,既美观又省流量:
1 | lazyload: true |
7.3.11 开启“页面顶部加载进度条”
在页面加载时,顶部会出现一条细细的进度条,消除用户的等待焦虑,非常有科技感。
1 | pace: |
7.3.12 开启”本地搜索”功能
需要先安装插件:npm install hexo-generator-searchdb
然后设置字段:
1 | local_search: |
7.3.13 开启“访客与阅读量统计”
1 | busuanzi_count: |
6.3.14 开启“评论系统”(推荐 Utterances)
打开 Utterances 的官方配置页面:https://utteranc.es/
点击蓝色的 utterances app 链接(这会跳转到 GitHub 的授权页面)。
在 GitHub 页面中,选择 Only select repositories(仅选择特定仓库),然后在下拉菜单中勾选你的博客仓库.
点击绿色的 Install 按钮完成授权
在next配置文件中找到utterances字段并修改:
1 | utterances: |
7.3.14 修改主题配置文件, 增加版权说明
先版本有自带的版权功能.下面是老方法:
修改custom_file_path字段:
1 | custom_file_path: |
这里相当于从source/_data中读取了一些配置,
我们需要手动创建:
1 | mkdir -p /e/myBlog/source/_data |
1 | touch /e/myBlog/source/_data/head.njk |
styles.styl写入下面的配置来使版权说明更加好看:
1 | /* ========================================== |
7.3.15 鼠标点击爱心特效
创建love.js
1
mkdir -p /e/myBlog/source/js
写入love.js文件
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
42cat > source/js/love.js << 'EOF'
!function(e, t, a) {
function r() {
for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x + "px;top:" + s[e].y + "px;opacity:" + s[e].alpha + ";transform:scale(" + s[e].scale + "," + s[e].scale + ") rotate(45deg);background:" + s[e].color + ";z-index:99999");
requestAnimationFrame(r)
}
function n() {
var t = "function" == typeof e.onclick && e.onclick;
e.onclick = function(e) {
t && t(), o(e)
}
}
function o(e) {
var a = t.createElement("div");
a.className = "heart", s.push({
el: a,
x: e.clientX - 5,
y: e.clientY - 5,
scale: 1,
alpha: 1,
color: c()
}), t.body.appendChild(a)
}
function i(e) {
var a = t.createElement("style");
a.type = "text/css";
try {
a.appendChild(t.createTextNode(e))
} catch (t) {
a.styleSheet.cssText = e
}
t.getElementsByTagName("head")[0].appendChild(a)
}
function c() {
return "rgb(" + ~~(255 * Math.random()) + "," + ~~(255 * Math.random()) + "," + ~~(255 * Math.random()) + ")"
}
var s = [];
e.requestAnimationFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function(e) {
setTimeout(e, 1e3 / 60)
}, i(".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"), n(), r()
}(window, document);
EOF在
source/_data/head.njk中写入配置:1
2
3
4cat > source/_data/head.njk << 'EOF'
<!-- 页面点击小红心特效 -->
<script type="text/javascript" src="/js/love.js"></script>
EOF
7.3.16 文章加密功能
安装插件
1
npm install hexo-blog-encrypt --save
在Hexo配置(myBlog/_config.yml), 注意, 不是next主题配置, 末尾添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# 文章加密配置 (hexo-blog-encrypt)
encrypt:
enable: true
# 首页显示的摘要提示
abstract: '🔒 这篇文章已被加密,请输入密码查看。'
# 密码输入框的提示文字
message: '请输入访问密码:'
# 密码错误时的提示
wrong_pass_message: '密码错误,请重新输入!'
# 【关键修改】使用内置主题,不再使用 template 写 HTML
# 可选值: default, blink, flip, shrink, surge, up, wave, xray
# 推荐使用 default 或 blink
theme: default
# 【消除警告】提高密码派生函数的迭代次数,增强防暴力破解能力
kdf:
iterations: 600000
7.3.17 网站运行时间统计
在source\_data\footer.njk写入:
1 | <!-- 网站运行时间计数 --> |
7.3.18 关闭next主题的标题自动排序
假如我们平时写markdown文档的时候, 就有良好的分级习惯, 就不需要next主题来帮我们排序, 不然会出现重复给标题分级的情况, 将toc字段的number关闭:
1 | toc: |
7.3.19 文章顶部显示tag
next主题中, 文章元数据中默认只显示category不显示tag,
所以我们要在post_metazpost_meta字段中添加tags:
1 | # Post meta display settings |
7.3.20 支持Latex渲染
原生的 hexo-renderer-marked 不支持latex, 需要安装新的渲染器 比如hexo-renderer-markdown-it 或者 解决:
确保没有安装不需要的渲染器:
1
2
3
4npm uninstall hexo-renderer-marked hexo-renderer-kramed --save
npm uninstall hexo-math mathjax --save
npm uninstall hexo-pangu --save
npm uninstall hexo-renderer-markdown-it markdown-it-mathjax3 --save添加pandoc渲染器:
注意, 由于我们的NodeJS没有装在系统盘, 需要以管理员权限打开git bash执行下面的命令:
1
2cd /e/myBlog
npm install hexo-renderer-pandoc --save在next主题中配置文件中, 修改math字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15math:
# Default (false) will load mathjax / katex script on demand.
# That is it only render those page which has `mathjax: true` in front-matter.
# If you set it to true, it will load mathjax / katex script EVERY PAGE.
every_page: true
mathjax:
enable: true
# Available values: none | ams | all
tags: none
katex:
enable: false
# See: https://github.com/KaTeX/KaTeX/tree/master/contrib/copy-tex
copy_tex: false修改Hexo的配置文件, 注意不是next的, 在文件末尾添加以下配置,这是一些pandoc的配置:
1
2
3
4
5
6
7
8# Pandoc 渲染器配置
pandoc:
mathjax: true
extensions:
- tex_math_dollars # 启用 $ 和 $$ 作为数学公式定界符
- tex_math_single_backslash # 兼容单反斜杠转义
- raw_html # 允许输出原始 HTML
- raw_tex # 允许 Pandoc 遇到无法转换的公式时,直接保留原始 LaTeX
7.4 首页排序: 置顶 + 按更新时间降序
hexo默认按时间索引对文章进行排序。本博客最初的方案是装第三方插件
hexo-generator-index-pin-top
来支持top置顶:
1 | npm install hexo-generator-index-pin-top --save |
但它只能”置顶优先 + 其余按date降序”,
不能按更新时间排;
而且它是用JS真值判断top的,
带着一个隐蔽的坑(见下)。所以后来改成博客自带的生成器:
scripts/index.js。
Hexo会把博客根目录scripts/下的所有.js当插件脚本加载,
且加载时机晚于node_modules里的插件 ——
于是同名生成器会被覆盖,
scripts/index.js成为首页排序的唯一实现(所以hexo-generator-index-pin-top已从package.json移除,
也不再需要hexo-generator-index)。顺带说明:
Hexo的script_dir在源码里被写死为<博客根>/scripts,
无法用配置修改,
因此Python流水线脚本都放在tools/而不是scripts/(否则Hexo会挨个require这些.py/.md文件,
每次构建刷一堆Script load failed报错)。
scripts/index.js的排序规则:
- 置顶优先:
top数值大的在前(top缺失或非纯数字一律按”不置顶”处理, 构建日志里会warn出具体文件); - 置顶之间、以及非置顶之间: 按
updated降序(没有updated则退回date); - 仍相同: 按
date降序, 保证跨次构建顺序稳定; - 每页
index_generator.per_page篇(本博客10篇), 第2页起路径为/page/N/(第1页是站点根/, 不是/page/1/)。
因此_config.yml里的index_generator.order_by对首页不再生效(存档页、分类页、标签页等其他生成器不受影响)。另外,
首页位置只跟front matter的updated有关: 改正文或其它front
matter字段时,
自动流程的第4步会在提交前把updated刷成当前时间(见15.2.4节);
而只翻图片路径形态、只做高亮转换、只调top开关都不算内容更新,
顺序不变。若想在不改内容的前提下调整顺序,
直接改那篇文章的updated即可(md文件自身的创建/修改时间与首页排序无关)。
7.4.1
top里混入不可见字符的坑
在front matter里写top: 0表示不置顶,
但top必须是纯数字。旧的pin-top插件是用JS真值判断”有没有top”的,
而YAML里只有数字0才是假值:
若top: 0后面混进了不可见字符(例如从左往右标记U+200E、零宽空格U+200B、BOM
U+FEFF), YAML会把它解析成字符串"0\u200e"
—— 字符串恒为真, 这篇文章就被当成了置顶文章;
更麻烦的是字符串与数字相减得到NaN(规范规定比较函数返回NaN时视作”相等”),
排序退化成按日期降序,
于是它会挤掉top: 100这类真正的置顶文章,
跑到首页最前面。本博客就踩过:
Model Post-Training.md与Paper Summary – SWE-smith.md的top: 0后面各藏了一个U+200E,
结果这两篇长期占着首页第1、2位。
现在的scripts/index.js不再有这个问题:
它用Number(top)判断,
非纯数字一律按”不置顶”处理,
并在构建日志里warn出具体文件(不会静默变成置顶,
也不会让排序退化成NaN)。另外top的改动不会刷新updated(置顶开关属于排版意图,
见15.2.4节), 所以修top不会把文章顶到”最近更新”的前面。
这类字符肉眼完全不可见,
靠编辑器很难发现。本博客的做法是在tools/auto_update_blog.py --selfcheck里加了一节检查:
扫描全部文章的front matter, 报告不可见字符(Unicode
Cf/Cc)、非整数top值、updated缺失或早于date的异常,
并预览按当前规则算出的首页前5篇, 命中即返回非零退出码:
1 | python tools/auto_update_blog.py --selfcheck |
排查时也可以用Python直接看原始码位,
一眼就能看出0后面是否藏了东西:
1 | import pathlib |
8. Hexo 发布新文章
8.1 去除md文件的空格
当我们使用hexo new命令创建文章的时候, 会调用我们的模板,
并且会默认地把我们的md文件的空格替换成-
1 | test@ѩ▒ MINGW64 /e/myBlog (main) |
比如这里默认把我们的Pro Tips & Commands for Essential Softwares & Tools替换成了Pro-Tips-Commands-for-Essential-Softwares-Tools.md
这样的好处是不需要操心permalink和title的事情, 但是不够美观, 我个人还是喜欢保留markdown文件的空格.
建议直接重命名, typora中的rename很快捷
8.2 Front-matter字段
使用hexo new filename即可在source/_posts下面创建对应的文章.
文章初始只有下面的一段:
1 | --- |
被两个 --- 包裹的区域,在 Hexo 中被称为
Front-matter(文章元数据)。它使用 YAML
语法编写,用于告诉 Hexo 和 Next 主题该如何处理、展示这篇特定的文章。
对于其他的md文件, 我们可以手动加上开头这一段来让它变成可以被hexo渲染的博客文件.
下面说明这些字段的功能和用法:
基础字段:
| 字段 | 作用 | 示例 |
|---|---|---|
| title | 文章标题。 显示在浏览器标签页、首页列表和文章页顶部。 |
title: Python 基础入门学习笔记 |
| date | 创建/发布时间。 Hexo 根据这个时间对文章进行排序 |
date: 2026-06-26 13:02:12 (保持默认即可) |
| updated | 最后修改时间。 如果你以后修改了文章,填上修改时间,Next 主题会在文章顶部显示“更新于 XXXX”。 |
updated: 2026-06-27 10:00:00 (不填则默认使用 date) |
| categories | 文章分类。 决定文章归属哪个专栏,会在侧边栏和分类页展示。 |
categories: [Tech] |
| tags | 文章标签。 提取文章的核心知识点,会在标签页展示。 |
tags: [Python, 编程基础] |
| description | 文章摘要。 1. 显示在首页文章列表中(代替自动截取的正文); 2. 作为网页的 <meta name="description">,对 SEO
极其重要。 |
description: 本文记录了 Python 环境搭建及基础语法的学习心得。 |
高级字段(按需使用,不用全填):
建议所有带特殊符号的文章名, 都使用一下permalink,
比如本文的名字是Build Blog with Hexo, 如果不改permalink,
文章的URL是:
1 | https://wangyier.top/2026/06/26/Build%20Blog%20with%20Hexo/ |
如果把permalink手动指定为Buil-Blog-with-Hexo/,
那么URL会变成:
1 | https://wangyier.top/2026/06/26/Buil-Blog-with-Hexo/ |
更加美观, 注意手动指定要加/
| 字段 | 作用 | 示例 |
|---|---|---|
| top | 文章置顶。 数字越大,在首页排得越靠前。设为 0 或删掉该行表示不置顶。 |
top: 1 (置顶) / top: 0 (不置顶) |
| password | 文章加密。 配合你之前安装的 hexo-blog-encrypt 插件,填入密码后文章将被加密。 |
password: mysecret123 (不填或留空则公开) |
| copyright | 版权声明开关。 覆盖全局的 CC 协议设置。如果你想某篇文章不显示底部的版权声明,可设为 false。 |
copyright: true (显示) / false (隐藏) |
| comments | 评论区开关。 覆盖全局的 Utterances 评论设置。如果是纯公告或草稿,可关闭评论。 |
comments: true (开启) / false (关闭) |
| permalink | 自定义永久链接。 覆盖全局的 URL 生成规则。如果你想让某篇文章的 URL 特别短或特殊,可以用这个。 |
permalink: python-basics/ (URL变为
.../python-basics/) |
| keywords | 文章关键词。 覆盖全局的 keywords,专门针对这篇文章的 SEO 优化。 |
keywords: [Python, 教程, 新手] |
8.3 注意事项
冒号后面必须有且只有一个空格
列表(数组)的两种正确写法:
写法 A(推荐,紧凑):
tags: [Python, AI, 操作系统](注意逗号后要有空格)写法 B(多行,适合标签多时):
1
2
3
4tags:
- Python
- AI
- 操作系统注意:
-前面必须有两个空格的缩进,-后面必须有一个空格!
特殊字符加引号:
如果你的标题或描述里包含冒号 :、中括号 []、大括号 {} 等特殊符号,必须用单引号或双引号把整句话包起来。
正确示例:
title: 'Python 教程: 从入门到放弃'错误示例:
title: Python 教程: 从入门到放弃, 因为(YAML 会把冒号当成键值对解析,导致报错)
9. 源码备份
事实上, 每次执行hexo d的时候,
只是推送了一部分md文件和生成好的前端文件到GitHub上,
这样做会有一个代码丢失的风险, 假如本地代码被破坏或丢失,
那么将不能恢复到以前的状态,
所以我们需要一个额外的仓库来备份我们的整个Hexo项目:
登录 GitHub,点击右上角 “+” -> “New repository”。
Repository name 填入:
hexo-blog-source(或者你喜欢的名字)。Visibility 务必选择:Private (私有)。
不要勾选 “Add a README file”(保持空仓库)。
点击 Create repository。
在本地的blog根目录下添加或修改
.gitignore文件, 保证文件中拥有以下内容:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# Hexo 生成和部署的临时目录
public/
.deploy_git/
db.json
# Node.js 依赖包(体积巨大,随时可以通过 npm install 恢复)
node_modules/
# 系统生成的垃圾文件
.DS_Store
Thumbs.db
*.log
# 自动更新任务的日志与锁文件
.update_logs/
.auto_update.lock
# Python 缓存
__pycache__/
*.pyc
# drawio 临时备份 / 编辑器临时文件
.$*
*.bkp将源码推送到私有仓库, 在myBlog文件夹中使用以下命令, 注意, 由于我们next主题是用git下载的, 所以要删掉他的.git文件避免冲突:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# 1. 核心操作:删除 Next 主题内部的 .git 目录(解决嵌套仓库问题)
rm -rf themes/next/.git
# 2. 初始化本地 Git 仓库(如果之前已经 init 过,提示已存在则忽略)
git init
# 3. 将所有源码添加到暂存区
git add .
# 4. 提交到本地仓库
git commit -m "Initial commit: Backup hexo source code"
# 5. 关联你的 GitHub 私有仓库(使用你提供的地址)
git remote add origin https://github.com/AskemiWang/hexo-blog-source.git
# 6. 重命名主分支并推送到 GitHub
git branch -M main
git push -u origin main
10. 图片问题
10.1 压缩图片
文章中插入图片就两种方式, 利用图床或者插入本地的图片. 我个人对插入图片很有执念, 因为mermaid太难用了.
然而我个人不放心图床的方式, 有跑路的风险(, 决定用本地的图片.
这样有一个问题, 如果图片数量过多, 仓库的体积就会很大, 后面甚至不能push, 所以我决定采用压缩图片的方法. 流程上:
1 | 用一个log文件记录图片是否被压缩过了 |
Pillow 是 Python 最流行的图像处理库,用于压缩图片。
1 | pip install pillow |
默认把所有文章的图片放在:
1 | source/images/article_images |
我们的文章在:
1 | source/_post |
所以图片的相对路径是:
1 | ../images/article_images |
下面是测试文件, 原大小为653KB:
压缩/路径转换脚本不再内嵌代码, 以仓库
tools/compress_images.py、tools/path_convert.py
及 tools/README.md 为准(避免与仓库版本漂移).
执行完之后的输出:
1 | Scanning 6 images in 'source/'... |
.compressed_log文件中的记录:
1 | source\images\article_images\test.png |
10.2 图片的路径问题
本地编辑器里面插入图片时, 用绝对路径很方便, 比如:
1 | E:\myBlog\source\images\article_images\everything_search_01.png |
但是, 我们的hexo g的时候,
会把source/images的文件夹复制到 public/images,
部署到网站和, 图片的实际地址是:
1 | https://wangyier.top/images/article_images/everything_search_01.png |
用原来的地址是找不到图片的, 所以我们要用相对路径, 由于我们的图片是从
source/images到public/images
我们的网站根目录/是
https://wangyier.top
所以我们的图片用相对路径/images/,
但是这样做会有另一个问题: 我们在本地编辑器中预览的时候,
/代表操作系统的根目录, 我们是无法找到这个图片的.
10.3 终极解决方案
我们的根本问题是路径转换的问题.
功能1:
将source/_post下所有markdown文件中的图片本地绝对路径转化为相对路径,
如:
E:\myBlog\source\images\article_images\转化为:
/images/article_images/
功能2:
逆操作, 将图片转化回来方便我们本地预览;
脚本:
路径转换脚本源码见 tools/path_convert.py(本文不再内嵌,
避免漂移; 功能说明见上方与 tools/README.md).
11. 博客更新流程
再myBlog根目录下:
创建/编辑博客
1
hexo new filename
编辑…
压缩图片并 转换 图片路径
1
python tools/compress_images.py && python tools/path_convert.py online && python tools/highlight_convert.py
备份源码
1
git add . && git commit -m "update" && git push
发布博客
1
hexo clean && hexo g -d
转回本地预览模式 (方便下次在编辑器里看图)
1
python tools/path_convert.py local
12. 更换电脑后如何更新博客
以下内容未经测试, 实际方法可能有偏差:
克隆私有源码仓库:
1
git clone https://github.com/AskemiWang/hexo-blog-source.git myBlog
安装 Node.js 依赖(恢复插件和主题依赖), 由于Next 主题文件是直接存在
themes/next里的,克隆时已经一起下载下来了,所以不需要重新下载主题。1
2cd myBlog
npm install本地验证
1
hexo clean && hexo g && hexo s
Git配置
1
2git config --global user.name "AskemiWang"
git config --global user.email "wye18280075427@gmail.com"恢复日常工作流
1
python tools/compress_images.py && python tools/path_convert.py online && python tools/highlight_convert.py
1
git add . && git commit -m "update" && git push
1
hexo clean && hexo g -d
1
python tools/path_convert.py local
13. 关于mermaid
之前留存了很多mermaid的图片, 后续打算全部用正在的图片替换
14. 一键更新
注意, 没开代理的情况下git push可能会失败, 后面的hexo命令也不会执行.
开启代理后, 配置Git的代理:
1 | git config --global http.proxy http://127.0.0.1:7897 |
更新:
1 | cd /e/myBlog && python tools/compress_images.py && python tools/path_convert.py online && python tools/highlight_convert.py && python tools/auto_update_blog.py --refresh-updated && git add . && git commit -m "update: $(date '+%Y-%m-%d %H:%M')" && git push && hexo clean && hexo g -d && python tools/path_convert.py local |
15. 定时任务自动更新
15.1 功能目的
前面第11节和第14节介绍了更新博客的完整流程: 压缩图片, 转换图片路径, 备份源码到私有仓库, 最后部署到公开仓库. 这套流程虽然一条命令就能串起来(第14节), 但每次更新仍然有几个麻烦:
- 要手动确认图片都压缩过、路径都转换过, 漏掉任何一步都会导致图片在线上无法显示;
git commit的时候要手动总结这次改了什么, 时间久了提交历史里全是update, 根本分不清哪次改了哪篇文章;- 最关键的是: 电脑开着但是忘了跑更新, 或者网络断了(比如Jamjams掉线)没有及时发现, 文章就一直躺在本地, 博客没有更新.
所以我们的目标是: 让电脑每天定时自动检查改动, 有实质内容变化就自动完成整个更新流程, 全程不需要人工干预.
15.2 工作流程
整体流程如下:
1 | 每天 21:00 (电脑开机且用户已登录时触发, 锁屏不影响, 睡眠/关机则跳过) |
下面逐步说明:
15.2.1 改动检查
这一步的目的是防止”无意义”的更新. 如果每天只是本地预览切换了图片路径, 或者只改了几个错别字, 每次都提交部署一遍是没有意义的, 还会污染提交历史.
脚本会把所有改动临时暂存(git add -A), 然后统计改动行数:
只统计文本文件新增+删除的行数; 图片是二进制文件,
没有”行”的概念,
且不计入阈值(避免只新增/替换图片就触发无意义更新),
其他非图片的二进制文件每份折合50行.
合计超过500行才执行更新, 否则撤销暂存, 工作区保持原样,
安静退出. 另外, 只要存在删除类改动(比如删除某篇文章或文件),
无论行数多少都会直接执行更新——内容下线应当及时, 不应被阈值卡住.
阈值和各权重都在脚本顶部的配置区, 可以随时改.
纯机械改动不计入:
只差图片路径形态/换行/==↔︎<mark>/updated/top
的文章会先归一化比对, 相同就整篇跳过(否则 [8/8] 转回 local
造成的300+行路径噪声会天天占掉阈值);
另外图谱产物有变化也会触发本次执行.
15.2.2 网络自检与Jamjams兜底
git push和hexo部署都需要访问GitHub, 网络不通的话整个流程都会失败, 所以执行前先检测两条通道:
- HTTPS通道: 私有备份仓库走HTTPS,
依赖Jamjams的本地HTTP代理(
127.0.0.1:7897); - SSH通道:
公开博客仓库走SSH(
git@github.com), 依赖Jamjams的全局TUN通道.
如果检测到网络不通, 脚本会自动重启Jamjams:
先强制结束进程(Jamjams长时间运行会断连, 只有杀掉进程重连才行),
重新启动后等待代理端口恢复, 再确认系统代理和git代理配置.
随后它会快速探测隧道是否真正连通(端口监听不等于隧道可用).
如果仍不通(常见原因是当前选中的服务器故障, Jamjams 共 6 台可选),
脚本会环形轮换服务器: 依次把 state.json
里的 account.selectedServer 改成下一台并重启,
找出当前可用的那台并保留该选择(不留任何永久黑名单,
服务器恢复后下次仍会参与轮换). 6 台全部失败才报错停止并弹桌面通知,
避免在断网状态下执行半截流程.
注意: Jamjams要在设置里勾选 connect on start, 否则重启后不会自动连接, 代理端口也不会恢复, 只能手动点System Proxy按钮.
15.2.3 自动提交信息
更新流程和手动更新完全一样(压缩图片, 转换路径), 唯一区别是提交信息是自动生成的. 脚本从文章front-matter里提取标题, 用英文动词总结本次改动, 比如:
1 | update: 2026-09-02 21:00 | add 3 posts: ALFWorld、DeepSeek Harness、WebShop | modify 1 post: Panorama of Agent | delete 1 post: Panorama of Agent V2 | images: add 6 | add 1 file: auto_update_blog.py | modify 1 file: .gitignore |
以后翻提交历史, 一眼就能看出每次更新动了哪些文章.
15.2.4 刷新更新时间(第4步)
第7.4节说过, 首页顺序由scripts/index.js决定: 置顶优先,
其余按front matter的updated降序.
但”什么时候算更新过”不能靠文件时间——本地预览模式与部署模式会反复翻转图片路径形态,
每次都会改写文件、刷新文件修改时间; 若按文件时间排,
全部文章每天都会被刷成”刚更新”.
所以自动流程的第4步做的是内容比对:
把工作区里的文章与HEAD(上次提交)里的同一篇文章都做归一化处理——折叠图片路径形态(绝对路径
<->
相对路径)、折叠换行差异(仓库开了core.autocrlf=true:
git里存LF而工作区是CRLF,
不折叠的话每个被改动过的文件都会被误判成”内容变了”)、折叠==高亮==与<mark>的差异、并忽略updated与top两行(调置顶开关属于排版意图,
不该把文章顶到”最近更新”的前面)——然后逐字节比较.
归一化后仍然不同才算”实质内容变化”,
这时才把该文章的updated刷成当前时间;
只有机械改动(路径翻转、高亮转换)的文章则保持原值.
这样既保证”最近真正改过的文章排在最前”,
又不会因为每天的路径翻转让首页天天翻新.
updated只往更晚的时间写(单调, 不回退);
新建文章若没有updated,
会自动插到date行后面(沿用该文件自己的换行符,
不破坏CRLF).
15.2.5 通知: 发进 Windows 通知中心
最初脚本用的是System.Windows.Forms.NotifyIcon.ShowBalloonTip——经典的右下角气泡.
它有三个毛病: 只在屏幕上闪一下,
不进通知中心(关了弹窗就再也找不回来); 顶部没有应用名,
看起来像个不明来源的弹窗; 而且为了让气泡显示完, 还得让一个 powershell
进程在后台白挂14秒.
现在改成 Windows 的 Toast 通知, 也就是 Windows Defender 那种提示所用的同一套机制:
- 通知交给系统通知中心托管: 点
✕只是关掉弹窗, 之后仍能在通知中心(Win + N)回看; - 遵守系统的通知设置与专注助手, 不会被当成”野弹窗”;
- 有应用身份: 顶部显示应用名博客自动更新和博客图标;
- 点击通知直接打开博客(
activationType="protocol"+launch="https://wangyier.top/"), 并带默认提示音; - 发完即退出, 不再有常驻进程.
实现上有两个要点:
- 通知里显示的”应用名”其实就是
AUMID(AppUserModelID)本身.
如果用
AskemiWang.BlogAutoUpdate这种反查名, 通知顶部就会原样显示这串 ID, 很难看; 所以干脆把 AUMID 取成中文应用名博客自动更新. 桌面程序发 Toast 一般要求把 AUMID 注册到开始菜单快捷方式里, 但实测 Windows 11 (26100) 直接调用即可; 为了让它在”设置 > 系统 > 通知”里出现并能单独开关, 脚本每次发送前会幂等地登记一次注册表:HKCU\Software\Classes\AppUserModelId\博客自动更新->DisplayName/IconUri(博客 favicon) /ShowInSettings=1. - 发送是 Python 调 PowerShell 调 WinRT:
用
[Windows.UI.Notifications.ToastNotificationManager]+Windows.Data.Xml.Dom.XmlDocument组装并投递. 标题与正文先由 Python 做 XML 转义, 再经环境变量传给 PowerShell(不往脚本源码里拼字符串, 提交信息里带引号、反斜杠也安全). 发送后会回读通知中心历史确认投递成功; 若 WinRT 不可用(老系统)或投递失败, 自动回退到原来的气泡提示, 不影响主流程.
想静音:
删掉_send_toast()里的<audio .../>一行;
想临时关掉通知: 在”设置 > 系统 > 通知 >
博客自动更新”里关闭即可(不用改脚本).
改完通知逻辑后可以用--test-notify单独验证.
15.2.6 图谱: 每天构建并发布 /graph/
知识图谱项目(kg/)已经并进博客仓库, 不再是独立仓库.
每次运行会多做两件事:
- 在”检查改动规模”之前跑一次
python tools/update_graph.py -q: 它封装了两步——kg/tools/update.py(编译→体检→出图, 读kg/vault/和source/_posts里带kg:块的文章)和kg/tests/selftest.py(37 条回归测试); - 把可复现的产物
kg/graph/graph.html与source/graph/.graph-hash里记录的 SHA1 比对, 不同才复制到source/graph/index.html.
于是source/graph/index.html会随hexo g -d一起发布到https://wangyier.top/graph/,
导航栏也有Graph入口.
几个设计要点:
- 只有 Resource 在博客里:
博客文章只允许
type: Resource; Concept/Claim 这类知识节点仍只放在kg/vault/, 博客里没有它们的文件. - 改概念的「一句话定义」或
relations才会发布: 图谱只读节点的summary(一句话定义)和边, 不读概念正文; 只改正文不会改变graph.html. 图谱产物变化本身算触发条件(不受500行限制), 否则改十几行概念笔记会一直卡在阈值下、图谱永远不更新. - 失败不阻断博客:
图谱构建失败只记
[WARN]并保留上一版页面; 但kg/目录缺失会被流水线拒绝执行(避免把整个图谱当成删除类改动提交), 想停用请注释kg/ontology.yaml的sources.posts, 不要删目录. - 产物可复现:
graphify.py导出前对边做了排序, 同一张图两次构建字节一致; 这样”没变”时页面不变、不产生 diff 噪声(也让上面的哈希判断成立). - 页面必须原样复制:
_config.yml里的skip_render: graph/**让 Hexo 直接复制source/graph/index.html; 少了它, 整份查看器会被套进主题的 page 布局(实测显示区域只剩 475×150). .graph-hash是”上次写入页面的 SHA1”: 若上次运行在提交/部署前失败, 页面会停在”已改未提交”, 下次凭git status的脏状态继续执行, 不会丢信号.
15.3 新引入的脚本与工具
| 工具 | 说明 |
|---|---|
tools/auto_update_blog.py |
tools目录下的总控脚本,
实现上面完整流程(共8步[1/8]~[8/8]). 支持四个调试参数:
--dry-run(只检查不改动),
--test-notify(测试系统通知),
--test-repair(测试Jamjams重启修复),
--selfcheck(离线自检: 路径/工具/高亮护栏单测/front
matter不可见字符与top值/首页顺序预览/触发预览) |
scripts/index.js |
博客自带的首页生成器(必须放在博客根目录的scripts/下,
见7.4节): 置顶优先 -> 其余按updated降序 -> 分页.
它取代了第三方插件hexo-generator-index-pin-top(已从package.json移除) |
tools/update_graph.py |
图谱封装脚本: 先跑 kg/tools/update.py(编译/体检/出图),
再跑 kg/tests/selftest.py(37 条回归测试);
自动流程通过它更新图谱 |
kg/ |
知识图谱项目(已并入本仓库, 见15.2.6):
kg/tools/update.py一条命令跑完编译/体检/出图,
kg/vault/放 Concept/Claim 等知识节点, 博客文章通过
kg/ontology.yaml 的 sources.posts
就地读取 |
tools/jamjams_server.py |
【手动工具】查看/切换 Jamjams 服务器: --list 列出 6
台与当前选择, --use S3 切换(停应用→备份→改
state.json→启动). 网络故障时手动救急用 |
| Windows任务计划程序 | 注册了一个名为BlogAutoUpdate的定时任务,
每天21:00用pythonw.exe运行E:\myBlog\tools\auto_update_blog.py(无窗口),
仅当电脑开机且用户已登录时触发 |
.update_logs/ |
每次运行的日志目录(已加入.gitignore): update_*.log
逐次明细、summary.log
单文件一行索引(时间/结果/行数)、highlight.log
高亮转换审计、BlogAutoUpdate.xml.bak 定时任务配置留档 |
.auto_update.lock |
锁文件, 防止上一次任务没跑完时重复触发(已加入.gitignore) |
其中auto_update_blog.py复用了同目录下的compress_images.py(第10.1节)、path_convert.py(第10.3节)和highlight_convert.py(把
Typora 的 ==高亮== 转成 <mark> 再部署,
因为博客的 pandoc 渲染默认不解析 ==;
该脚本自带反引号守恒与幂等护栏, 护栏不过会失败通知而不会带伤提交),
只是把第14节的一整条命令链变成了自动判断+自动执行.
高亮脚本的代码围栏判定按 CommonMark 规则:
开围栏是”行首最多3个空格 + 3个以上反引号/波浪号(可带语言名)“,
闭围栏必须同字符、不短于开围栏且不能带语言名 —— 所以
```sh
这种带语言名的行只能开启围栏、不能闭合它(早期实现没区分这一点,
配对错位后整段正文被当成代码块跳过转换, 实测漏转96处高亮);
另外若某篇文章有未闭合的围栏,
脚本与--selfcheck都会告警 ——
因为那之后的正文会被当代码块渲染, 需要手动补上闭合的
```.
Python脚本统一放在
tools/, 不要放回scripts/: Hexo会把scripts/下的所有文件(不分扩展名)当作插件脚本require一遍, 放那里会每次构建都刷出Script load failed报错. 而script_dir在Hexo源码里被写死为<博客根>/scripts, 无法通过配置修改.
15.4 使用方法
手动测试或者排查问题时, 在博客根目录下执行:
1 | # 预览: 只做本地检查并生成提交信息, 不做任何修改 |
任务运行结果在.update_logs/目录下,
成功/失败都会发系统通知(通知中心可见).
15.4.1 暂停 / 恢复自动更新
需要临时停止自动发布时(改流水线、改动量太大、或不想让半成品被提交), 直接禁用整个计划任务:
1 | Disable-ScheduledTask -TaskName BlogAutoUpdate # 暂停(对所有日期生效) |
确认状态:
1 | Get-ScheduledTask -TaskName BlogAutoUpdate | Select-Object TaskName, State |
- 暂停后:
State=Disabled、NextRunTime=N/A, 每天都不会再跑(不是只跳过一次); 恢复后:State=Ready、下次运行时间重新出现(通常是次日 21:00); - 想确认某天确实没跑:
看
.update_logs/里有没有对应的update_YYYYMMDD_2100*.log; - 只想跳过某一天: 当天 21:00 前禁用、次日恢复即可;
- 不会补跑:
触发器的
StartWhenAvailable=False, 睡眠/关机/禁用期间错过的运行不会在恢复后补上; - 不要手动造
.auto_update.lock来代替暂停: 锁超过 6 小时会被当作残留自动接管, 而且它的语义是”另一个实例在跑”, 不是”今天不跑”.
暂停不影响工作区: 改动留在本地, 恢复后由下一次运行按阈值处理; 急着发布就走 15.4 里的手动链路.
15.5 版本索引
自动化链路按版本迭代, 每个版本在私有源码仓库里打一个同名 git
tag。各版本做了什么、为什么改, 统一记在下一章《16. 版本记录》里;
仓库侧的同款记录在tools/CHANGELOG.md。
16. 版本记录
本章先说明发版与标签约定(16.1),
再按版本正序(从早到晚)记录博客(尤其是自动化链路)的每一次迭代:
每个小节对应私有源码仓库里的一个 git tag,
并说明这个版本主题是什么、加了什么、改了什么、修了什么。仓库侧的同款记录在tools/CHANGELOG.md。查看或切换版本:
1 | git tag -l -n1 # 列出所有版本标签与说明 |
16.1 版本管理约定
- 每个版本在私有源码仓库打一个附注标签,
与
tools/CHANGELOG.md、本文本章的条目一一对应:
1 | git tag -a 2.0.1 -m "v2.0.1: <一句话主题>" |
- 标签指向”该版本功能全部完成”的那次提交; 若同一版本内还有后续小修,
在原标签上追加提交即可(需要改指向时:
git tag -d <tag> && git push origin :refs/tags/<tag>后重建)。 - 标签只在博主明确要求时改动: 即使之后又提交了若干次(修 bug、补文档), 也不要因为”有更新提交了”就自动把标签前移 —— 打标签、改指向、推送标签都必须以博主的指示为准。
- 版本号语义: 主版本号变化 = 排序/流程等行为发生变化(如2.0.0改了首页排序); 次版本号 = 新增能力(如新增某条自检); 修订号 = 只修缺陷或文档。
16.2 1.0.0 — 2026-06-29 · 博客本体完成
版本主题: 搭起一个能长期写下去的博客底座。
内容
- 站点: Hexo + NExT主题(Muse),
本地搜索、代码高亮、
hexo-blog-encrypt文章加密、hexo-filter-mermaid-diagrams等插件配好(见第6、7章)。 - 写作链路:
hexo new出稿 → 本地预览 → 生成部署, 文章front matter约定(title/date/updated/top/permalink/password/description等)在此时定型(见第8章)。 - 双仓库:
私有源码仓库
hexo-blog-source做备份、公开仓库AskemiWang.github.io做部署(见第9章)。 - 域名:
自定义域名
wangyier.top绑定与CNAME配置(见第5章)。 - 渲染: 用pandoc渲染LaTeX数学公式(见第3章)。
对应标签:
1.0.0指向提交c6855334。
16.3 2.0.0 — 2026-09-13 · 首页按”更新时间”排序
版本主题:
首页从”按发布时间(date)排序”改为”按更新时间(updated)排序,
最新的排最前”, 并围绕它补齐了一整条”更新时间自动维护 +
自检可验证”的链路。
新增
- 首页排序规则改为「置顶优先 → 其余按
updated降序」。新增博客自带的生成器scripts/index.js(放在博客根目录的scripts/下, Hexo会自动加载); 排序相同则按date降序兜底, 每页index_generator.per_page篇, 第2页起为/page/N/。 - 流水线新增第4步「刷新 front matter
updated」: 只有”实质内容变化”的文章才把updated刷成当前时间。判定办法是把工作区内容与HEAD内容都归一化后比对 —— 折叠换行差异(仓库开了core.autocrlf: git里存LF、工作区CRLF)、折叠图片路径形态(local/online)、折叠==高亮==与<mark>、忽略updated与top两行。updated单调不回退; 文章缺updated时插到date行之后, 并沿用该文件自身的换行符。 - 新增
--refresh-updated开关(可加--dry-run只预览): 供手动发布链路使用, 避免”手动发布时updated没刷新、首页顺序不更新”。 --selfcheck从5节扩充到6节: 现在会报告不可见字符(UnicodeCf/Cc, 容忍开头BOM与标题里的ZWJ)、非整数top、updated缺失或早于date, 并预览按当前规则算出的首页前5篇与总页数、预览第4步会刷新哪些文章; 另含3条归一化回归单测与scripts/目录纯净度检查。
变更(重构)
- Python流水线从
scripts/迁到tools/。原因: Hexo的script_dir在源码里写死为<博客根>/scripts且会把该目录下所有文件(不分扩展名)当插件require, 以前每次构建都会刷6行Script load failed报错、还会淹没真实错误; 现在scripts/专供Hexo,tools/放流水线, 构建日志干净。 - 定时任务
BlogAutoUpdate改指"E:\myBlog\tools\auto_update_blog.py"(21:00触发、单实例、2小时上限、不唤醒睡眠均保留); 改动前的任务配置留档在.update_logs/BlogAutoUpdate.xml.bak。 - 移除第三方插件
hexo-generator-index-pin-top: 它只能按date排序, 且用JS真值判断top, 对含不可见字符的top会误判。
修复
top里混入U+200E导致置顶误判:Model Post-Training.md与Paper Summary – SWE-smith.md的top: 0后面各藏了一个从左往右标记(U+200E), 旧插件把它当成”有top”→两篇文章被当成置顶, 长期占据首页第1、2位(真正的置顶top: 100/10反被挤到后面)。现在生成器用Number(top)判断, 非纯数字一律按”不置顶”处理并在构建日志warn,--selfcheck也会直接拦下。- 换行未折叠导致第4步误判:
core.autocrlf=true下git show HEAD:<path>返回LF、工作区是CRLF, 归一化漏折叠换行 → 17篇只翻了路径形态的文章被误刷updated。已折叠换行、加回归单测, 并还原其中14篇(保留3篇当天真实编辑过的)。 - 翻页第2页的”上一页”指向不存在的
/page/1/: 改为与hexo-pagination规范一致(第1页就是站点根/), 否则404。 - 手动链路无法刷新
updated的缺口(见上--refresh-updated)。 - 定时任务在
pythonw下崩溃('NoneType' object has no attribute 'encoding', 2026-09-17 21:00 那次):pythonw.exe无控制台时sys.stdout/sys.stderr为None, 而[4/8]把模块import进主进程调用, 模块顶部的编码守卫读到None.encoding即抛异常; 同时给主流程补上失败收尾(撤销暂存 + 图片路径转回local), 详见§17.6。
文档
tools/README.md: 脚本一览、8步流程、触发规则、网络兜底、首页排序与updated、配置点、用法、运行产物、定时任务接线、维护提示(含换行陷阱与top排除规则)全部更新; 新增第8节指向tools/CHANGELOG.md。- 本文(第7.4、15.2.4、15.3、15.4、16章)同步改写;
正文中的脚本路径统一为
tools/。 - 第17章新增§17.6《定时任务”只在计划任务里”崩溃:
无控制台时
sys.stdout是None》, 记录pythonw崩溃的现象、排查与预防。
对应提交:
3f2496d(生成器+第4步+目录迁移) →
6b8296c(换行修复) →
f8849b1/d9816ba/b28214a(文档、自检、忽略top、手动开关)
→ c6f136f(围栏规则 + 找回96处高亮) →
9939fc0(公式与围栏内容修复) →
db461c8(第17章《问题处理》) →
8984082(批量渲染修复 + 渲染风险体检) →
7a30a97(标题改写) → fcffec4(pythonw崩溃修复 +
pythonw兼容自检 + 失败收尾 + 渲染体检误报修正) →
中间若干次定时更新的内容提交(9/18的0847c24、9/20的841db69等,
不含版本改动) → 本次文档同步提交。按博主2026-09-21的指示,
标签2.0.0已前移到这一次提交(具体提交号用git rev-list -n1 2.0.0查看)。
1.0.0 到 2.0.0 之间陆续加入的自动化能力(图片压缩、图片路径local/online切换、
==高亮==→<mark>转换、每天21:00定时更新、网络自检与Jamjams服务器轮换、自动提交信息、系统通知)在本文件第10、11、14、15章里描述, 但没有单独打标签, 统一收进2.0.0。
16.4 2.1.0 — 2026-10-22 · 通知中心 + 渲染体检
版本主题: 把”右下角气泡”换成 Windows 通知中心的
Toast, 渲染体检补上第⑤条; 同时把 26 篇博客文章的 kg:
块补齐(为后面”图谱直接读博客”做准备)。
新增
- 通知改为 WinRT Toast(与 Windows Defender
提示同一机制): 通知由系统通知中心托管,
点
✕只是关掉弹窗、之后仍能回看; 遵守专注助手与通知设置; 顶部显示应用名与图标; 点击通知直接打开博客(activationType="protocol"), 带默认提示音; 发完即退出, 不再有常驻进程。应用身份用中文 AUMID博客自动更新(通知头显示的就是 AUMID), 并幂等登记到HKCU\Software\Classes\AppUserModelId, 使其出现在”设置 > 系统 > 通知”里。WinRT 不可用或投递失败时自动回退到旧气泡。 --selfcheck渲染体检新增第⑤条「列表紧贴段落」: 段落(含加粗小标题)与列表之间缺空行时, pandoc 会把整张表并进一句话。检测时跳过 front matter、代码围栏与$$块, 只认顶格行。
修复
- 全库 8 处”列表紧贴段落”补齐空行(符号表三节 +
SWE-agent Traj Analysis6 处 +Mechanism of Agent1 处), 线上三节恢复为独立<ul>。
文档
- 第15章新增 §15.2.5《通知: 发进 Windows 通知中心》与 §15.4.1《暂停 / 恢复自动更新》; §17.4 表格新增”列表紧贴段落”一行, §17.5 的体检清单同步为 ①–⑤。
tools/README.md同步: §5 通知小节、§6 暂停/恢复小节、§7 渲染体检扩为五条。
对应提交: c9ac555(通知中心 Toast) →
63e0140(列表并段修复) → 948c40d(渲染体检⑤) →
29a5dab…b125179(26 篇博客补 kg:
块) → 81ac0b3(Agent S 的边同步回博客) →
2538055(暂停/恢复手册) → 本次文档同步提交;
标签2.1.0指向最后一次提交(具体提交号用git rev-list -n1 2.1.0查看)。
16.5 3.0.0 — 2026-10-27 · 图谱接入 + Butterfly + 视觉系统
版本主题: 把知识图谱正式并入博客仓库, 主站从 NExT 迁移到 Butterfly 5.7.0, 并完成纸/夜双主题与 TOC/Tag 的阅读优化。
kg/用git subtree并入(保留图谱侧51个提交), 位于仓库根、不在source/下, Hexo 不发布它;Resource改为就地读取:kg/ontology.yaml的sources.posts指向../source/_posts, 26个vault副本删除; 概念/主张等知识节点仍只在kg/vault/;- 流水线在”检查改动规模”之前构建图谱, 产物变化才写
source/graph/index.html(统一LF, 与部署产物字节一致), 并随hexo g -d发布; 图谱变化本身也是触发条件; - 导航栏新增
Graph入口; 页面靠_config.yml的skip_render: graph/**原样复制; - 阈值统计排除”纯机械改动”(路径/换行/高亮/updated/top), 否则
[8/8]转回 local 的300+行噪声会吃掉阈值; - 主题迁移: NExT 8.27 → Butterfly 5.7.0;
themes/butterfly入库、NExT 保留回滚, 新增tools/theme_switch.py; Mermaid/MathJax/搜索/加密 TOC 适配; - 视觉系统: 亮色纸面 + 墨蓝、暗色夜色 + 暖灰;
hero/post-bg
SVG、
index_layout: 4、16px/Georgia、固定导航、页脚三栏、相关文章 3、代码块 500px + 换行、prev/next 卡片统一; - 阅读交互: TOC 整卡折叠(localStorage 记忆); Tag 云按频次前25%放大加粗; 灯箱/懒加载/分享裁剪;
- 验收:
hexo clean && hexo g488 files、0 错误; 独立子代理完整验收 + 冒烟回归; Mermaid 9 SVG、MathJax 93 公式、8 篇加密无 TOC/明文/评论、search 无//; - 对应标签
3.0.0(git rev-list -n1 3.0.0可查); 仓库侧同款记录在tools/CHANGELOG.md的 3.0.0 段落。
详细迁移清单与踩坑见第 18 章。
17. 问题处理
这里记录搭博客与写自动化链路时踩过的坑:
每个小节按”现象 → 排查 → 原因 → 解决 → 预防”来写,
方便日后遇到同类问题时按图索骥。脚本侧的历次修复同时记在tools/CHANGELOG.md与第16章。
17.1 高亮标记从某一节起不再被替换
现象:
Notes on Machine Learning & Deep Learning & Reinforce Learning.md
从 §3.2.5 开始, 正文里的 ==高亮== 全部原样显示(线上看到的是
==Reward== 而不是高亮), 而文件前半部分一切正常; 更迷惑的是
highlight_convert.py
的两道护栏(反引号守恒、幂等断言)和--selfcheck都不报错,
只报告”转换了 0 个文件”。
排查:
- 先怀疑文章自身(比如代码围栏没闭合) ——
用渲染结果交叉验证: 该页
3.2.5仍是正常标题、整页有 52 个<pre>块, 说明 pandoc 的围栏配对是正确的, 文件没问题; - 把脚本的围栏判定逻辑单独抽出来逐行打印状态, 发现读到文件末尾时仍停在”围栏内”;
- 列出所有围栏行, 注意到文中有
```sh、```py这种带语言名的行出现在本该是”闭合”的位置。
原因: 旧实现是
FENCE_RE = ^\s*({3,}|~{3,}), 只要行首是三个及以上反引号就翻转一次状态。但按 CommonMark, **闭围栏必须与开围栏同字符、长度不短于开围栏、且后面不能跟语言名** —— 所以 ```` ```sh ```` 这种行只能"开启"围栏。被误当成闭合之后, 配对整体错位, 最后"文件读完仍停在围栏内", 于是它后面的**全部正文都被当成代码块跳过**, 高亮自然不会被转换。同一原因还影响另外 3 篇文章(Paper
Summary – SWE-RL.md6 处、Paper Summary –
SWE-smith.md4 处、Panorama of Agent.md` 3 处), 合计
96 处漏转。
解决:
把围栏判定改成状态机(FenceTracker), 实现上面三条规则, 并让
process_text / count_remaining /
_collect_matches 三处共用它;
顺带支持引用块里的围栏(> ```sh)。改完重跑, 96
处全部补齐, 幂等复查为 0。
预防:
--selfcheck第 4 节新增回归单测(“带 info string 的围栏行不闭合围栏”、“未闭合围栏可被识别”);- 新增未闭合围栏体检: 如果某篇文章真的有围栏没闭合,
脚本会
[WARN]列出文件名 —— 那种情况下它后面的正文会被整段渲染成代码块, 需要补上闭合的```; - 定位这类问题的通用手法:
先看渲染产物(
public/*.html)反推, 比在源码里盯着围栏更快。
17.2 公式显示异常: 字面美元符号与未定义命令
现象: 页面里有多处公式原样显示成
$...$(没有被渲染成数学); 另有一处 \infin
显示成红字 Undefined control sequence \infin。
原因与解决(四类):
| 类型 | 例子 | 处理 |
|---|---|---|
$ 与内容之间有空格 |
...= \infty $ |
pandoc 规定开 $ 右侧、闭 $
左侧不能是空白, 删掉空格即可 |
$...$ 跨行 |
开 $ 在一行、闭 $ 在下一行 |
合并成一行, 或改成 $$...$$
显示公式(长公式推荐后者) |
| 正文里的美元金额 | revenue was $2.5M ... was $2.3M |
两个 $ 会被 pandoc 配成一段”公式”, 必须转义写
\$2.5M |
| 非标准 TeX 命令 | \infin |
MathJax 只认标准命令; 无限大是 \infty(注意
\inf 是”下确界”算符, 是另一个东西) |
预防: 记住上表四条。定位手法: 把渲染产物中
class="math" 的片段剥掉, 正文里剩下的 $...$
就是没渲染成功的公式。
17.3 首页置顶与”更新时间”的两个坑
现象 A: 有两篇文章长期固定在首页第 1、2 位,
把真正的置顶(top: 100 / top: 10)挤到后面。
原因: 它们的 top: 0
后面各混入了一个不可见的”从左往右标记”(U+200E), YAML
把它解析成字符串"0\u200e";
而旧的置顶插件用 JS 真值判断”有没有 top“, 字符串恒为真 →
这两篇被当成了置顶文章。
解决: 删掉不可见字符。--selfcheck
现在会扫描全部文章的 front matter, 直接拦下这类不可见字符与”非整数
top“。
现象 B: 首页改为按 updated
降序后第一次上线, 17 篇”只翻过图片路径形态”的文章被刷成”今天更新”,
首页顺序失真。
原因: 仓库开了 core.autocrlf=true ——
git 里存 LF、工作区是 CRLF; 拿工作区文本与
git show HEAD:<path>
比对时没有折叠换行,
于是每个改动过的文件都被判成”内容变了”。
解决:
比对前折叠换行(连同图片路径形态、==与<mark>的差异一起折叠),
并忽略 updated 与 top 两行; 补回归单测,
把被误刷的 14 篇还原。
预防: 任何”工作区 vs
提交历史”的文本比对都必须先归一化; --selfcheck 第 5
节会预览”哪些文章会被刷新”, 上线前看一眼就能避免意外。
17.4 一批渲染问题与修法(体检后集中修复)
用脚本把线上全部文章抓下来逐页体检(未渲染的公式/高亮、Markdown 外泄、代码围栏、图片与内部链接、以及用同版本 MathJax 做命令级校验)后, 集中修了下面几类:
| 现象 | 根因 | 修法 |
|---|---|---|
正文里的 Windows 路径被吃掉:
主题文件夹(C:)、**D:_global_modules** |
博客给 pandoc 开了 raw_tex,
正文里的”反斜杠+字母”(\softwares、\nodejs)被当成原始
TeX 命令丢弃(行内代码或代码块里则安全) |
路径一律写成行内代码:
`D:\softwares\nodejs\node_global`; 单独的反斜杠用
\\ 转义 |
$$ 里的 \\ 在本地 Typora
里换行、网站上却挤成一行 |
\\ 只在「对齐环境」里才会换行: Typora
用 KaTeX(裸 \\ 也换行), 本站用 MathJax(裸 \\
被忽略) |
多行公式包进 \begin{aligned}...\end{aligned}(或
gathered), 行尾用 \\ |
$$...$$ 显示公式被拆成几段、_i
变成斜体、TeX 命令丢失 |
$$ 块内不能有空行, 空行会提前结束公式,
其后内容按 Markdown 解析 |
删掉块内空行(长公式改用 \begin{aligned},
同样不留空行) |
正文出现字面 ```, 列表里的代码块结构错乱 |
围栏写在列表标记后面(- ```sh)不算开围栏;
内层字面围栏的缩进又落在”允许闭合”的范围内, 会把外层块提前闭合 |
围栏独立成行并与列表内容对齐; 外层围栏用 4
个反引号, 内层字面 ``` 就永远无法闭合它 |
表格/正文里出现多余星号:
****粗体****、***\*粗斜体\**** |
四个以上星号、或”星号+转义星号”会被解析成”粗斜体里再带一个字面星号” | 统一写成 **粗体** / ***粗斜体*** |
表格单元格里的 \n 丢失(OBSERVATION:\nFile
→ OBSERVATION:File) |
同 raw_tex: \n 被当命令丢弃 |
写 \\n(显示为 \n), 或套行内代码 |
文章开头多出一行 [TOC] |
pandoc 不认 [TOC] 标记(那是别的渲染器语法),
会原样输出 |
不要写 [TOC]; 目录由主题侧边栏提供 |
符号表/步骤列表整段变成一句话, 条目之间只剩 - 分隔 |
段落(含加粗小标题)与列表之间缺空行: pandoc 不能用列表打断段落, 会把整个列表并进同一段 | 在段落与列表之间补一个空行;
--selfcheck 的渲染体检第⑤条会扫出来 |
17.5 加密文章的两个坑(hexo-blog-encrypt)
现象 A: 加密文章输入密码解锁后, 整篇正文变成居中(普通文章正常)。
原因: source/_data/styles.styl
里为了美化密码框写了
#hexo-blog-encrypt { text-align: center; }; 而
hexo-blog-encrypt 解密成功后会复用同一个
id新建容器(class="hbe hbe-decrypted-content")替换原容器
—— 于是解密后的正文也继承了居中。
解决:
把居中规则限定在”未解锁”状态(#hexo-blog-encrypt:not(.hbe-decrypted-content)),
再给解密后的容器显式 text-align: left。
现象 B: 加密文章解锁后, 末尾的图片不显示(空白), 但图片 URL 本身是 200。
原因: NexT 用 lozad 做图片懒加载(构建时把
<img src> 改写成 data-src), 而
next-boot.js 只在页面启动时执行一次
window.lozad('.post-body img').observe();
解密是”点击后才把正文插入页面”, 新插入的图片永远不会被 observe,
也就不会补上真正的 src。
解决: 在 source/_data/post-body-end.njk
里加一段脚本(仅当页面存在未解锁容器时才生效): 用
MutationObserver 监听 .post-body,
在解密容器被替换后重新执行一次
lozad('.post-body img').observe(); 另加一个最多约 60
秒的兜底轮询。
体检顺手加进了--selfcheck(只告警、不阻断流程):
①$$ 块内含空行; ②$$ 块里用了
\\ 换行却没有对齐环境; ③正文里的反斜杠命令;
④提交历史里的图片引用是否为相对路径;
⑤段落与列表之间是否缺空行(前三条自动跳过代码块、行内代码、图片链接与公式;
第⑤条还跳过 front matter、代码围栏与 $$ 块, 且只认顶格行,
列表项的缩进续行不会误报)。
17.6
定时任务”只在计划任务里”崩溃: 无控制台时 sys.stdout 是
None
现象: 2026-09-17 21:00 的定时任务失败,
summary.log 记为 FAIL,
当天日志的最后三行是:
1 | [21:00:12] [2/8] 转换图片路径为相对路径 (online) ... |
迷惑之处有两点: ①同一个脚本在终端里手动跑一切正常, 从没报过这个错; ②此前几天(9/14–9/16)的定时任务都”正常”(没报错), 看上去是”今天突然坏了”。
排查:
- 先看那几天到底跑到哪一步。翻日志发现 9/14–9/16
三次定时任务都是
SKIP(改动 216 / 216 / 206 行, 未超过 500 行阈值), 也就是在第 1 步就结束了, 根本没走到[4/8]; 而 9/13 那几次成功的运行是手动在终端里跑的。所以真正的差别不是日期, 而是”9/17 是第一次在计划任务环境下真正执行到[4/8]“(当天折合 548 行, 越过了阈值)。 - 再看执行者是谁。计划任务用的是
pythonw.exe(无控制台窗口), 手动跑用的是python.exe。 - 复现: 直接用
pythonw E:\myBlog\tools\auto_update_blog.py跑, 稳定重现同一个报错。
原因: pythonw.exe 没有控制台, Python
启动时会把 sys.stdout 与 sys.stderr 置为
None。而流水线里各步骤的实现方式并不一样:
[1/8]~[3/8]是另起子进程执行独立脚本(压缩图片、路径转换、高亮转换), 子进程自己拿到的句柄是好的, 所以不受影响;[4/8]是把tools/path_convert.py等模块import进主进程直接调用函数。这些模块顶部都有一段”把标准输出改成 UTF-8”的编码守卫, 写成sys.stdout.encoding.lower() != "utf-8", 当sys.stdout is None时就在导入阶段抛AttributeError。
于是这条代码路径只有在 pythonw 下才第一次被执行,
也就只在定时任务里炸。另外 [4/8] 本身是 2026-09-13 22:07
才加进流水线的, 加完又赶上三天 SKIP, 直到 9/17
才第一次被真正走到 —— 这几件事叠在一起, 才显得”莫名其妙今天坏的”。
解决:
- 把几个模块顶部的编码守卫改成 None
安全(
path_convert.py、highlight_convert.py、compress_images.py、jamjams_server.py四处):
1 | for _s in (sys.stdout, sys.stderr): |
--selfcheck新增 pythonw 兼容性检查: 把sys.stdout临时置为None再逐个导入会被主进程import的模块, 任何一个在导入阶段依赖标准输出就直接报错。以后往流水线里加”被 import 的模块”, 自检就能提前拦住;- 主流程新增失败收尾(
recover_after_failure): 不管崩在哪一步, 都先撤销暂存、再把图片路径转回 local 形态。
预防:
- 不能只在终端里用
python.exe测。凡是会被定时任务以pythonw执行的脚本, 都必须用pythonw真跑一次(哪怕只是pythonw tools/auto_update_blog.py --selfcheck)。 print到None同样会炸。无控制台环境里, 任何”直接写sys.stdout“的代码都要先判空; 日志一律写文件句柄, 不写标准输出。- 阈值触发会把”很久才走一次的代码路径”藏起来。那三天因为改动不足
500 行而
SKIP, 恰好把新加的[4/8]一直挡在门外; 等 9/17 改动够了才暴露。所以改了流水线的任何一步之后, 都要立刻用pythonw手动走一遍全流程(或至少--selfcheck), 不能等下一次”够 500 行”的时候顺便验。 - 失败会留下半成品工作区。崩在
[4/8]时,[1/8]~[3/8]已经动过工作区(图片压缩了、18 个文件的图片路径转成了 online、==高亮==转成了<mark>), 但[5/8]的提交与[7/8]的部署都没执行 —— 结果是”站点没更新, 仓库却已经不干净”。更麻烦的是第 1 步的统计基线会跟着变(本地/线上两种路径形态带来的那部分行数一下子消失), 阈值更容易达不到, 于是可能出现”明明改了东西却连着几天SKIP“。失败收尾就是为这个准备的: 收尾后工作区回到运行前的形态, 下一次统计到的才是你真正改了多少。真遇到”待发布内容一直在、却连续SKIP“时, 用一次手动发布(见第 11、15 章)打破即可。
18. 从 NExT 换到 Butterfly(2026-10)
第 7 章记录的是 NExT 的安装与配置;2026-10-26 本站迁移到 Butterfly 5.7.0。这一节是完整的迁移清单、配置对照与踩坑记录。迁移后
themes/next仍然保留,只用于回滚;确认没问题后再删。
18.1 先备份
主题迁移会动
_config.yml、themes/、source/_data/,先做一份可回滚的备份(两个文件就够):
1 | git bundle create D:\Backups\myBlog.bundle --all # 全部提交历史 |
bundle是完整历史;tree是当前工作区(含未提交改动)。恢复时git clone myBlog.bundle再robocopy tree覆盖即可。
18.2 安装 Butterfly
1 | cd themes |
三个容易错的地方:
- clone 的当前目录:必须在
themes\下执行;在根目录执行会得到E:\myBlog\butterfly(不是themes\butterfly); - 删掉
butterfly\.git:不删的话主仓库会把它当成嵌套仓库(这个坑在这个仓库历史上已经踩过一次); - 必须装
hexo-renderer-pug:Butterfly 5.x 用 Pug 模板,缺了会构建失败。
18.3 配置放在哪
| 文件 | 作用 |
|---|---|
themes/butterfly/_config.yml |
主题自带默认配置,不要直接改(升级会被覆盖) |
_config.butterfly.yml(站点根目录) |
站点级覆盖,推荐所有自定义都写这里 |
_config.yml |
theme: butterfly |
1 | Copy-Item themes\butterfly\_config.yml _config.butterfly.yml |
18.4 配置对照(NExT → Butterfly)
| 功能 | NExT | Butterfly |
|---|---|---|
| 布局方案 | scheme: Muse |
无此概念(只有一套布局) |
| 菜单 | menu:(键名小写) |
menu:(键名即显示文字,图标用 fas) |
| 头像 | avatar.url |
avatar.img |
| favicon | favicon.small/medium/apple_touch_icon/... |
单个 favicon + inject.head 补多尺寸 |
| 社交 | social: |
social:(格式:图标: url \|\| 文字 \|\| 颜色) |
| 作者卡片按钮 | 无 | aside.card_author.button.link |
| 深色模式 | darkmode: true |
darkmode.enable: true |
| TOC | toc.enable / toc.number |
toc.post / toc.number;加密文章由
scripts/encrypt-toc-guard.js 自动关闭 |
| 文章元信息 | post_meta |
post_meta.page / post_meta.post |
| 公式 | math.every_page + mathjax.enable |
math.use: mathjax |
| 搜索 | local_search.enable |
search.use: local_search |
| 评论 | comments.active |
comments.use |
| 页脚年份 | footer.since |
footer.owner.since |
| 代码块主题 | codeblock.theme |
code_blocks.theme |
| 阅读进度 / 返回顶部 | reading_progress / back2top |
右侧按钮自带,无同名配置 |
| 自定义注入 | custom_file_path |
inject.head / inject.bottom |
| 版权块 | 手写 post-body-end.njk |
内置 post_copyright |
| 图片懒加载 | lozad(解密后要重触发) | lazyload.enable: false,默认原生加载,不需要那段修复 |
几个必须改的:
1 | # _config.butterfly.yml |
加密文章不需要逐篇写
toc: false:scripts/encrypt-toc-guard.js会在 HBE 加密后自动把page.toc设为false,并把page.origin清空,避免明文标题进入 HTML。
18.5 需要新写的文件
18.5.1
scripts/search-fix.js(修 searchdb 的双斜杠)
hexo-generator-searchdb 1.5.0 用
config.root + post.path 拼 URL;Hexo 8 的
post.path 自带前导 /,于是生成
//alfworld/,点击搜索结果会跳到
http://alfworld/。
scripts/ 比 node_modules
插件后加载,重新注册同名 json 生成器即可覆盖:
1 | /* global hexo */ |
18.5.2
source/css/custom.css(加密文章样式)
NExT 的 source/_data/styles.styl 是 Stylus,Butterfly
不读。把加密输入框那部分翻成普通 CSS 放进
source/css/custom.css,并在 inject.head
引入:
1 | inject: |
注意两点:
- HBE v4 的真实类名是
.hbe-input/#hbePass/.hbe-input-label/.hbe-error,旧的.hbe-input-container已经不存在; - 不要覆盖插件 default 主题的
opacity动画(输入框默认opacity:0,聚焦/填充后变 1),否则 label 会叠字;暗色用html[data-theme="dark"],不是 NExT 的.darkmode。
18.5.3
source/js/runtime.js +
footer.custom_text(运行时间)
NExT 的 footer.njk 里是”一段 HTML +
一段内联脚本”,Butterfly 拆成两半:
1 | footer: |
runtime.js 只要每秒更新
runtime_days/hours/mins/secs 即可;用
setInterval 常驻,PJAX
换页后也能继续。不想保留这个组件的话,这一小节可以整段跳过。
18.6 NExT 的 4 个注入文件去哪了
NExT source/_data/ |
Butterfly 对应 |
|---|---|
head.njk(love.js) |
inject.head 加
<script src="/js/love.js"></script> |
footer.njk(运行时间) |
footer.custom_text + inject.bottom 的
runtime.js |
post-body-end.njk(版权块) |
主题内置 post_copyright,不用再手写 |
post-body-end.njk(lozad 重触发) |
不需要:Butterfly lazyload
默认关闭,图片原生加载 |
styles.styl(加密样式) |
source/css/custom.css + inject.head |
post-body-start.njk(空) |
忽略 |
迁移完成后,source/_data/*.njk 和
styles.styl 可以删掉;但建议先留着,回滚
NExT 时还要用。
18.7 图片路径形态
Butterfly 的构建同样会把 Markdown 里的图片路径原样渲染:浏览器预览 / 构建 / 提交 / 部署前必须先转 online;Typora 编辑前再转回 local。
1 | python tools/path_convert.py online # 浏览器预览、构建、提交、部署前 |
这一步忘了,首页/文章里的图片会全部裂掉(src="E:\...")。
18.8 验证清单
| 页面 | 检查点 |
|---|---|
| 首页 | 摘要显示
description(不是正文/代码);头像是自己的;菜单有
Graph;Follow Me 指向自己的 GitHub |
| 普通文章 | 图片正常;侧栏 TOC 正常(无编号);版权块正常;代码高亮正常 |
| 加密文章 TOC | 源码无
id="card-toc"、无明文标题(scripts/encrypt-toc-guard.js
生效) |
| Mermaid | 图渲染成 SVG;代码示例里的字面
<pre><code class="mermaid"> 不被改写 |
| 加密文章 | 有密码框;解密后正文和图片都正常 |
| 公式文章 | $...$ 渲染成公式,不是原文 |
| 搜索 | 能搜到结果,跳转 URL 没有 // |
/graph/ |
200、独立页、菜单可点 |
| 暗色模式 | 右上角切换正常 |
| favicon | 浏览器标签页是自己的图标,不是主题默认 |
18.9 踩过的坑
- 主题 clone 到了根目录:
cd themes没生效,git clone ... butterfly得到E:\myBlog\butterfly;Copy-Item themes\butterfly\_config.yml自然找不到; - 忘了装
hexo-renderer-pug:构建直接失败; favicon仍指向/img/favicon.ico:那是主题自带的蝴蝶图标,要改成/images/site/favicon.ico;- 首页显示正文而不是
description:
index_post_content.method默认是3(自动截正文),改成2; - 公式不渲染:
math.use为空,填mathjax; - 搜索没开、开了又跳错:
search.use为空;填local_search后还要处理 searchdb 的//(见 18.5.1); - 菜单是空的:Butterfly 自带配置里
menu:全是注释,必须自己补; - 图片全裂:忘了
path_convert online; - 旧配置没搬:默认
footer.owner.since: 2025、toc.number: true、post_meta只显示 created,和 NExT 行为不一致;TOC 默认是开的,但加密文章要由scripts/encrypt-toc-guard.js单独关; - 嵌套
.git:克隆完没删themes/butterfly/.git; - MathJax 等不到
load:Butterfly 的mathjax.pug用window.addEventListener('load', ...),分享/图标/统计等第三方资源会拖住load,公式一直不渲染;已把该行改成DOMContentLoaded(升级主题时要重新打这个补丁,见 18.9 本条)。
18.10 回滚
1 | python tools/theme_switch.py --to next |
或手动把 _config.yml 的 theme: 改回
next。
回滚还需要站点级 _config.next.yml(Hexo 8
会自动读取),用它覆盖 NExT 的默认配置,而不是改 tracked 的
themes/next/_config.yml:
1 | toc: |
themes/next、source/_data/* 都还在;确认
Butterfly 稳定后再删除它们并单独提交一次。
18.11 Mermaid
NExT 时期 mermaid.enable 一直是 false,所以
11 个图在旧主题下也没渲染。Butterfly 也不能直接吃
hexo-filter-mermaid-diagrams 的
<pre><code class="mermaid">,需要一层适配:
hexo-filter-mermaid-diagrams(before_post_renderpriority 9)把 ```mermaid 转成<pre class="mermaid">;scripts/mermaid-fix.js(priority 11)把它改成 Butterfly 认识的<pre><code class="mermaid">;_config.butterfly.yml开启mermaid.enable: true、code_write: true(Butterfly 只有在 true 时才执行codeToMermaid)。
1 | hexo.extend.filter.register('before_post_render', function (data) { |
为什么用 before_post_render:加密文章的内容在
after_render
阶段已经被换成密文,只有渲染前才改得到;解密后主题的
hexo-blog-decrypt 会重新渲染 Mermaid。
18.12 主题切换脚本
tools/theme_switch.py 只改 _config.yml 的
theme: 一行,两套主题的配置各自保持独立:
1 | python tools/theme_switch.py --status |
- 不修改
search.path:NExT 和 Butterfly 都支持search.json,这样scripts/search-fix.js始终生效; --to next会检查/创建_config.next.yml;--build发现source/_posts还是E:\...图片形态时会拒绝构建,提示先path_convert online;- 切换后必须
hexo clean,避免旧主题 CSS/JS 和旧search.json残留。