1 分钟阅读

Content collections:让博客不出错的那道防线

类型化的前置元数据听起来像是多此一举,直到某天一个拼写错误让文章悄悄从列表里消失。

深色背景上的三张抽象文档卡片

有个故障模式我踩过不止一次:你写了一篇文章,页面渲染正常,但它从来没出现在列表里。原因只是前置元数据少了一个字符——date 写成了 published,或者 draft 拼错了。页面照样构建成功,索引查询返回空,一片安静。

Content collections 的做法是把前置元数据从「字符串魔法」变成真正的 schema,从而根治这个问题。

Schema

// src/content.config.ts
const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog/en' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    draft: z.boolean().default(false),
    tags: z.array(z.string()).default([]),
  }),
});

这样缺了 title 就会带着有用的报错信息构建失败,而不是产出一个标题为空的页面。z.coerce.date() 接受 2026-09-28 并返回一个真正的 Date,排序时不再需要任何猜测。

两种语言用两个集合

我把中英文拆成两个集合,而不是在一个集合里加 lang 字段。配置上多了点重复,页面里却省了很多事:

export const collections = {
  'blog-en': defineCollection({ /* ... */ }),
  'blog-zh': defineCollection({ /* ... */ }),
};

每种语言的索引只查自己的集合,不存在「忘了过滤」的可能,也不会因为某个人漏写判断条件,导致中文文章出现在英文列表里。

我往 schema 里放什么

只放模板真正会读取的字段。每加一个字段,就意味着每篇文章都要多填一项,而且这个字段还可能和页面实际用法脱节。title、description、date、draft、tags、cover——就这些。阅读时长和摘要都是从正文在构建时算出来的,不存。

我遵循的原则是:如果一个值可以从文章本身算出来,就不要让作者再敲一遍。

astromarkdown类型