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 优化
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行才执行更新, 否则撤销暂存, 工作区保持原样,
安静退出. 另外, 只要存在删除类改动(比如删除某篇文章或文件),
无论行数多少都会直接执行更新——内容下线应当及时, 不应被阈值卡住.
阈值和各权重都在脚本顶部的配置区, 可以随时改.
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.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/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/目录下,
成功/失败都会弹桌面通知. 想要暂停自动更新,
在任务计划程序中禁用BlogAutoUpdate任务即可.
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>后重建)。 - 版本号语义: 主版本号变化 = 排序/流程等行为发生变化(如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)。
文档
tools/README.md: 脚本一览、8步流程、触发规则、网络兜底、首页排序与updated、配置点、用法、运行产物、定时任务接线、维护提示(含换行陷阱与top排除规则)全部更新; 新增第8节指向tools/CHANGELOG.md。- 本文(第7.4、15.2.4、15.3、15.4、16章)同步改写;
正文中的脚本路径统一为
tools/。
对应提交:
3f2496d(生成器+第4步+目录迁移) →
6b8296c(换行修复) →
f8849b1/d9816ba/b28214a(文档、自检、忽略top、手动开关)
→ c6f136f(围栏规则 + 找回96处高亮) →
9939fc0(公式与围栏内容修复) →
db461c8(第17章《问题处理》) →
8984082(批量渲染修复 + 渲染风险体检) →
7a30a97(标题改写) → 本文档同步提交;
标签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。
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]; 目录由主题侧边栏提供 |
体检顺手加进了--selfcheck(只告警、不阻断流程):
①$$ 块内含空行; ②$$ 块里用了
\\ 换行却没有对齐环境;
③正文里的反斜杠命令(自动跳过代码块、行内代码、图片链接与公式)。