Let's talk
engineering

A content schema is a publishing gate a CMS cannot give you

In a CMS, a page with no meta description publishes. A page with an empty FAQ section publishes. A page whose category was never set publishes, and lands in whatever the theme does with an undefined value. None of these are errors, because there is nothing in the system that believes they are errors. They are absences, and absences are silent.

The single largest practical difference in moving this site to a static build was not speed or security. It was that content acquired a schema, and the schema can refuse.

What that looks like

Every content type here is declared with a validator. An article:

const insights = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    published: z.coerce.date(),
    topic: z.enum(['engineering', 'ai', 'operations', 'search', 'security']),
    description: z.string().min(70).max(165),
    order: z.number().default(50),
    draft: z.boolean().default(false),
  }),
});

A product carries more, because more depends on it: a status from a fixed set, at least three features each with a title and a body, at least three bullets, at least four FAQ entries, and a category term that is used to build the page title.

If any of that is wrong, astro build stops. Not a warning in a log, not a red dot in a dashboard. The build fails and there is no artefact to deploy.

It caught something this week

Writing an article about verification, we gave it a description of 169 characters. The limit is 165, chosen because that is roughly where Google truncates. The build refused:

insights → a-redirect-that-404s-teaches-nothing.md frontmatter does not match collection schema.
description: String must contain at most 165 character(s)

Four characters. In a CMS that page ships, the snippet is truncated mid-sentence in the results, and nobody finds out unless somebody happens to look at that result on that query. Here it cost thirty seconds and could not reach production.

That is the entire argument, and it scales: the same mechanism is why no page on this site is missing a meta description, because a missing one is not a thing that can exist.

Minimums are more interesting than types

The type checks are table stakes. The constraints are where the editorial standard lives.

faqs.min(4) on a product means nobody ships a product page with two lazy questions on it. It is a decision about quality, enforced by the build rather than by a reviewer remembering. features requiring a title and a body means no feature can be a bare noun. bullets.min(3) means the summary cannot be a single line of marketing.

None of these is a technical requirement. Every one of them is an editorial policy that has been made executable, which is the part a CMS genuinely cannot do — a required field can be filled with a space.

The enum that prevented a lie

The one that has earned the most is small: product status is z.enum(['live', 'uat', 'in-build', 'pre-launch']).

It sounds like housekeeping. What it actually does is make the status of every product a single fact with one home, which the page renders, the platform index groups by, and the homepage counts. When we later wrote longer product pages, the instruction “do not describe an in-build product as though it were in production” had something to check against, and the count of what is live is derived rather than typed — so the sentence on the homepage saying how many applications are in production cannot drift from the products themselves.

Before, that number was a word in a paragraph. Words in paragraphs go stale silently. This one cannot.

The cost

Authors have to satisfy a validator, and validators are not diplomatic. The error above names a file and a rule and offers no suggestion. For an engineering team writing its own material that is fine and arguably preferable. For a marketing team that expects to publish without a build step it is friction that has to be designed around — usually with a git-based editing interface, which is real work and should be budgeted rather than assumed.

The other cost is that schema changes are migrations. Adding a required field means every existing entry fails until it is filled in. That is the correct behaviour and it is still an afternoon.

The generalisation

The useful frame is not “static sites are better”. It is that content is data, and data with no schema drifts.

Every CMS-based site of a certain age has the same archaeology in it: posts with no excerpt, pages with a category nobody uses any more, three different conventions for the same field because the convention changed twice and nothing went back to fix the old entries. None of that is anybody’s fault. It is what happens when the only thing enforcing consistency is memory.

Put a schema in front of it and the drift stops being possible, which is a much stronger position than the drift being noticed.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch