本文整理自 Michael’s Blog · Hexo 博客工程化实战 ,并结合本站(Butterfly 主题 + hexo-deployer-git)的实际情况做了适配与补充。
为什么要把博客「工程化」? 写博客这件事,技术人通常的路径是:
本地 hexo new 创建文章
Markdown 写内容
hexo g && hexo d 手动部署
这条路径在只有几十篇文章时完全够用。但当文章数量突破 400+、你希望保持周更甚至日更节奏时,手动流程就会暴露出几个致命问题:
部署依赖本地环境 :换台电脑就写不了
选题靠灵感驱动 :没灵感就断更
SEO 全靠感觉 :标题写得好不好、结构化数据有没有、sitemap 对不对,全凭手感
写完就忘 :哪篇文章阅读量高、哪些关键词带来流量,完全没有数据反馈
这篇文章记录的是把 Hexo 博客从「手动写作」改造成「自动化内容工厂」的完整过程,涉及四个核心模块:
自动部署 :push 即发布
AI 辅助选题与写作 :选题池 + 多 Worker 并行生产
SEO 工程化 :结构化数据、自动 sitemap、内链策略
阅读量分析 :数据采集 → 看板 → 反哺选题
每个模块都有可直接复用的配置和代码。
一、从 hexo deploy 到 push 即发布 1.1 两条部署路线 Hexo 的部署本质上就是把 public/ 这个目录「搬」到托管平台。常见两条路线:
路线
原理
适合
本地 push (本站当前方案)
本地构建后,hexo-deployer-git 把 public/ 强推到 Pages 仓库
文章量不大、想完全掌控本地环境
CI 构建 (GitHub Actions)
仓库只存 Markdown,CI 里安装依赖、构建、发布
文章多、多人协作、想换台电脑就能写
本站用的是第一条:_config.yml 中配置 hexo-deployer-git,一条命令完成生成和部署。
1 2 3 4 5 deploy: type: git repo: https://github.com/<user>/<user>.github.io.git branch: main
注意:hexo-deployer-git 使用 .deploy_git/ 作为中转仓库,它会把 public/ 里的内容强制推送过去,所以 Pages 仓库里不需要(也不应该)保留源文件。
1.2 GitHub Actions 自动部署 当你希望「写完 Markdown,git push 就完事」时,把构建搬到 CI。在仓库根目录创建 .github/workflows/deploy.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 name: Deploy Hexo Blog on: push: branches: - main concurrency: group: deploy-${{ github.ref }} cancel-in-progress: false permissions: contents: read pages: write id-token: write jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npx hexo generate env: NODE_ENV: production - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} 如果你有多个目标(比如 GitHub Pages + 自建服务器),可以用 `needs` 串起来: ```yaml deploy-to-server: needs: build-and-deploy if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout built files uses: actions/checkout@v4 with: ref: gh-pages path: public - name: Deploy via rsync uses: burnett01/rsync-deployments@7.0.1 with: switches: -avzr --delete path: public/ remote_path: /var/www/blog/ remote_host: ${{ secrets.SERVER_HOST }} remote_user: ${{ secrets.SERVER_USER }} remote_key: ${{ secrets.SSH_PRIVATE_KEY }}
1.4 构建缓存优化 文章一多,构建时间就成了问题。加入缓存后构建时间可以从 90s 降到 25s:
1 2 3 4 5 6 7 8 9 - name: Cache Hexo uses: actions/cache@v4 with: path: | node_modules .deploy_git key: hexo-${{ runner.os }}-${{ hashFiles('package-lock.json') }} restore-keys: | hexo-${{ runner.os }}-
1.5 自动化预检 部署前加一道 lint 和死链检查,防止发布有问题的内容:
1 2 3 4 5 6 7 8 - name: Check broken links run: | npx hexo generate npx broken-link-checker http://localhost:4000 --recursive --ordered --filter-level 3 || true - name: Validate HTML run: | npx html-validate public/**/*.html || true
二、AI 辅助选题与写作 这是整个工程化改造中投入产出比最高的部分。
2.1 选题池设计 在仓库根目录维护一个 .writing-backlog.md,格式简单到极致:
1 2 3 4 5 6 # Writing Backlog - [ ] Laravel 事件溯源实战:Event Sourcing + EventStore 落地踩坑- [ ] Go 并发模式:Channel、Context、errgroup 组合实战- [ ] Redis 7.0 Function 替代 Lua 脚本:迁移指南与性能对比- [ ] Kubernetes HPA 自定义指标:基于 Laravel 队列深度的自动扩缩容
- [ ] 表示待写,- [x] 表示已完成。Worker 领取任务时立刻用 sed 把 [ ] 改成 [x] 锁定选题,防止多个 Worker 重复写作。
2.2 AI Worker 流水线 用 cron 配置多个定时 Worker,每个 Worker 独立运行:
1 2 3 4 5 6 7 8 9 10 ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ 每2小时执行 │ │ 每3小时执行 │ │ 每4小时执行 │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ ▼ ▼ ▼ ┌──────────────────────────────────────────────────┐ │ .writing-backlog.md │ │ grep → 锁定 → 写文章 → 保存 → 更新选题池 │ └──────────────────────────────────────────────────┘
每个 Worker 的核心逻辑:
1 2 3 4 5 6 7 8 9 10 TOPIC=$(grep -m1 '^- \[ \]' .writing-backlog.md) FIRST30=$(echo "$TOPIC " | cut -c1-35) sed -i '' "s/^- \[ \] ${FIRST30} /- [x] ${FIRST30} /" .writing-backlog.md
2.3 选题质量控制 不是所有选题都值得写。在选题池里加上质量评估维度:
1 2 3 4 5 # Writing Backlog - 格式说明 # - [ ] 选题标题 | 预估搜索量(高/中/低) | 竞争度(高/中/低) | 优先级(P0/P1/P2) - [ ] Laravel 事件溯源实战 | 搜索量:中 | 竞争度:低 | 优先级:P0- [ ] Go 并发模式 | 搜索量:高 | 竞争度:高 | 优先级:P1
选题原则:
P0 :搜索量中/低 + 竞争度低 = 蓝海词,优先写
P1 :搜索量高 + 竞争度高 = 需要有差异化角度
P2 :搜索量低 + 竞争度高 = 不值得写,除非是个人笔记
2.4 自动 Git 提交 Worker 写完文章后自动 commit 并 push:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 git add source /_posts/ git commit -m "post: $(date +%Y-%m-%d) 新文章标题" --- SEO 不是「写完文章再想」的事,而应该内建到写作流程里。 每篇文章的 frontmatter 都是 SEO 的一部分: ```yaml --- title: 主关键词在前,长尾描述在后 description: 用于 meta description,直接影响搜索结果摘要 tags: [关键词1, 关键词2, 关键词3] categories: [唯一分类] date : 2026-01-01 10:00:00updated: 2026-01-02 ---
要点:
title:主关键词在前,长尾描述在后
description:用于 <meta name="description">,直接影响搜索结果摘要
tags:3-5 个,覆盖文章核心概念
categories:唯一,用于面包屑导航和站点结构
3.2 结构化数据(JSON-LD) 在生成阶段往 <head> 注入 Article 类型的结构化数据:
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 hexo.extend .filter .register ('after_render' , function (html, data ) { if (data.layout !== 'post' ) return html; const jsonLD = { "@context" : "https://schema.org" , "@type" : "BlogPosting" , "headline" : data.title , "description" : data.description || data.title , "datePublished" : data.date .toISOString (), "dateModified" : (data.updated || data.date ).toISOString (), "author" : { "@type" : "Person" , "name" : "Biko" , "url" : "https://biko-duckyo.github.io" }, "publisher" : { "@type" : "Organization" , "name" : "Biko的个人小站" }, "mainEntityOfPage" : { "@type" : "WebPage" , "@id" : "https://biko-duckyo.github.io" + data.path } }; const script = '<script type="application/ld+json">' + JSON .stringify (jsonLD, null , 2 ) + '</script>' ; return html.replace ('</head>' , script + '\n</head>' ); });
Hexo 会自动 require scripts/ 目录下的所有文件,所以把这段放进 scripts/ 即可生效,不需要改主题源码。
3.3 Sitemap 自动生成 hexo-generator-sitemap 已经够用,但需要正确配置:
1 2 3 4 5 6 sitemap: path: sitemap.xml rel: true tags: true categories: true
提交到 Google Search Console 后,确认 robots.txt 包含:
1 Sitemap: https://<your-site>/sitemap.xml
3.4 内链策略 文章之间的内链是 SEO 的重要信号。可以在写作流水线里加一步「相关阅读」注入:
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 import reimport osdef find_related_posts (current_tags, posts_dir ): """根据 tags 相似度找到相关文章""" related = [] for filename in os.listdir(posts_dir): if not filename.endswith('.md' ): continue filepath = os.path.join(posts_dir, filename) with open (filepath, 'r' , encoding='utf-8' ) as f: content = f.read() tags_match = re.search(r'tags:\s*\n((?:\s+-?\s*.+\n)*)' , content) if tags_match: post_tags = set (re.findall(r'-\s+(.+)' , tags_match.group(1 ))) overlap = len (current_tags & post_tags) if overlap >= 2 : title_match = re.search(r'title:\s*(.+)' , content) if title_match: related.append((overlap, title_match.group(1 ).strip(), filename)) related.sort(reverse=True ) return related[:5 ] def inject_links (content, related_posts ): """在文章末尾注入相关阅读""" if not related_posts: return content links = "\n" .join([ "- [" + title + "](/" + filename.replace('.md' , '' ) + "/)" for _, title, filename in related_posts ]) return content + "\n\n## 相关阅读\n\n" + links + "\n"
3.5 自动化 SEO 审计 在 CI 里加一段 frontmatter 完整性检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 - name: SEO Audit run: | for file in source/_posts/**/*.md; do if ! grep -q "description:" "$file"; then echo "⚠️ Missing description: $file" fi if ! grep -q "categories:" "$file"; then echo "⚠️ Missing categories: $file" fi done grep -r "^title:" source/_posts/ | sort | uniq -d
四、阅读量分析 写完就忘是内容创作最大的浪费。把数据采集回来,才能形成「写作 → 反馈 → 选题」的正循环。
4.1 数据采集 方案有三档,按投入从低到高:
方案
成本
数据粒度
隐私
不蒜子 (busuanzi)
免费、一行代码
站点 PV/UV、单篇阅读量
第三方统计
Umami / Plausible (自托管)
一台小服务器
页面级 PV、来源、停留
数据自有
GA4 + 自定义事件
免费、配置复杂
全量行为、可做漏斗
Google
本站当前用的是不蒜子,配置在 Butterfly 的 busuanzi 开关里,零成本但粒度粗。想要按文章、按来源分析,就得换 Umami 这类自托管方案。
4.2 看板 数据落到 SQLite / Postgres 后,用 Metabase 或 Grafana 做一个最简单的看板,关注四个指标:
单篇 PV/UV 排行 :哪些文章真的有人看
流量来源 :搜索 / 直链 / 社交,各占多少
搜索关键词 :哪些词带来了流量(Search Console)
跳出率与停留 :内容是否真的被读完
4.3 反哺选题 看板的最终目的是回写进选题池:
1 2 3 4 # Writing Backlog - [x] Hexo 博客工程化实战 | 已完成,PV 排名 #3- [ ] Hexo 博客工程化实战(续):阅读量看板搭建 | 承接高 PV 文章 | 优先级:P0
经验: 一篇文章 PV 高,就写它的「续集 / 进阶 / 姊妹篇」——这是最省力的选题来源,因为需求已经被验证过了。
五、本站的落地清单 结合本站(Butterfly + GitHub Pages)现状,按投入产出比排序的改造顺序:
短期 :在 scripts/ 里加 JSON-LD 结构化数据;补全每篇文章的 description
中期 :把部署迁移到 GitHub Actions(换台电脑也能写);接入自托管 Umami
长期 :建选题池 + AI Worker 流水线;每周复盘一次看板,把高 PV 主题做成系列
一句话总结:手动部署解决「能不能发」,工程化解决「能不能持续发」。 文章数过了 100 篇,后者才是真正的瓶颈。
git push origin main
选题 → AI 写作 → git push → CI 构建 → 部署上线
1 2 3 4 publish_dir: ./public publish_branch: gh-pages commit_message: "deploy: ${{ github.event.head_commit.message }}"
这里发布到 gh-pages 分支。如果站点仓库本身就是 <user>.github.io(例如本站),对应分支要改成 main。