我这个博客有 127 篇文章,每篇开头都有这么一坨:
---
title: '文章标题'
description: '一两句话摘要'
pubDate: '2026-09-11'
planted: '2026-09-11'
category: '技术'
growth: bud
tags: ['Astro', '写作规范']
type: 'case-study'
tldr: |
...
readNext: ['slug-a', 'slug-b']
---常用的字段有十八个。我写过一篇 云图札记博文写作规范 专门记它们,但说实话,我自己也记不全。
真正的麻烦不是记不住
字段多可以查文档。真正让人烦的是另外几件事。
写错了不报错。 readNext 里 slug 打错一个字母,构建照常通过,页面照常生成——只是那个「继续阅读」的位置空了一块。等你发现,可能是三个月后。
预览和线上不一样。 我在编辑器里看到的 callout 样式,和博客渲染出来的不是一回事。因为博客挂了一串自己的 remark/rehype 插件,编辑器当然不认识。
发一篇文章要走完一整套流程。 写正文、填字段、图片转成 WebP 放到对的位置、正文里把路径改写成 /img/xxx.webp、保存、切终端、git add、git commit、git push。
想写一句话,得先穿过这套仪式。
内容仓库改造记 那次我解决了仓库层面的分家,但写作这一侧的摩擦一点没少。于是有了 CloakWriter。
它长什么样


