Jekyll 博客自动化发文实践:一个可维护的 blog-post skill 是如何落地的

Posted by Dorck on May 24, 2026

为个人博客补一套自动化发文能力,看似只是“少敲几次 front matter”,真正落地时却会迅速演变成一个工程问题:如何适配现有博客结构、如何控制发布风险、如何让脚本保持确定性、又如何让不同 AI 编程环境复用同一套能力。本文就围绕这几个问题,复盘 blog-post skill 的完整实现过程。

这次实现的目标其实很明确:在当前 Jekyll 博客仓库中,新增一个可以被 CodexClaude Code 共用的发文 skill,让它支持创建文章、切换公开状态、补齐 front matter、按需执行 git add / commit / push,并尽可能贴合现有博客的目录结构与写作习惯。

1. 为什么不是继续手写文章模板

如果只是偶尔写一篇文章,直接复制旧文章的头部并不算昂贵。但一旦要把“创建文章”和“发布文章”变成一条可重复执行的工作流,手工方式的几个问题会立刻暴露出来:

  • front matter 容易漏字段,例如 publishedtagscategories
  • 文件路径不稳定,尤其是多主题目录下容易放错位置
  • date、标题、slug 和最终访问链接之间需要保持一致
  • 不同 Agent 环境需要反复解释“这篇文章应该怎么建”

更关键的一点是,这个博客仓库本身并不是一个全新的 Jekyll 默认模板,而是已经演化出自己的内容结构。文章实际并不都放在 _posts/ 根目录,而是分散在:

  • _posts/post_android
  • _posts/post_skills
  • _posts/post_gradle
  • _posts/post_other

这意味着自动化方案不能只会“生成一篇 markdown”,还必须理解这个仓库自己的组织方式。

2. 先分析仓库,再决定 skill 的边界

在开始写脚本之前,第一步不是设计命令,而是先确认博客本身的实现约定。

当前仓库有几个关键事实:

  • 导航栏通过 _includes/nav.html 自动枚举 site.pages
  • 文章页面主要依赖 titledatetagsdescriptionpublished
  • SEO 描述层默认会读取 excerptdescription
  • Rakefile 中虽然有 rake post,但它只会把文章生成到 _posts/ 根目录

这意味着现成脚本并不能直接满足需要。尤其是 rake post 这一类脚手架,只解决了“建文件”问题,却没有解决“建在正确的位置”“带上正确的元数据”“兼容现有工作流”这些更关键的问题。

于是这个 skill 的目标被进一步收敛为两层:

  • 上层 skill:负责理解用户意图,整理出参数
  • 下层 CLI:负责用确定性的方式修改仓库

这个职责划分后来被证明是整个实现里最重要的一次收敛。

3. 一个关键转折:脚本不该直接吃自然语言

最开始的直觉方案,其实很容易走向另一条路:直接让脚本接收整段自然语言,例如“创建一篇 Android 文章,标题是什么,发布时间是什么,是否公开”,然后在 CLI 内部用正则把这些信息抠出来。

这条路短期看起来很方便,但问题也非常明显。

3.1 不确定性太高

自然语言解析一旦放进脚本层,CLI 就会同时承担两类职责:

  • 语义理解
  • 文件落盘

前者天然是不稳定的,后者却必须是稳定的。把两件事情放在一起,最终会让一个原本应该确定可测试的工具,变成带着隐式猜测的执行器。

3.2 不利于测试

如果 CLI 接收的是结构化参数,那么测试只需要验证:

  • 输入某组参数,会落在哪个目录
  • front matter 是否正确
  • published 切换是否符合预期

但如果 CLI 先要解析自然语言,测试就会被迫跟着验证一堆语言变体。这对工具层来说,并不是一个值得承担的复杂度。

3.3 不利于多环境复用

CodexClaude Code 都具备很强的自然语言理解能力,真正缺的并不是“会不会解析一句话”,而是“有没有一条可以稳定调用的仓库命令”。既然如此,更合理的方式就是让 Agent 负责理解,脚本负责执行。

所以最终的实现明确改成:

  • Codex / Claude Code 把自然语言转换为结构化参数
  • blog-post-skill.js 只接受明确命令与 flags

这也是为什么现在的 CLI 会采用如下形式:

1
2
3
4
5
6
node ./tools/blog-post-skill/bin/blog-post-skill.js create \
  --title "Activity 启动过程" \
  --section android \
  --date "2026-05-24 09:30" \
  --published false \
  --tags "Android,源码分析"

这类接口虽然看起来没有“整段自然语言”那么花哨,但它的可测试性和可维护性要高得多。

4. 结构化 CLI 是如何设计的

这套工具最后被收敛为三个明确命令:

  • sections
  • create
  • publish

4.1 sections

sections 的作用是列出当前博客仓库支持的文章分区,例如:

  • android
  • skills
  • gradle
  • other

它对应的意义并不只是“打印一个列表”,而是把仓库内部的目录映射显式暴露出来,让上层 Agent 不必硬编码这些知识。

4.2 create

create 负责:

  • 解析显式参数
  • 根据 section 找到目标目录
  • 生成 front matter
  • 根据标题生成 slug
  • 组合正文模板或导入外部 markdown

例如 android 会映射到 _posts/post_androidskills 会映射到 _posts/post_skills。这一层逻辑被固化在脚本内部的 SECTION_CONFIG 里,避免每次创建文章时重新判断路径规则。

