搜索博客与维基

技术/写作

用内容集合给 Markdown 加类型检查

字段写错不该等到上线才发现。Astro 的内容集合会在构建阶段校验每篇文章顶部的元数据。

写博客最容易出的问题不是正文写错,而是顶部那几行元数据写错:pubDate 写成了 pubdate,tags 忘了加逗号,分类写成了数组又写了字符串。这些错如果不管,文章会静默地消失或者排到错误的位置。

内容集合(Content Collections)就是干这个的:给每个 Markdown 文件顶部的字段定一套规则,然后在构建时逐个校验。

一个字段怎么定义

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    category: categoryPath,
    tags: tagList,
    draft: z.boolean().default(false),
  }),
});

拆开看:

  • loader 告诉 Astro 去哪找文件。
  • schema 用 zod 描述每个字段应该是什么类型。
  • z.coerce.date() 的意思是「不管写的是日期对象还是字符串,都帮我转成日期」。这样前置元数据里写 2025-11-02 就够了。
  • .default(false) 的意思是「没写的话就是 false」,不用每篇都填。

让一种字段接受两种写法

分类我希望能这么写 category: 技术/前端/构建,也能这么写 category: [技术, 前端, 构建]。做法是先用联合类型接住两种输入,再统一转换成字符串数组:

const categoryPath = z
  .union([z.string(), z.array(z.string())])
  .optional()
  .transform((value) => {
    if (!value) return [] as string[];
    const parts = Array.isArray(value) ? value : value.split('/');
    return parts.map((p) => p.trim()).filter(Boolean);
  });

transform 是关键:校验完之后顺手换成程序里真正好用的形状。所以页面代码里拿到的永远是 string[],不用每个地方都判断一次。

写错了会怎样

把 pubDate 故意拼错,构建会直接失败,并指出是哪个文件的哪个字段:

[InvalidContentEntryDataError] blog → hello-world data does not match collection schema.
  pubdate: Unrecognized key

这就是想要的效果:在构建阶段报错,而不是在浏览器里安静地少一篇文章。

顺手的好处

因为字段有类型,编辑器里敲 post.data. 会自动补出所有可用字段。不用翻文档确认字段名,也不怕记错。

查询和排序

const posts = sortByDate(
  (await getCollection('blog')).filter(isPublished),
);

两个要点:

  • getCollection 返回的是整个集合,筛选和排序都发生在内存里。文章量到几千篇之前不用担心性能。
  • isPublished 只在正式构建时过滤草稿。开发模式下草稿也能看到,方便边写边看。

踩过的一个小坑

维基和博客的字段不一样:维基不需要 pubDate(它关心的是「现在写得对不对」,不是「什么时候写的」)。一开始我在维基卡片里写了 entry.data.updated ?? entry.data.pubDate,构建直接报错说 pubDate 不存在。

这个错报得非常好,因为如果 schema 是宽松的,这行代码会在运行时永远取到 undefined,然后日期位置显示空白,你还得去查为什么。

判断一个方案值不值得用

标准很简单:它能不能把错误提前到「你还记得自己在干什么」的时候。能,就值得。