为个人博客补一套自动化发文能力,看似只是“少敲几次 front matter”,真正落地时却会迅速演变成一个工程问题:如何适配现有博客结构、如何控制发布风险、如何让脚本保持确定性、又如何让不同 AI 编程环境复用同一套能力。本文就围绕这几个问题,复盘 blog-post skill 的完整实现过程。
这次实现的目标其实很明确:在当前 Jekyll 博客仓库中,新增一个可以被 Codex 和 Claude Code 共用的发文 skill,让它支持创建文章、切换公开状态、补齐 front matter、按需执行 git add / commit / push,并尽可能贴合现有博客的目录结构与写作习惯。
1. 为什么不是继续手写文章模板
如果只是偶尔写一篇文章,直接复制旧文章的头部并不算昂贵。但一旦要把“创建文章”和“发布文章”变成一条可重复执行的工作流,手工方式的几个问题会立刻暴露出来:
- front matter 容易漏字段,例如
published、tags、categories - 文件路径不稳定,尤其是多主题目录下容易放错位置
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 - 文章页面主要依赖
title、date、tags、description、published - SEO 描述层默认会读取
excerpt或description Rakefile中虽然有rake post,但它只会把文章生成到_posts/根目录
这意味着现成脚本并不能直接满足需要。尤其是 rake post 这一类脚手架,只解决了“建文件”问题,却没有解决“建在正确的位置”“带上正确的元数据”“兼容现有工作流”这些更关键的问题。
于是这个 skill 的目标被进一步收敛为两层:
- 上层 skill:负责理解用户意图,整理出参数
- 下层 CLI:负责用确定性的方式修改仓库
这个职责划分后来被证明是整个实现里最重要的一次收敛。
3. 一个关键转折:脚本不该直接吃自然语言
最开始的直觉方案,其实很容易走向另一条路:直接让脚本接收整段自然语言,例如“创建一篇 Android 文章,标题是什么,发布时间是什么,是否公开”,然后在 CLI 内部用正则把这些信息抠出来。
这条路短期看起来很方便,但问题也非常明显。
3.1 不确定性太高
自然语言解析一旦放进脚本层,CLI 就会同时承担两类职责:
- 语义理解
- 文件落盘
前者天然是不稳定的,后者却必须是稳定的。把两件事情放在一起,最终会让一个原本应该确定可测试的工具,变成带着隐式猜测的执行器。
3.2 不利于测试
如果 CLI 接收的是结构化参数,那么测试只需要验证:
- 输入某组参数,会落在哪个目录
- front matter 是否正确
published切换是否符合预期
但如果 CLI 先要解析自然语言,测试就会被迫跟着验证一堆语言变体。这对工具层来说,并不是一个值得承担的复杂度。
3.3 不利于多环境复用
Codex 和 Claude 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 是如何设计的
这套工具最后被收敛为三个明确命令:
sectionscreatepublish
4.1 sections
sections 的作用是列出当前博客仓库支持的文章分区,例如:
androidskillsgradleother
它对应的意义并不只是“打印一个列表”,而是把仓库内部的目录映射显式暴露出来,让上层 Agent 不必硬编码这些知识。
4.2 create
create 负责:
- 解析显式参数
- 根据
section找到目标目录 - 生成 front matter
- 根据标题生成 slug
- 组合正文模板或导入外部 markdown
例如 android 会映射到 _posts/post_android,skills 会映射到 _posts/post_skills。这一层逻辑被固化在脚本内部的 SECTION_CONFIG 里,避免每次创建文章时重新判断路径规则。
4.3 publish
publish 的职责相对更偏向“已有文件修改”:
- 切换
published: true/false - 更新
date - 更新
tags、description - 可选执行
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
博客文章页本身会利用 description 和 excerpt 做头部描述与 SEO 文本,因此脚本在用户未显式传入摘要时,会基于:
- 标题
- subtitle
- tags
- section
自动生成一条短描述。它未必是最终最优文本,但足以保证生成出来的文章不是一份“只有标题没有描述”的半成品。
5.3 默认加入 excerpt_separator
脚本会默认写入:
1
excerpt_separator: "<!--more-->"
这样做的目的,是把首页摘要和正文主体的边界提前规范化,而不是把摘要逻辑留给每一篇文章临时发挥。
6. 为什么还要补正文骨架模板
很多发文工具会停在 front matter 层,但这个 skill 又向前多走了一步:当用户没有提供完整正文时,脚本会根据 template 生成一个最小可写的骨架。
目前支持的模板包括:
defaulttutorialnotereview
例如 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.yml 的 exclude。
这类问题很容易被忽视,因为它不属于“发文脚本是否能运行”的范畴,却直接影响最终站点表现。也正因为如此,发文自动化不应该被理解为一段孤立脚本,而应该被视为站点工程的一部分。
9. 这套实现真正解决了什么
到这里再回头看,这个 skill 真正解决的并不是“创建文章文件很麻烦”这么简单的问题,而是把博客发文流程中的几个关键环节结构化了:
- 文章目录归类
- front matter 规范化
- 草稿与公开状态切换
- AI 编程环境复用
- Git 提交与推送串联
更重要的是,它明确了一个适用于很多仓库自动化场景的原则:
语义理解交给 Agent,确定性执行交给脚本。
一旦这条边界稳定下来,后续无论是继续接图片处理、封面生成、文章校验还是发布检查,都可以沿着同一条结构继续扩展,而不需要反复推翻底层设计。
10. 后续可以继续演进的方向
目前这套 blog-post skill 已经足够覆盖个人博客的日常发文流程,但它仍然有不少可以继续扩展的空间:
- 在创建文章前自动校验标签与分类是否符合既有约定
- 增加图片资源目录的自动创建与引用修正
- 在发布前执行本地预览或构建检查
- 将文章元数据进一步抽象为可复用 schema
如果把它放到更大的团队环境里,这套能力甚至可以继续演化成一个“内容仓库操作层”:前端页面、博客文章、知识库文档、发布说明,统一都通过结构化命令落库。
对个人博客来说,这一步或许还谈不上“平台化”,但它至少证明了一件事:只要先把职责边界拉清,自动化往往比想象中更容易做对,也更容易持续迭代。
许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。