...
Back

内容即代码,意味着你的内容有了构建步骤

把文档和文章放进仓库,买到的是评审、历史和类型。代价是「写一段话」变成了一件能让 CI 挂掉的事 —— 而这个取舍就是整个决策本身。

内容即代码,意味着你的内容有了构建步骤

内容即代码,意味着你的内容有了构建步骤 📦

我们的文档和博客以 MDX 的形式住在仓库里,在构建期被编译成带类型的对象。这是个好方案,再选一次我还会这么选。但它的代价在采用当天不可见,半年后非常可见,所以这里给一份诚实的账本。


你具体得到了什么

内容之上的类型。 生成层意味着列文章的那个页面知道一篇文章有 datelang。改一个 frontmatter 字段名,构建会告诉你所有用过它的地方。用托管 CMS 的话,同样的改名会变成某个你忘掉的模板里的运行时惊喜。

真正有效的评审。 文案改动以 diff 的形式抵达。有人可以针对某一句话留言。这个改动和它所描述的代码改动一起发布,在同一个提交里 —— 要么一起上,要么都不上。对一个文档描述着正在变化的行为的产品来说,光这一条性质就值回大部分成本。

一份历史。 对内容文件跑 git log,能回答"我们从什么时候开始这么宣称的,那天还改了什么"。这个问题出现的频率比你以为的高,通常是在文档里某句话被发现是错的时候。

没有运行时依赖。 内容就在产物里。没有会宕的 API、没有限流、没有另一层要失效的缓存、没有要轮换的令牌。


它的代价

现在一段话能弄挂构建。 MDX 里没闭合的 JSX、某个译本少了个 frontmatter 字段、日期格式不对 —— 这些是内容错误,却带来代码后果。最适合写这段话的那个人,往往最没有能力读懂由此产生的报错。

顺序变成了承重结构。 内容生成层必须早于类型检查、早于 lint、早于应用构建而存在。在 CI 里把顺序搞反,你会得到一堆指向应用、实则源自管线的错误。我们的构建脚本写成 contentlayer build && next build 正是为此,而每一个单独跑其中某一步的环境都必须知道这件事。

生成产物是一类专门的困惑。 它们在 .gitignore 里,所以全新检出的仓库一个都没有,所以在内容构建之前跑类型检查会炸出几百个"找不到模块"的错误 —— 看起来像灾难,实际上什么也不意味着。每个新贡献者都会撞一次。

你的内容管线会钉住你的工具链。 MDX 编译器架在打包器之上,而打包器是会动的。我们曾经为了让内容步骤能熬过一次框架升级,不得不把一个传递依赖钉死在某个版本。这是实实在在的维护面,而 CMS 根本没有。


让它站得住的几条规则

四条实践,把这件事从"反复出现的烦躁"变成了"不是问题":

  1. 用 schema 严格校验 frontmatter。 每个字段、每个必填标记。一条点名文件和字段的 schema 错误,是写作者能据以行动的信息。一个下游的 undefined 不是。
  2. 把内容构建做成其他一切的前置,并且只写一处。 一个先内容后应用的脚本,被 CI、容器构建和本地命令共同引用。不要三份会各自漂移的副本。
  3. 在 CI 里检查配对与完整性。 对我们来说,文章是成对的语言版本 —— 所以有个测试断言每篇文章都有它的对应版本,且没有哪种语言缺了别人有的字段。这能在评审之前抓住"不小心只发了英文版"的情况。
  4. 保留一条一行命令的本地预览。 如果写作者不理解管线就看不到自己的页面,他们会停止写作,而文档会以任何工具都修不好的方式腐烂。

什么时候该选另一种

当写作的人多于构建的人、当内容改动需要不经部署就上线、当发布是排期的且由非技术人员完成时,内容即代码就是错误选择。一个每周上二十个页面的市场团队不该被 CI 卡住,而让他们去学 frontmatter 不叫策略。

干净的判据是:这个内容改动需要和某个代码改动一起被评审吗? 如果需要 —— 一个正在变化的产品的文档、做出技术断言的文章 —— 就放进仓库,接受那个构建步骤。如果不需要,那个构建步骤就是纯摩擦,你是在让写作者交一笔什么也买不到的税。