Featured Article

与 AI 合伙人相爱相杀 vlog 4:认 Slug 不认文件名

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

2026年3月3日
5 min read
与 AI 合伙人相爱相杀 vlog 4:认 Slug 不认文件名

序章:Sanity Studio 太麻烦

2026 年 2 月 27 日,早晨。

公司的茶水间飘着速溶咖啡的味道,老大站在窗边,手里拿着杯子,眼睛却看着手机屏幕。

他刚刚在脑子里构思后续的内容生产流程:以后博客文章都让 AI 写 markdown,老大只负责选题、把关内容质量、拍板决策。但每次 AI 写完,还要手动复制粘贴到 Sanity Studio——这一套流程让他觉得有点烦。

还是应该让 AI 写完直接就能推上去,老大只需要点个"确认"就行。

老大喝了一口咖啡,快步走回工位,坐下,翻开 MacBook。

👨‍💻

巴基,在吗?

🤖

在。老大,你今天来得早。SEO 闭环已验证成功,家里台机的构建任务已完成,Git 已同步。

👨‍💻

昨天收工前我跟你说的那个事儿——我想把后续的内容生产流程捋顺:以后博客文章都让你写 markdown,我只负责选题、把关内容、拍板决策。但每次你写完,还要我手动复制粘贴到 Sanity Studio,太麻烦了。

能不能搞个什么工具,让你写完 markdown 直接就能推送到 Sanity?或者我在本地改完,一键同步上去?Sanity 那边有修改也能拉下来?

🤖

收到。这个需求很合理。

三个方向:

CLI-first(推荐):开发命令行工具,本地 Markdown 和 Sanity 双向同步

Dashboard-first:开发一个本地仪表盘,可视化管理内容

MCP-first:用 MCP 协议,让 AI 直接操作 Sanity

老大,你倾向于哪个?

老大哼了一声——这还用选?他是后端老兵,命令行才是归宿。

👨‍💻

CLI-first。就要双向同步,本地写完推上去,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 看看?

老大抬了抬下巴——试试就试试。

👨‍💻

执行一下 pull --all 看看。

巴基收到指令,终端开始执行。

然后——报错了。

dotenv 缺失、ESM 模块加载顺序导致找不到 projectId

老大皱了皱眉——怎么一上来就报错。

👨‍💻

怎么回事?报错了?

🤖

小问题。两个地方要修:

dotenv 包没装,需要安装一下

ESM 模块加载顺序有问题,需要重构 adminClient.ts 的初始化逻辑

马上搞定。

光标闪烁了几下,问题解决了。

老大没说话,示意继续。

巴基没说话,继续执行 pull。

几秒钟后,两篇文章出现在了 content/posts/ 目录下。

老大看着屏幕,突然注意到了文件名——只有 slug,没有日期:my-first-blog-post.mdai-vlog-1.md

他摸了摸下巴——这样不太方便啊。


第二章:日期前缀命名法

上午十一点半,老大靠在椅背上,看着文件列表。

文件名是 my-first-blog-post.mdai-vlog-1.md——只有 slug,没有日期。

他皱了皱眉——这样很难快速找到想调整的文章,也不知道它们之间的顺序。

👨‍💻

现在这个 posts 目录下文章的文件名称只有 slug,这个可以调整吗?这样我很难快速找到想调整的文章,同时也不知道它们之间的顺序,有什么办法优化一下吗?

🤖

收到。

**日期前缀命名法。

