zod as a frontmatter contract: bad articles fail the build
The Content Collections schema is the single source of truth. A title over 120 characters, a mangled date or a string where an array belongs fails immediately instead of silently dropping a page.
The most common mistake when writing a post is not the content, it is the format. A missing quote, a date written in prose, tags without brackets, and the page either vanishes or renders something strange.
One schema, one place
src/content.config.ts is the only source of truth. Note that zod comes from astro/zod, not 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),
}),
});
What each constraint catches
The 120-character limit catches writing the summary as the title. That string also feeds the title tag, og:title and JSON-LD, where it gets truncated in search results.
The date field uses coerce rather than z.date() because in YAML it is a string. That lets 2026-08-13 through and rejects a prose date.
tags, draft and featured have defaults, so they may be omitted; once present they must be an array and booleans, so a quoted draft: true is rejected.
retainBody and locale
retainBody: true keeps the raw entry.body, which is what reading time is computed from: characters for CJK, whitespace tokens otherwise, added together. The locale comes from the entry.id prefix, so adding a post configures no routes.
What the schema cannot enforce
It cannot stop a non-ASCII slug, a post translated into two languages out of three, or a description that says this article introduces something. Those are covered by the writing guide, which requires all three languages.
Which way changes flow
Content conforms to the schema, never the reverse. To loosen a constraint: edit the schema, update the guide, and only then touch articles. Do it backwards and you eventually commit something that runs locally and breaks the build.
A build failure costs five seconds. A missing page costs you not knowing it is missing.

Comments
…