用 Markdown 管内容是件很舒服的事:纯文本、能进 Git、随手就能改。但它有个不太明显的问题——frontmatter 里的字段是没人管的。
问题长什么样
假设每个作品文件都要写标题、标签、作品地址。半年后你新增一个文件,字段名写成了 taglines(多了个 s),或者忘了写 demo。
编辑器不会报错,Git 也不会。它会一路通过构建,直到线上某个页面上那块内容是空的——或者更糟,构建时抛出一个看不懂的 undefined 错误,而你得花时间去找是哪个文件。
Markdown 的灵活性,在这里正好变成了它的弱点。
schema 把它变成构建期错误
内容集合的核心价值,是把这些字段声明成有约束的数据结构:
const works = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/works' }),
schema: z.object({
title: z.string(),
tagline: z.string(),
status: z.enum(['live', 'wip', 'archived']).default('live'),
demo: z.object({
embed: z.string().optional(),
interactive: z.boolean().default(false),
}).default({}),
}),
});
带来的变化很直接:
- 字段名写错 → 构建直接失败,不用等上线
- 忘了写必填字段 → 同上
status写成了published→ 立刻报错,因为只允许三个枚举值- 可选字段没写 → 自动拿默认值,模板里不用到处判空
把运行时的问题提前到构建期,这是它最大的价值。构建失败会立刻被你看到,线上空白不会。
一个反直觉的点
加 schema 会让“新增一个作品”变慢吗?
恰恰相反。没有 schema 时,你需要记住每个字段叫什么、哪些必填、格式是什么——这些约束只存在于你脑子里,或者散落在某个文档里。
有了 schema,约束写在代码里,编辑器会直接提示。新增文件时照着补全就行,比翻文档快。
顺带解决的问题:类型
定义 schema 之后,读取内容的代码会自动获得类型推导。getCollection('works') 拿到的每一项,data.title 是 string,data.status 是那三个字面量之一。
这意味着模板里写 data.demo.interactive 会有补全和类型检查,而不是盲猜。
对一个会长期维护的个人站来说,这些约束加起来的价值,远超过定义 schema 所花的那点时间。