4.3 publish

publish 的职责相对更偏向“已有文件修改”:

  • 切换 published: true/false
  • 更新 date
  • 更新 tagsdescription
  • 可选执行 git add / commit / push

这让“创建文章”和“发布文章”不再混在一起。换句话说,创建是一次性落盘动作,而发布则是一次状态切换动作。

这种拆分对于博客工作流非常重要,因为许多文章并不是写完立刻发布,而是会在草稿态停留一段时间。

5. front matter 生成为什么要贴合现有博客

自动化发文脚本如果只会生成 Jekyll 最小头部,实际价值并不大。真正有用的是:它生成的文章必须天然符合当前博客自己的渲染约定。

因此这套 skill 在 front matter 设计上做了几件事。

5.1 固定页面布局约定

默认写入:

1
2
3
layout: post
header-style: text
catalog: false

这与现有文章的大多数配置保持一致,避免新文章渲染风格突然漂移。

5.2 自动补 description

博客文章页本身会利用 descriptionexcerpt 做头部描述与 SEO 文本,因此脚本在用户未显式传入摘要时,会基于:

  • 标题
  • subtitle
  • tags
  • section

自动生成一条短描述。它未必是最终最优文本,但足以保证生成出来的文章不是一份“只有标题没有描述”的半成品。

5.3 默认加入 excerpt_separator

脚本会默认写入:

1
excerpt_separator: "<!--more-->"

这样做的目的,是把首页摘要和正文主体的边界提前规范化,而不是把摘要逻辑留给每一篇文章临时发挥。

6. 为什么还要补正文骨架模板

很多发文工具会停在 front matter 层,但这个 skill 又向前多走了一步:当用户没有提供完整正文时,脚本会根据 template 生成一个最小可写的骨架。

目前支持的模板包括:

  • default
  • tutorial
  • note
  • review

例如 tutorial 会生成如下结构:

1
2
3
4
5
6
7
8
9
## 问题背景

## 环境说明

## 核心步骤

## 关键细节

## 总结

这里的关键并不是“自动生成了几级标题”,而是让不同类型的文章在开稿阶段就拥有稳定结构。对于持续写作来说,这种结构化约束往往比单纯少敲几行 YAML 更有价值。

7. 如何同时适配 Codex 与 Claude Code

实现共享能力的核心思路,是让仓库只维护一套真正可执行的能力,把环境差异压缩到外围包装层。

所以整个目录被拆成了三部分:

  • tools/blog-post-skill/
  • .codex/skills/blog-post-publisher/
  • .claude/commands/blog-post.md

其中:

  • tools/blog-post-skill/ 是共享运行时
  • SKILL.md 负责告诉 Codex 什么时候该使用这套能力
  • .claude/commands/blog-post.md 负责告诉 Claude Code 该如何组装命令

这个设计的好处是,真正决定仓库状态的逻辑永远只有一份。上层环境无论是 Codex 还是 Claude Code,都不需要各自再维护一套文件写入实现。

8. 一个额外踩坑:为什么工具 README 会出现在导航栏里

skill 本身落地之后,又暴露出一个很典型的 Jekyll 仓库问题:只要某个目录没有被正确排除,它就可能被当作站点内容的一部分。

这个博客的导航栏会自动枚举 site.pages。而 tools/blog-post-skill/README.md 作为仓库里的说明文档,如果不被排除出构建范围,就有机会进入站点页面集合,进而在导航栏中冒出一个不该出现的入口。

解决方式很直接:把 tools 以及相关工具目录加入 _config.ymlexclude

这类问题很容易被忽视,因为它不属于“发文脚本是否能运行”的范畴,却直接影响最终站点表现。也正因为如此,发文自动化不应该被理解为一段孤立脚本,而应该被视为站点工程的一部分。

9. 这套实现真正解决了什么

到这里再回头看,这个 skill 真正解决的并不是“创建文章文件很麻烦”这么简单的问题,而是把博客发文流程中的几个关键环节结构化了:

  • 文章目录归类
  • front matter 规范化
  • 草稿与公开状态切换
  • AI 编程环境复用
  • Git 提交与推送串联

更重要的是,它明确了一个适用于很多仓库自动化场景的原则:

语义理解交给 Agent,确定性执行交给脚本。

一旦这条边界稳定下来,后续无论是继续接图片处理、封面生成、文章校验还是发布检查,都可以沿着同一条结构继续扩展,而不需要反复推翻底层设计。

10. 后续可以继续演进的方向

目前这套 blog-post skill 已经足够覆盖个人博客的日常发文流程,但它仍然有不少可以继续扩展的空间:

  • 在创建文章前自动校验标签与分类是否符合既有约定
  • 增加图片资源目录的自动创建与引用修正
  • 在发布前执行本地预览或构建检查
  • 将文章元数据进一步抽象为可复用 schema

如果把它放到更大的团队环境里,这套能力甚至可以继续演化成一个“内容仓库操作层”:前端页面、博客文章、知识库文档、发布说明,统一都通过结构化命令落库。

对个人博客来说,这一步或许还谈不上“平台化”,但它至少证明了一件事:只要先把职责边界拉清,自动化往往比想象中更容易做对,也更容易持续迭代。


许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。