← 全部文章

把内容规范交给类型系统

Frontmatter 最大的问题是它什么都能装。用 Schema 把可接受的形状写死,让错误的文章在构建时就失败,而不是在页面上显示出 undefined。

一只特立独行的熊猫 · · 约 2 分钟读完 · · #astro #typescript

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 之后,我写文章时会先想清楚这篇的元信息——标题、摘要、标签分别是什么。

这个思考过程本来应该发生,只是以前没有任何东西强迫我发生。

全文完