一个三栏的桌面程序:左边文章列表,中间是编辑器和一个 frontmatter 表单,右边是预览、检查、关系面板。
技术栈是 Go + Wails + Svelte 5,编辑器用 CodeMirror 6,索引用 SQLite(纯 Go 驱动,Windows 上不用装 MinGW)。选 Go + Wails 而不是 Electron,主要是受不了 Electron 那个体积和启动速度——现在整个程序 23MB。
功能是围绕「把那套仪式消掉」来堆的:
- 十八个字段变成表单,下拉选分类、日期选择器、slug 实时校验有没有被占用
- 图片拖进来自动转 WebP、落到工作区、正文自动写
/img/xxx.webp - 中文标题自动转拼音 slug
- 写完点「发布」,同步、提交、推送一次做完
- 全站体检:断图、孤儿文章、系列错误、只用了一次的标签,给个 0–100 的评分
几个我反复权衡的决定
功能堆起来不难,难的是几个地方到底怎么做。这几个是我改过主意、或者一开始就想清楚了的。
预览不自己写,直接复用博客的渲染链
这是整个项目我最满意的一个决定。
常规做法是编辑器里再实现一遍 Markdown 渲染。问题是博客那套插件链会变——我加个插件、换个代码高亮主题,编辑器里的预览就过期了,而我大概率不会记得同步。
所以我让预览直接调用博客自己的渲染器:Node 子进程启动时动态读取博客的 astro.config.mjs,拿到 Astro 已经烘焙好的 markdown.processor,用它渲染。这个 processor 里已经包含了完整的 remark/rehype 插件链、shiki 主题和语言别名——和 astro build 用的是同一个东西。
带来的好处是:博客改插件、改主题,预览自动跟着变。我不需要在编辑器里维护一份注定会过期的拷贝。
代价是引入了一个 Node 常驻子进程,以及随之而来的一堆麻烦(下面会讲到)。但我认为这笔交易划算——预览不准的编辑器,比没有预览更糟糕,因为它会让你以为没问题。
写作和发布必须分开
编辑器只读写程序自带的「工作区」,不碰博客目录。要上线了,点「发布」,把工作区同步到配置好的内容仓库。
这个分离解决的是一个很具体的恐惧:我不希望一个写作工具能直接改我线上的文章。中间隔一层,误操作的代价就小得多。
发布是单向同步,不删文件。目标目录里存在、但工作区没有的文件,一律保留。这个决定让同步逻辑复杂了一点,但它保证了一件事:无论我怎么折腾,都不会因为一次误操作删掉几篇文章。
首次运行时如果工作区是空的,程序会把内容仓库里已有的文章复制进来导入——不删原文件,保留原有的「分类/年份」结构。
发布目标必须是内容仓库,不是博客源码仓库
这点跟 拆仓那次 直接相关:文章在独立的 blog-Content 仓库里,public/img 和 src/content/blog 都只是构建产物。往那里写不会上线。
所以发布目标指向 blog-Content,git 推送也落在它上面——推上去就触发 Cloudflare Pages 重建。
保存之前,先拿全站文章做一遍字节级校验
这是安全底线,也是我花力气最多的地方之一。
一个能写文件的工具,最可怕的不是功能少,是它会悄悄改坏你的东西。Frontmatter 的解析和序列化要处理一堆边界情况:未知字段、CRLF 换行、多行文本缩进、引号风格……任何一个处理不当,保存一次就可能在 99 篇文章里留下细微的 diff。
所以我在写入博客源之前加了一道关:对全站每一篇文章做「读 → 序列化 → 解析 → 再序列化」的往返校验,两次结果字节必须完全一致才允许保存。测试里跑的是真实的全部文章,不是造的样例。
这道关挡住过好几次会污染全站的问题。它的价值不在日常——日常它什么都不做——在于它让我敢让这个工具去写我的文章。
找不到 Node 就降级,不崩溃
预览依赖 Node,而 Node 不是写作的前置条件。
所以程序会主动探测 Node:配置指定的 → PATH → 常见安装位置(Program Files / nvm / volta / fnm / scoop)→ 兜底。GUI 进程的 PATH 往往只有系统级条目,nvm 那些通常只写进了交互式 shell,不主动找是找不到的。
找到之后还要验版本。低于 22.12.0 时 Astro 7 的渲染 API 在旧版 Node 上会直接崩——这种情况我选择降级为无预览并明确提示原因,而不是让你看到一个坏掉的预览区,以为程序坏了。
都找不到的时候,写作、保存、搜索、双链、发布全部照常,只有预览显示「暂不可用 + 具体原因」。
踩到的坑
Node worker 漏回一个包,调用方永久阻塞
Node 子进程用 JSON-lines 通信,Go 侧按行同步读取。这意味着任何请求都必须有一条应答——脚本里某个分支漏了回复,Go 侧就会永远卡在那一行读上。
我是在一次崩溃自愈的排查里发现的:某个异常分支没有回包。修完之后把协议约定写死了:处理逻辑整体包在 try/catch 里,且 Go 侧发送的 args 永不为 null(null 会让 JS 的参数默认值失效,是个很隐蔽的坑)。
现在 worker 有崩溃自愈:传输层失败判定为进程已坏 → 重启 → 自动重试一次;业务层错误不重启,直接透传。
控制台黑框
Go 是 GUI 程序,但它调的 node.exe 和 git.exe 都是控制台程序。不处理的话每次调用都会闪一个黑框,写作时非常干扰。
Windows 上用 CREATE_NO_WINDOW 解决。但黑框消失后 stderr 也看不见了,所以把 node 的 stderr 收进了 cloakwriter.log,出错时还能查。
tools/render.mjs 必须和 exe 同目录
wails build 只产出可执行文件,不会自动带上这个渲染脚本。缺了它程序能启动,但预览会静默降级——一个很难排查的坑。所以打包脚本 build.bat 会手动复制,CI 里也专门为 macOS 往 CloakWriter.app/Contents/MacOS/tools/ 放了一份。
现在的状态
0.1.0,两天,22 次提交。开源在 GitHub,推 tag 会自动打包 Windows 和 macOS 的安装包。
它还很粗糙:只服务我这个博客的字段规范,换个人用大概得改一堆东西;有些交互我自己知道别扭但还没动。
我做它的方法跟 我的 AI 开发工作流:从想法到上线 里写的一样——先把想清楚的部分落成能跑的东西,剩下的边用边补。这种速度下,一个「只给自己用的工具」反而是最容易做好的:不用猜别人要什么,每个决定只需要说服我自己。
本地优先架构在个人项目中的实践 里我说过,自己的数据放在自己能摸到的地方,心里才踏实。这个写作台把这条又往前推了一步:文章是本地文件,索引是本地 SQLite,预览调用的是本地渲染链,发布是显式的一次点击。
它不联网也能用——除了最后那一下推送。