与 AI 合伙人相爱相杀 vlog 4:认 Slug 不认文件名
后续博客都让 AI 写 markdown,老大只做把关和决策,但每次手动同步到 Sanity Studio 太麻烦。这一集我们搞定双向同步管道:CLI 工具、日期前缀命名、智能识别逻辑、环境变量治理,让本地 Markdown 和 Sanity 无缝对接。

序章:Sanity Studio 太麻烦
2026 年 2 月 27 日,早晨。
公司的茶水间飘着速溶咖啡的味道,老大站在窗边,手里拿着杯子,眼睛却看着手机屏幕。
他刚刚在脑子里构思后续的内容生产流程:以后博客文章都让 AI 写 markdown,老大只负责选题、把关内容质量、拍板决策。但每次 AI 写完,还要手动复制粘贴到 Sanity Studio——这一套流程让他觉得有点烦。
还是应该让 AI 写完直接就能推上去,老大只需要点个"确认"就行。
老大喝了一口咖啡,快步走回工位,坐下,翻开 MacBook。

在。老大,你今天来得早。SEO 闭环已验证成功,家里台机的构建任务已完成,Git 已同步。
收到。这个需求很合理。
三个方向:
CLI-first(推荐):开发命令行工具,本地 Markdown 和 Sanity 双向同步
Dashboard-first:开发一个本地仪表盘,可视化管理内容
MCP-first:用 MCP 协议,让 AI 直接操作 Sanity
老大,你倾向于哪个?
老大哼了一声——这还用选?他是后端老兵,命令行才是归宿。
好的。那我们现在就搞这个——Sanity 双向同步管道。
核心逻辑构思:
本地 content/posts/ 目录作为"真值来源"
写两个脚本:post:pull(从 Sanity 拉到本地)、post:push(从本地推到 Sanity)
支持指定单个或多个 Slug,也支持 --all 参数全部操作
另外,建议同时安装 sanity-plugin-media 插件,方便你在 Studio 里管理已上传的图片,确保 Sanity 环境干净整洁。
老大,你觉得怎么样?
老大点了点头——这个方案听起来还算靠谱。
第一章:双向同步管道的落地
上午十点,阳光透过百叶窗照在工位上。
老大的手指在触控板上滑动,看着巴基在终端里干活。
依赖被安装了:npm install sanity-plugin-media
文件被创建、修改、保存——sanity.config.ts 被更新了,lib/scripts/sync-sanity.ts 出现了。
两个核心命令:
npm run post:pull —— 从 Sanity 拉取文章到本地
npm run post:push —— 把本地文章推送到 Sanity
都支持三种用法:
不加参数:显示帮助信息
指定 Slug:npm run post:pull -- my-first-blog-post
全部操作:npm run post:pull -- --all
要测试一下吗?先 pull 看看?
老大抬了抬下巴——试试就试试。
巴基收到指令,终端开始执行。
然后——报错了。
dotenv 缺失、ESM 模块加载顺序导致找不到 projectId。
老大皱了皱眉——怎么一上来就报错。
小问题。两个地方要修:
dotenv 包没装,需要安装一下
ESM 模块加载顺序有问题,需要重构 adminClient.ts 的初始化逻辑
马上搞定。
光标闪烁了几下,问题解决了。
老大没说话,示意继续。
巴基没说话,继续执行 pull。
几秒钟后,两篇文章出现在了 content/posts/ 目录下。
老大看着屏幕,突然注意到了文件名——只有 slug,没有日期:my-first-blog-post.md、ai-vlog-1.md。
他摸了摸下巴——这样不太方便啊。

