从零搭建一个技术博客——Hexo + Butterfly 主题深度定制实录
一、项目概述
黑岩的小屋 是一个面向安全研究与工具开发的技术博客,基于 Hexo 7.3.0 静态站点生成器与 Butterfly 5.5.4 主题搭建,部署于 GitHub Pages。博客目前包含 20 篇文章,涵盖 Android 逆向工程、Linux 内核调试、CTF 题解、直播工具开发与 AI 应用等方向。
技术栈一览
| 层面 | 技术选型 |
|---|---|
| 静态站点生成 | Hexo 7.3.0 |
| 主题 | Butterfly 5.5.4 |
| 评论系统 | Utterances(GitHub Issues 驱动) |
| 站内搜索 | hexo-generator-search + Butterfly local_search |
| 部署 | hexo-deployer-git → GitHub Pages (gh-pages) |
| 文章抓取 | 自研 Python 脚本(52pojie.cn → Markdown) |
| CSS 预处理 | Stylus(Butterfly 原生支持) |
| 语法高亮 | highlight.js(darker 主题) |
二、搭建与基础配置
2.1 Hexo 初始化
Hexo 的初始化非常简洁,几条命令即可完成:
1 | npm install -g hexo-cli |
Butterfly 主题通过 npm 安装:
1 | npm install hexo-theme-butterfly |
然后在 _config.yml 中将主题设为 butterfly。
2.2 部署到 GitHub Pages
使用 hexo-deployer-git 插件,配置 SSH 免密推送:
1 | # _config.yml |
每次发布只需执行:
1 | hexo generate --deploy |
hexo generate 将 Markdown 文章渲染为静态 HTML,hexo deploy 自动将 public/ 目录推送到 gh-pages 分支。整个流程一键完成,无需 CI/CD 流水线。
三、主题深度定制
3.1 暗色模式全局改造
Butterfly 主题内置暗色模式,但默认的 #121212 纯黑背景对比度过高,长时间阅读容易疲劳。我决定将所有暗色背景统一调整为 #4d4d4d(炭灰色)。
主题的暗色变量定义在 themes/butterfly/source/css/_mode/darkmode.styl,这是一个 Stylus 文件。Strictly speaking 直接修改主题文件不是最佳实践(升级时会被覆盖),但在没有子主题机制的情况下,这是获得完全控制权的最快路径。
改动涉及约 25 个 CSS 变量:
1 | // darkmode.styl 中的关键变量 |
同时,对于分页和推荐文章的卡片,进一步细化为 #5a5a5a,并调整了字体大小与卡片高度,使整体视觉层次更加分明。
3.2 毛玻璃导航栏与页脚
Butterfly 的导航栏支持 fixed 固定定位。利用 CSS 的 backdrop-filter 属性,可以轻松实现毛玻璃效果:
1 | #footer { |
导航栏的毛玻璃效果由主题原生支持,页脚则需要通过 Butterfly 的 inject 注入机制添加自定义 CSS。两者配合,形成了统一的视觉风格。
3.3 主题持久化
Butterfly 的主题切换使用 localStorage 存储用户偏好,但初始加载时依赖服务端配置的 display_mode。为了让用户的主题选择在页面导航时保持一致,需要在页面加载早期读取 localStorage 并设置 data-theme 属性。
Butterfly 内部使用 btf.saveToLocal 封装,存储格式为 JSON:
1 | { |
在 Butterfly 的 inject.bottom 中注入脚本,确保在其他 JS 执行前完成主题恢复:
1 | (function() { |
3.4 头像黑边裁切
头像通过 object-fit: cover 在 110px 圆形容器中展示,但图片本身带有黑边。在不修改原图的前提下,使用 CSS transform: scale() 放大图片,配合父容器的 overflow: hidden 裁切掉黑边:
1 | .avatar-img img { |
悬停时保留旋转动画,同时维持缩放比例。
四、站内搜索
Hexo 生态中有多种搜索方案,我选择了最轻量的 hexo-generator-search,它会在构建时生成一个 search.xml 文件,包含所有文章的标题、路径和正文内容。
配置分为两部分:
Hexo 层(_config.yml):
1 | search: |
Butterfly 主题层(_config.butterfly.yml):
1 | search: |
关键点是 Butterfly 的搜索开关是 search.use: local_search,而不是 search.local_search.enable: true。这个配置差异是 Butterfly 特有的设计——它支持多种搜索后端(Algolia、本地搜索等),use 字段决定激活哪一个。
五、文章抓取自动化
博客中 15 篇逆向工程文章来自吾爱破解论坛。手动搬运效率低下,因此编写了一个 Python 爬虫 scrape_52pojie.py(363 行),自动化整个流程。
5.1 核心流程
1 | 论坛帖子 URL → HTTP 请求 → HTML 解析 → Markdown 转换 → 保存为 .md |
5.2 技术要点
请求处理:使用 urllib(无第三方依赖),处理 gzip 压缩、GBK 编码、重试逻辑(3 次,5 秒退避)。
HTML 解析:纯正则表达式实现,不依赖 BeautifulSoup。论坛 HTML 结构复杂,包含多层 <div> 嵌套、JS 包裹的图片链接、多种代码块格式等。正则虽不如 HTML 解析器优雅,但在处理不一致的论坛标记时反而更灵活。
代码块转换:论坛使用 <pre class="brush: LANG;"> 标记代码块,需要映射为 Markdown 的 fenced code block:
1 | LANG_MAP = { |
图片提取:论坛图片通常以缩略图 + 原图的形式出现。原图 URL 藏在 zoomfile 或 file 属性中,需要优先提取。
自动标签与分类:通过约 40 组关键词映射自动打标签。分类则有优先级逻辑——CTF 文章归为”安全分析”,包含逆向关键词的归为”逆向工程”,内核/驱动相关归为”软件调试”。
输出:生成 YAML frontmatter(标题、日期、标签、分类、描述)+ Markdown 正文,直接保存到 Hexo 的 source/_posts/ 目录。
六、DOM 异常排查:侧边栏位置错乱
这是开发过程中遇到的最隐蔽的 bug。
6.1 现象
在分页第 2 页和部分文章详情页,原本位于右侧的侧边栏(#aside-content)掉到了内容下方,页面布局完全错乱。
6.2 排查过程
初步怀疑是 CSS flexbox 布局问题。Butterfly 的 .layout 使用 display: flex,主内容区占 74%,侧边栏占 26%。但检查编译后的 CSS 和 HTML 结构,两页完全相同,没有发现 flex 相关差异。
然后怀疑是 position: sticky 导致了视觉偏移。侧边栏内部的 .sticky_layout 使用 sticky 定位,但 sticky 只在有滚动时生效,按理不应影响水平布局。
最后,通过编写一个 DOM 深度分析脚本,逐标签解析页面结构并计算嵌套层级,发现了关键差异:
1 | PAGE1: #aside-content at depth 2, final depth: 1 (正常) |
侧边栏的嵌套深度多了 2 层,这意味着有未闭合的 HTML 标签被注入了 DOM。
6.3 根因
问题出在一篇 52pojie 转载文章的 frontmatter description 字段中:
1 | description: "...入口点果然被修改了, ```xml <activity android:name=\"com.xxxx.xxx.MainActivity\" android:exported=\"" |
内联的 ```xml 代码块标记未被 Markdown 渲染器识别为代码块(代码块语法要求反引号在行首),其中的 <activity android:name="..."> XML 标签被浏览器当作真实 HTML 元素解析,破坏了页面 DOM 结构。该文章只出现在分页第 2 页,恰好解释了 bug 的页面特异性。
同一篇文章的正文中还有更多未转义的 XML 标签(如 <intent-filter>、<action>、<category>),它们在 Markdown 正文中作为解释文字出现,同样被当作 HTML 注入页面。
6.4 修复
将所有非代码块内的 <> 转义为 <>:
1 | <!-- 修复前 --> |
这个问题的教训是:在 Markdown 中引用 XML/HTML 标签时,务必转义尖括号,即使它们看起来像是会被代码块语法捕获。Markdown 渲染器对代码块的识别依赖于严格的格式(反引号必须在行首或仅有少量缩进),内联的反引号不会触发代码块模式。
七、浏览器缓存策略
7.1 问题
每次部署新内容后,访问页面显示的仍是旧版本,必须手动 Ctrl+F5 强制刷新。这是因为 GitHub Pages 为 HTML 文件设置了默认缓存头(Cache-Control: max-age=600,即 10 分钟),浏览器在这段时间内不会重新请求。
7.2 解决方案
在 Butterfly 的 inject.head 中注入缓存控制 meta 标签:
1 | <meta http-equiv="Cache-Control" content="no-cache"> |
这里 no-cache 的含义不是”禁止缓存”,而是”使用缓存前必须先向服务器验证”。实际行为是:
- 浏览器发送条件请求(
If-None-Match/If-Modified-Since) - 如果页面未变化,服务器返回
304 Not Modified,浏览器使用本地缓存 - 如果页面有更新,服务器返回新内容
这说明 no-cache 在保持缓存优势的同时确保了内容新鲜度,是一个适合静态博客的折中方案。
八、文章提交与部署流程
Hexo 博客的文章发布遵循一条固定的流水线,掌握后只需两条命令即可完成。
8.1 文章文件结构
每篇文章是一个独立的 Markdown 文件,存放在 source/_posts/ 目录下。文件命名采用 标题.md 格式,Hexo 会根据文件名和 frontmatter 中的日期生成最终 URL。
文件由两部分组成:
YAML Frontmatter(文章元数据):
1 |
|
Markdown 正文:标准 Markdown 语法,支持代码块、图片、表格等。
8.2 添加新文章
有两种方式创建新文章:
方式一:Hexo 命令行创建
1 | hexo new post "文章标题" |
这会在 source/_posts/ 下生成一个带默认 frontmatter 的 .md 文件,然后编辑该文件填入内容即可。
方式二:直接放置 Markdown 文件
将写好的 .md 文件直接复制到 source/_posts/ 目录下,确保 frontmatter 格式正确。这是批量导入或从外部来源获取文章时最常用的方式:
1 | cp /path/to/文章.md source/_posts/ |
8.3 本地预览
在部署前建议先本地预览,确认排版和链接无误:
1 | hexo server |
启动后在浏览器访问 http://localhost:4000 即可查看。Hexo 会监听文件变化并自动刷新页面。
8.4 生成与部署
确认无误后,一条命令完成构建和发布:
1 | hexo generate --deploy |
这等价于先后执行:
1 | hexo generate # 将 Markdown 渲染为静态 HTML,输出到 public/ 目录 |
部署使用 hexo-deployer-git 插件,底层通过 git push 将构建产物推送到远程仓库的 gh-pages 分支。GitHub Pages 检测到分支更新后会自动部署上线,通常几秒内即可通过 https://<username>.github.io 访问到新内容。
8.5 完整工作流示例
以添加本文为例,完整流程如下:
1 | # 1. 将写好的文章复制到 Hexo 文章目录 |
无需 CI/CD 流水线,无需操作 GitHub 网页端,整个发布流程控制在一条命令内。
九、总结
本项目从零搭建到上线,涉及了静态站点生成器的配置、CSS 主题定制、搜索与评论集成、自动化内容抓取、DOM 调试以及部署优化等多个方面。几个关键收获:
先理解框架再定制:Butterfly 的配置体系有自己的约定(如搜索的
use字段),不能想当然地套用通用模式,需要阅读源码或文档确认。Markdown 中的 HTML 转义:转载外部内容时,最容易被忽略的安全问题不是 XSS,而是 XML 标签破坏 DOM 结构。任何非代码块内的
<tag>都需要转义。缓存策略需要精细权衡:完全禁用缓存会损害性能,完全依赖默认缓存则影响更新时效。
no-cache提供了良好的中间地带。自动化脚本值得投入:363 行的爬虫脚本虽然编写耗时,但它使 15 篇文章的批量导入成为可能,并留下了可持续复用的工具。