Markdown 的 Frontmatter 是一段自由文本。这既是它好用的原因,也是它出问题的原因。
我遇到过这些:
- 日期写成
2026-7-2,某个解析器认,某个不认 - 标签写成
tags: astro,另一个页面当成字符串遍历,一个字符一篇文章 - 忘了写
description,页面上出现一个空白的摘要框
这三个问题都在浏览器里才暴露,而且其中两个不会报错,只是安静地显示错内容。
Schema 能拦下什么
Astro 的内容集合允许给每一类内容定义 Schema,用的是 Zod。写一次,之后所有文章都要过这个闸门:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).default([]),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
几个细节值得说。
z.coerce.date() 而不是 z.date()。 Frontmatter 里的值从 YAML 出来是字符串或 Date 对象,取决于引号怎么写。coerce 让两种写法都收敛成 Date,我在页面里就不用再判断类型。
.default([]) 而不是 .optional()。 可选意味着页面里每次用 tags 都要写 post.data.tags ?? []。给一个默认值,代码里就没有分支了。
不加 .optional() 的字段就是必填。 少写 description 会让构建直接失败,而不是渲染出一个空框。
失败的时机很关键
这是整件事的重点。
一个内容错误,在三个地方可能被发现:
| 时机 | 发现者 | 代价 |
|---|---|---|
| 浏览器渲染时 | 读者 | 最高 |
| 构建时 | CI | 中 |
| 写的时候 | 编辑器 | 最低 |
Schema 把校验点从「浏览器渲染时」提前到「构建时」。如果再配合编辑器插件,还能提前到「写的时候」——在 Frontmatter 里就能看到字段是否合法。
把校验往前挪,是工程上性价比最高的一类改动。它不增加任何运行时成本,只是让错误更早地撞到墙上。
一个副作用
定义 Schema 之后,我写文章时会先想清楚这篇的元信息——标题、摘要、标签分别是什么。
这个思考过程本来应该发生,只是以前没有任何东西强迫我发生。
全文完