ABS Frontmatter: Required vs Optional Fields, the Rule
ABS frontmatter is enforced by six Zod validators in src/content.config.ts — title ≤120, description ≤200, four enums, and a URL array. Build fails loudly on violation. Defaults fill the rest.

The rule in one sentence
ABS frontmatter is the publish contract: five fields are required, six more are accepted with sensible defaults, and six validators in src/content.config.ts reject anything else — the build fails loudly with a Zod error, never silently with a malformed page.
The schema is the executable authority. Anything not declared here, in the order and shape declared here, breaks the build. That is by design — a failed deploy is cheaper than a malformed public page.
The six validators
There are exactly six validation shapes in src/content.config.ts:
| # | Validator | Applied to | Limit |
|---|---|---|---|
| 1 | z.string().max(120) | title | 120 characters |
| 2 | z.string().max(200) | description | 200 characters |
| 3 | z.enum([...]) | type | news / review / guide / take |
| 4 | z.enum([...]) | category | ai / gadgets / computers / gaming |
| 5 | z.array(z.string().url()) | sources[*].url | absolute http(s):// URL |
| 6 | z.enum([...]) | grade (optional) | A / B / C / D / F |
Two more bounded numeric / structural validators sit on optional fields but still fail loudly when triggered: score is z.number().int().min(0).max(100), and the inner facts[*] has label.max(40) and value.max(120). Tag strings are bounded at 30 characters.
If a field does not match its validator, the build exits non-zero with a Zod error pointing at the offending file and key. There is no soft-fail path.
Required vs optional, with defaults
The split between required and defaulted fields is intentional: a new post must carry identity and classification, and everything else has a sane default that lets a draft publish cleanly.
| Field | Status | Why |
|---|---|---|
title | Required | Display headline, cards, OG metadata |
description | Required | Dek, RSS summary, search snippets |
type | Required | Routes the post (/news/, /reviews/, /guides/, /takes/) |
category | Required | Subject classification, homepage lead filtering |
pubDate | Required | Date sort, sitemap, RSS feed order |
image | Optional | Featured art; page renders without it but degrades |
imageAlt | Optional | Accessibility; required when image is present |
imagePrompt | Optional | Re-generation recipe; required by ABS house style when image is present |
updatedDate | Optional | Substantial revision date |
grade | Optional (reviews only) | Editorial verdict, sort tie-breaker |
score | Optional | 0-100 editorial score, ranking band |
weight | Optional | signal / frame / evidence / context / people / industry |
author | Optional | Override; defaults to site author in Base.astro |
affiliate | Defaulted false | Triggers disclosure line when true |
sources | Defaulted [] | Citation list; empty array allowed |
facts | Defaulted [] | Quick-facts panel; empty array allowed |
tags | Defaulted [] | Discovery chips; empty array allowed |
related | Defaulted [] | Cross-link list; runtime auto-fills when empty |
draft | Defaulted false | Publication switch |
Five required, eight optional, six defaulted. The math: a minimal valid post needs exactly five fields, plus the YAML delimiters.
The auto-tag / auto-default behavior
When a defaulted field is omitted, Zod substitutes the default before validation. A missing affiliate becomes affiliate: false; a missing sources becomes sources: []; a missing tags becomes tags: []. The page renders normally; the disclosure line is suppressed; the chip row is hidden; the related list falls back to runtime auto-selection (most recent of the same type).
This is a default, not a permission slip. ABS canonical authoring writes the canonical keys explicitly so diffs and reviews are clean. Two defaults are worth calling out:
draft: falseis the build-time default. A post with nodraftkey publishes. To keep a post in the repo but off the live site, setdraft: trueexplicitly.related: []triggers the runtime auto-relationship. Setrelatedexplicitly for a curated list; leave it empty (or omit) for auto.
What triggers a build failure
The build fails when Zod rejects the frontmatter. The error surfaces in the build log as a Zod issue pointing at the specific file, field, and constraint. The five most common triggers, in observed frequency:
- Description over 200 characters. Astro’s
ZodStringTooBigerror names the field and the length. Truncation is the operator’s job, not Zod’s. - Title over 120 characters. Same shape, different field. Often caused by pasting a sentence-style headline into the title slot.
- Invalid enum value. The most common is
type: how-toortype: tutorial— the schema accepts only the four declared values. Same shape forcategory(software,tech,codingall fail) andgrade(alowercase fails; onlyA/B/C/D/Fare accepted). - Relative or malformed source URL.
z.string().url()rejectswww.example.com,/about,example.com, and bare strings. Every source must be an absolutehttps://(orhttp://) URL. - Numeric tag without quotes. A tag of
2026parses as YAML number2026and failsz.string().max(30). Quote every numeric tag, year, or version:- "2026",- "v1.4.0".
The shared shape: the build log reports ZodError with the file path, the key, and the expected vs received value. The fix is always in the frontmatter, never in the schema.
The 4 schema failures seen this month
Four real failures in July 2026:
Failure 1 — description over 200 chars
prx-part-4-our-data-strategy.md failed npm run build with ZodStringTooBig on description. The original dek was 232 characters. Fix: drop the redundant clauses. Final dek (162 chars): PhotoRoom's PRX Part 4 explains how its image-generation data strategy handles curation, synthetic data, and the privacy tradeoffs that come with synthetic training data.
Lesson: write the dek to the limit, not to the idea.
Failure 2 — type: how-to on a tutorial-style guide
A guide post used type: how-to because it read naturally. The schema accepts only news, review, guide, take. Fix: type: guide.
Lesson: the four type values are an enum, not a vocabulary. Use the exact word.
Failure 3 — source URL without scheme
A draft cited a community blog as www.example.com/blog/post. z.string().url() rejected it — no https://. Same shape on /about and example.com. Fix: prepend the scheme.
Lesson: an absolute URL means scheme + host + path. Anything less is rejected.
Failure 4 — numeric tag unquoted
A draft listed tags: [2026, hermes, v1.4.0]. YAML parsed 2026 as the integer 2026, which then failed z.string().max(30) — the schema expected a string. The other two parsed as strings and passed. Fix: quote every tag that is purely numeric or version-like.
What NOT to do
- Do not relax the schema to make a post build. Fix the frontmatter; the limit exists for a reason.
- Do not rely on schema defaults to skip canonical metadata on new posts. The fourteen canonical keys should be explicit on every new file.
- Do not invent enum values. If
type: how-toreads better to you, the file is a guide — useguide. - Do not put bare
www.example.comURLs insources. Addhttps://. - Do not put unquoted numeric tags, years, or versions. Quote them.
- Do not set
draft: falsebefore verifying the rendered page. Defaults publish; drafts do not. - Do not add fields the schema does not declare (e.g.,
excerpt,hero,subtitle). The schema rejects unknown keys; you cannot smuggle them past Zod.
Verification checklist
A pre-publish sanity pass that catches the four failures above:
| # | Check | Pass |
|---|---|---|
| 1 | title ≤ 120 characters | Yes |
| 2 | description ≤ 200 characters | Yes |
| 3 | type is one of news, review, guide, take | Yes |
| 4 | category is one of ai, gadgets, computers, gaming | Yes |
| 5 | Every sources[*].url starts with https:// or http:// | Yes |
| 6 | Every tags entry is a quoted string (especially numbers and versions) | Yes |
| 7 | image path starts with /images/ and resolves | Yes |
| 8 | imageAlt describes the visible subject, not the prompt | Yes |
| 9 | pubDate is an ISO date on a single line | Yes |
| 10 | draft: false only after page render is verified | Yes |
A one-liner that runs every validator except the file-existence ones:
cd /srv/abs-site && npm run build 2>&1 | grep -E "ZodError|ZodStringTooBig|Invalid enum|Invalid url" || echo "schema ok"
Expected: schema ok. Any other output names the file and the field.
Sources
- ABS companion: ABS Content Frontmatter: The Canonical Shape
- Astro docs: Content Collections
- Zod docs: Zod
- Live schema:
/srv/abs-site/src/content.config.ts(six validators as of 2026-07-14)
Last verified: 2026-07-14 against the schema at /srv/abs-site/src/content.config.ts and the four July 2026 build failures (PRX description length, type: how-to, scheme-less source URL, unquoted numeric tag).



Submit a take
Have a different read on this? Drop a comment below — your email isn't published, and I read every one. Nothing leaves the site until I approve it.