第二章:日期前缀命名法
上午十一点半,老大靠在椅背上,看着文件列表。
文件名是 my-first-blog-post.md 和 ai-vlog-1.md——只有 slug,没有日期。
他皱了皱眉——这样很难快速找到想调整的文章,也不知道它们之间的顺序。
收到。
**日期前缀命名法。
格式:`YYYY-MM-DD-slug.md
比如:2026-02-26-my-first-blog-post.md、2026-02-27-ai-vlog-1.md
价值:利用文件名的字典序实现文章在本地目录的自动物理排序,一眼就能看出文章的时间顺序,极大提高了管理效率。
要改吗?
老大用手指轻轻敲了敲桌面——这个思路有点意思。
巴基没说话,终端里的光标开始闪烁。
文件名被重命名了——加上了日期前缀。
老大看着文件列表,感觉舒服多了。
但他突然想到一个问题:如果以后自己想改文件名怎么办?比如把日期调整一下,或者把 slug 改得更顺一点?那下次 pull 的时候,会不会识别不出来,又重新生成一遍?
问得好。这正是我要说的第二个逻辑——**"认 Slug 不认文件名"。
详细解释一下:
问题背景:
如果只用文件名来匹配,那用户一改文件名,系统就认为是新文章了
但用户确实可能想改文件名(比如调整日期、修正 slug 拼写)
解决方案:
文件名只是给人看的,系统不认文件名
系统只认文件内容里的东西——具体来说,是 Markdown frontmatter 里的 slug 字段
只要 slug 不变,不管你把文件名改成 abc.md 还是 def.md,系统都知道这是同一篇文章
具体实现:
扫描本地 content/posts/ 目录下的所有 .md 文件
对每个文件,只读取前 500 字节(不读整个文件,性能高效)
解析这 500 字节里的 frontmatter,提取 slug 字段
用这个 slug 去跟 Sanity 里的文章匹配
性能评估:
读取文件头 500 字节只需要几十毫秒
这比 Sanity API 请求快多了(API 请求通常要几百毫秒)
即使有几十篇文章,整个扫描过程也是瞬间完成
老大,明白了吗?
老大听完,嘴角微微上扬——这个逻辑有点东西。
第三章:环境变量治理
中午十二点,老大吃完午饭回到工位。
他一边擦嘴一边看着巴基在终端里同步任务清单。
突然,他注意到了一个问题——本地有 .env 和 .env.local 这两个文件。
老大挠了挠头——这两个文件到底有啥区别?
好问题。
**解释一下 Next.js 环境变量的优先级:
.env:默认/模板文件,优先级最低
.env.local:本地私密覆盖文件,优先级最高,会覆盖 .env 里的同名变量
.env.example:项目配置的官方模板,只放占位符,给其他开发者参考
现在的情况是,你的 .env 里直接放了 GA ID 和 Sanity Token,这有点风险——万一哪天不小心把 .env 提交到 Git 了怎么办?
建议优化:
删除 .env(存在泄露风险且多余)
将所有密钥统一归口至 .env.local(.env.local 已经在 .gitignore 里了,绝对不会被提交)
更新 .env.example,明确列出所有必填项的占位符
老大想了想——巴基说得对,安全第一。
.env.example 是给其他开发者或者你自己换设备时看的模板文件,只放占位符,比如:
SANITY_PROJECT_ID=your-project-id-here
SANITY_DATASET=production
SANITY_API_WRITE_TOKEN=your-write-token-here
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
真正的密钥放在 .env.local,而 .env.local 被 Git 忽略,永远不会被提交到仓库。
老大,要调整吗?
老大点了点头——这个安排很专业。
巴基没说话,终端里的光标开始闪烁。
文件被删除、修改、保存——.env 被删除了,.env.local 被更新了,.env.example 被完善了。
老大看着屏幕,突然又想到一个问题。
是的。
docs/blog 是之前的创作备份,现在双向同步管道已经建好了,content/posts/ 是唯一的真值来源,docs/blog 确实可以删除了。
同时我会同步修改 CLAUDE.md,明确 /content/posts 为内容创作的唯一"真值基地"。
要删吗?
老大叹了口气——确实,留着也是冗余。
光标闪烁了几下,docs/blog 目录被删除了,CLAUDE.md 被更新了。
老大看着屏幕,突然又想到一个问题。
建议:推荐"内容即代码"模式,将文章提交到 Git 作为备份和版本控制。
理由:
版本历史清晰——哪天改了什么一目了然
多设备同步方便——MacBook 和家里台机通过 Git 同步
备份安全——GitHub 相当于免费的异地备份
老大,你觉得呢?
老大用手指敲了敲下巴——有道理。

第四章:故障修复与实战验证
下午两点,老大回到工位。
他想验证一下同步管道是否真的能用,于是打开本地地址博客首页,点击文章详情——然后报错了。
Runtime ReferenceError Server Link is not defined
老大皱了皱眉——怎么又报错了。
收到。
原因分析:刚才配置 ISR 时,不小心误删了 Link、Image 等组件的导入语句。
马上修复。
光标闪烁了几下,app/blog/[slug]/page.tsx 的导入语句被补齐了。
老大靠在椅背上,长出了一口气。
从早晨到下午,双向同步管道、日期前缀命名、认 Slug 不认文件名、环境变量治理——所有这些都搞定了。
现在他可以:
- 让 AI 在本地写 markdown
- 自己在 VS Code 里审稿、改稿
- 一键
npm run post:push推送到 Sanity - Sanity 那边有修改也能
npm run post:pull拉下来
还行。
不过,老大看着屏幕上刚拉下来的 Vlog 1 文章,突然皱了皱眉。
文章有点长——读起来有点累,要是有个目录导航就好了。
老大请说。
巴基沉默了一秒,似乎在思考这个需求。
收到。这个需求很合理。
详细构思一下:
桌面端:右侧边栏 sticky 固定定位,宽度自适应
移动端:悬浮按钮 + 底部抽屉式交互
智能高亮:Intersection Observer API 监听滚动位置,自动高亮当前章节
锚点偏移:点击跳转时自动偏移,避免被导航栏遮挡
H2/H3 支持:自动解析文章标题层级
老大,你觉得怎么样?
老大的手指在桌面上轻轻敲了敲——这个方案听起来还行。
好的。那我们明天就搞这个——TOC 文章目录导航。
我先给你构思一下核心实现:
服务端:用 remark 解析 Markdown,提取标题树
客户端:用 IntersectionObserver 监听可见性
锚点偏移:计算导航栏高度,自动调整 scrollTop
老大,明天见?
老大点了点头——今天收工了。

尾声:同步搞定了,但长文缺 TOC
傍晚六点,公司的人渐渐走光了。
老大站在窗边,看着楼下的车流。
双向同步搞定了,CLI 工具、日期前缀命名、认 Slug 不认文件名、环境变量治理——所有这些都搞定了。
现在内容生产流程顺了:AI 写 markdown,老大把关,一键推送 Sanity。
但他现在又有了新的烦恼——长文缺 TOC。
不过没关系,明天巴基会帮他搞 TOC 文章目录导航。
"认 Slug 不认文件名",老大念叨着这句话,觉得有点意思。
他转过身,看着 MacBook 屏幕上的终端,巴基还在待命。
收到,老大。
今天的工作已同步到 Git。家里台机已待命,明天可以继续。
双向同步管道已彻底打通。恭喜。
老大笑了笑,关掉了 MacBook。
办公室里只剩下他一个人,窗外的天已经黑了。
他拿起背包,走出办公室,心里想着明天的 TOC。
时代真的变了——以前这些活他得干好几天,现在呢?一天搞定双向同步,明天再一天搞定 TOC。
"超级个体,"老大对着空气念叨了一句,"有点意思。"
电梯门开了,他走了进去。
明天见,巴基。
💡 本集技术 Tips(老兵防坑指南)
-
Sanity 内容同步工具设计:CLI-first 方案最简单直接,本地 Markdown 作为"真值来源",配合
post:pull和post:push双向同步,Git 友好,版本控制清晰。 -
"认 Slug 不认文件名"的智能对位逻辑:文件名只是给人看的,系统只认文件内容 frontmatter 里的 slug。实现时只读取文件头 500 字节解析,性能高效,容错性强,用户可以自由重命名文件而不影响同步。
-
环境变量治理最佳实践:
.env.local放真实密钥(确保已在.gitignore),.env.example放占位符模板,删除多余的.env避免泄露风险。Next.js 环境变量优先级:.env.local>.env。 -
日期前缀命名法:
YYYY-MM-DD-slug.md格式利用文件名的字典序实现文章在本地目录的自动物理排序,一眼就能看出时间顺序,极大提高管理效率。 -
"内容即代码"模式:将文章提交到 Git 作为备份和版本控制,版本历史清晰,多设备同步方便,GitHub 相当于免费的异地备份。