zod 做 frontmatter 契约:把文章格式变成构建期错误
Content Collections 的 schema 是唯一真相。标题超 120 字、日期写错、标签写成字符串,都在构建期直接失败,而不是线上少一页。
写文章最容易出的错不是内容,是格式。少一个引号、日期写成中文、标签忘了方括号,后果是页面静默消失,或者渲染出奇怪的东西。
schema 只有一处
src/content.config.ts 是唯一真相。注意 zod 要从 astro/zod 导入,不是 astro:content:
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog', retainBody: true }),
schema: z.object({
title: z.string().max(120),
description: z.string().max(300).default(''),
date: z.coerce.date(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
featured: z.boolean().default(false),
}),
});
三条约束各自挡住什么
120 字上限挡住的是「把摘要当标题写」。它同时是 title 标签、og:title 与 JSON-LD 的输入,超长会在搜索结果里被截断。
date 用 coerce 而不是 z.date(),因为 YAML 里它就是字符串;这样 2026-08-13 能过,写成中文日期会被拒。
tags、draft、featured 有默认值,可以完全不写;但一旦写了就必须是数组与布尔,draft: true 加了引号会被拒。
retainBody 与语言
retainBody: true 让 entry.body 保留原文,阅读时长就靠它算:中日韩按字符数、其余按分词,两边相加。语言从 entry.id 的前缀推导,加文章不需要配任何路由。
schema 管不了的部分
它挡不住「slug 用了中文」「三语只写了两语」「description 写成本文介绍」。这些靠撰写规范:交付清单要求三语齐全,因为缺一门语言时,那门语言的列表里就没有这篇。
修改顺序
内容迁就 schema,不是反过来。要放宽约束,先改 schema、再同步文档、最后才动文章。反过来做,早晚会出现「本地能跑、构建炸掉」的提交。
构建期失败的成本是五秒,线上少一页的成本是没人知道少了什么。

评论
…