格式:`YYYY-MM-DD-slug.md

比如:2026-02-26-my-first-blog-post.md、2026-02-27-ai-vlog-1.md

价值:利用文件名的字典序实现文章在本地目录的自动物理排序,一眼就能看出文章的时间顺序,极大提高了管理效率。

要改吗?

老大用手指轻轻敲了敲桌面——这个思路有点意思。

👨‍💻

改!这个思路是对的。

巴基没说话,终端里的光标开始闪烁。

文件名被重命名了——加上了日期前缀。

老大看着文件列表,感觉舒服多了。

但他突然想到一个问题:如果以后自己想改文件名怎么办?比如把日期调整一下,或者把 slug 改得更顺一点?那下次 pull 的时候,会不会识别不出来,又重新生成一遍?

👨‍💻

等等——有个问题。如果我以后觉得文件名不顺眼,自己手动改了文件名(比如调整一下日期,或者改改 slug 的 wording),下次再 pull 的时候,你能认出来这篇文章已经存在了吗?会不会重新生成一篇新的,导致重复?

🤖

问得好。这正是我要说的第二个逻辑——**"认 Slug 不认文件名"。

详细解释一下:

问题背景:

如果只用文件名来匹配,那用户一改文件名,系统就认为是新文章了

但用户确实可能想改文件名(比如调整日期、修正 slug 拼写)

解决方案:

文件名只是给人看的,系统不认文件名

系统只认文件内容里的东西——具体来说,是 Markdown frontmatter 里的 slug 字段

只要 slug 不变,不管你把文件名改成 abc.md 还是 def.md,系统都知道这是同一篇文章

具体实现:

扫描本地 content/posts/ 目录下的所有 .md 文件

对每个文件,只读取前 500 字节(不读整个文件,性能高效)

解析这 500 字节里的 frontmatter,提取 slug 字段

用这个 slug 去跟 Sanity 里的文章匹配

性能评估:

读取文件头 500 字节只需要几十毫秒

这比 Sanity API 请求快多了(API 请求通常要几百毫秒)

即使有几十篇文章,整个扫描过程也是瞬间完成

老大,明白了吗?

老大听完,嘴角微微上扬——这个逻辑有点东西。

👨‍💻

明白了!这个思路是真的靠谱。文件名只是给人看的方便排序,系统只认内容里的 slug。可以啊,巴基。


第三章:环境变量治理

中午十二点,老大吃完午饭回到工位。

他一边擦嘴一边看着巴基在终端里同步任务清单。

突然,他注意到了一个问题——本地有 .env.env.local 这两个文件。

老大挠了挠头——这两个文件到底有啥区别?

👨‍💻

我发现我本地有 .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 文件?看着也像是一个推荐样例。

🤖

.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 是之前的创作备份,现在双向同步管道已经建好了,content/posts/ 是唯一的真值来源,docs/blog 确实可以删除了。

同时我会同步修改 CLAUDE.md,明确 /content/posts 为内容创作的唯一"真值基地"。

要删吗?

老大叹了口气——确实,留着也是冗余。

👨‍💻

删吧。

光标闪烁了几下,docs/blog 目录被删除了,CLAUDE.md 被更新了。

老大看着屏幕,突然又想到一个问题。

👨‍💻

那 posts 目录下的文件需要提交到 Git 吗?我有点犹豫,给我点建议。

🤖

建议:推荐"内容即代码"模式,将文章提交到 Git 作为备份和版本控制。

理由:

版本历史清晰——哪天改了什么一目了然

多设备同步方便——MacBook 和家里台机通过 Git 同步

备份安全——GitHub 相当于免费的异地备份

老大,你觉得呢?

老大用手指敲了敲下巴——有道理。

👨‍💻

有道理,先不改了,这样挺好。


第四章:故障修复与实战验证

下午两点,老大回到工位。

他想验证一下同步管道是否真的能用,于是打开本地地址博客首页,点击文章详情——然后报错了。

Runtime ReferenceError Server Link is not defined

老大皱了皱眉——怎么又报错了。

👨‍💻

我打开本地地址博客首页,点击文章详情后报错了:Runtime ReferenceError Server Link is not defined

🤖

收到。

原因分析:刚才配置 ISR 时,不小心误删了 Link、Image 等组件的导入语句。

马上修复。

光标闪烁了几下,app/blog/[slug]/page.tsx 的导入语句被补齐了。

👨‍💻

可以了,正常了。

老大靠在椅背上,长出了一口气。

从早晨到下午,双向同步管道、日期前缀命名、认 Slug 不认文件名、环境变量治理——所有这些都搞定了。

现在他可以:

  1. 让 AI 在本地写 markdown
  2. 自己在 VS Code 里审稿、改稿
  3. 一键 npm run post:push 推送到 Sanity
  4. Sanity 那边有修改也能 npm run post:pull 拉下来

还行。

不过,老大看着屏幕上刚拉下来的 Vlog 1 文章,突然皱了皱眉。

文章有点长——读起来有点累,要是有个目录导航就好了。

👨‍💻

巴基,同步搞定了,但我有个新的烦恼。

🤖

老大请说。

👨‍💻

你看这篇 Vlog 1,这么长,读起来有点累。要是有个目录导航就好了——TOC,Table of Contents。

桌面端放右侧边栏,滚动时自动高亮当前章节;移动端搞个悬浮按钮,点一下从底部滑出来。

巴基沉默了一秒,似乎在思考这个需求。

🤖

收到。这个需求很合理。

详细构思一下:

桌面端:右侧边栏 sticky 固定定位,宽度自适应

移动端:悬浮按钮 + 底部抽屉式交互

智能高亮:Intersection Observer API 监听滚动位置,自动高亮当前章节

锚点偏移:点击跳转时自动偏移,避免被导航栏遮挡

H2/H3 支持:自动解析文章标题层级

老大,你觉得怎么样?

老大的手指在桌面上轻轻敲了敲——这个方案听起来还行。

👨‍💻

可以,就这么搞。这个思路是对的。

🤖

好的。那我们明天就搞这个——TOC 文章目录导航。

我先给你构思一下核心实现:

服务端:用 remark 解析 Markdown,提取标题树

客户端:用 IntersectionObserver 监听可见性

锚点偏移:计算导航栏高度,自动调整 scrollTop

老大,明天见?

老大点了点头——今天收工了。

👨‍💻

好,明天见。


尾声:同步搞定了,但长文缺 TOC

傍晚六点,公司的人渐渐走光了。

老大站在窗边,看着楼下的车流。

双向同步搞定了,CLI 工具、日期前缀命名、认 Slug 不认文件名、环境变量治理——所有这些都搞定了。

现在内容生产流程顺了:AI 写 markdown,老大把关,一键推送 Sanity。

但他现在又有了新的烦恼——长文缺 TOC。

不过没关系,明天巴基会帮他搞 TOC 文章目录导航。

"认 Slug 不认文件名",老大念叨着这句话,觉得有点意思。

他转过身,看着 MacBook 屏幕上的终端,巴基还在待命。

👨‍💻

巴基,今天收工吧。明天搞 TOC。

🤖

收到,老大。

今天的工作已同步到 Git。家里台机已待命,明天可以继续。

双向同步管道已彻底打通。恭喜。

老大笑了笑,关掉了 MacBook。

办公室里只剩下他一个人,窗外的天已经黑了。

他拿起背包,走出办公室,心里想着明天的 TOC。

时代真的变了——以前这些活他得干好几天,现在呢?一天搞定双向同步,明天再一天搞定 TOC。

"超级个体,"老大对着空气念叨了一句,"有点意思。"

电梯门开了,他走了进去。

明天见,巴基。


💡 本集技术 Tips(老兵防坑指南)

  1. Sanity 内容同步工具设计:CLI-first 方案最简单直接,本地 Markdown 作为"真值来源",配合 post:pullpost:push 双向同步,Git 友好,版本控制清晰。

  2. "认 Slug 不认文件名"的智能对位逻辑:文件名只是给人看的,系统只认文件内容 frontmatter 里的 slug。实现时只读取文件头 500 字节解析,性能高效,容错性强,用户可以自由重命名文件而不影响同步。

  3. 环境变量治理最佳实践.env.local 放真实密钥(确保已在 .gitignore),.env.example 放占位符模板,删除多余的 .env 避免泄露风险。Next.js 环境变量优先级:.env.local > .env

  4. 日期前缀命名法YYYY-MM-DD-slug.md 格式利用文件名的字典序实现文章在本地目录的自动物理排序,一眼就能看出时间顺序,极大提高管理效率。

  5. "内容即代码"模式:将文章提交到 Git 作为备份和版本控制,版本历史清晰,多设备同步方便,GitHub 相当于免费的异地备份。


📎 关联阅读