guide · computers

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.

July 14, 2026 · By Alastair Fraser

A retro robot inspector standing at a checkpoint with two lanes — REQUIRED and OPTIONAL — sorting Markdown posts into a glowing PASS chute and a red FAIL chute with a schema error list.

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:

#ValidatorApplied toLimit
1z.string().max(120)title120 characters
2z.string().max(200)description200 characters
3z.enum([...])typenews / review / guide / take
4z.enum([...])categoryai / gadgets / computers / gaming
5z.array(z.string().url())sources[*].urlabsolute http(s):// URL
6z.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.

FieldStatusWhy
titleRequiredDisplay headline, cards, OG metadata
descriptionRequiredDek, RSS summary, search snippets
typeRequiredRoutes the post (/news/, /reviews/, /guides/, /takes/)
categoryRequiredSubject classification, homepage lead filtering
pubDateRequiredDate sort, sitemap, RSS feed order
imageOptionalFeatured art; page renders without it but degrades
imageAltOptionalAccessibility; required when image is present
imagePromptOptionalRe-generation recipe; required by ABS house style when image is present
updatedDateOptionalSubstantial revision date
gradeOptional (reviews only)Editorial verdict, sort tie-breaker
scoreOptional0-100 editorial score, ranking band
weightOptionalsignal / frame / evidence / context / people / industry
authorOptionalOverride; defaults to site author in Base.astro
affiliateDefaulted falseTriggers disclosure line when true
sourcesDefaulted []Citation list; empty array allowed
factsDefaulted []Quick-facts panel; empty array allowed
tagsDefaulted []Discovery chips; empty array allowed
relatedDefaulted []Cross-link list; runtime auto-fills when empty
draftDefaulted falsePublication 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:

  1. draft: false is the build-time default. A post with no draft key publishes. To keep a post in the repo but off the live site, set draft: true explicitly.
  2. related: [] triggers the runtime auto-relationship. Set related explicitly 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:

  1. Description over 200 characters. Astro’s ZodStringTooBig error names the field and the length. Truncation is the operator’s job, not Zod’s.
  2. Title over 120 characters. Same shape, different field. Often caused by pasting a sentence-style headline into the title slot.
  3. Invalid enum value. The most common is type: how-to or type: tutorial — the schema accepts only the four declared values. Same shape for category (software, tech, coding all fail) and grade (a lowercase fails; only A/B/C/D/F are accepted).
  4. Relative or malformed source URL. z.string().url() rejects www.example.com, /about, example.com, and bare strings. Every source must be an absolute https:// (or http://) URL.
  5. Numeric tag without quotes. A tag of 2026 parses as YAML number 2026 and fails z.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-to reads better to you, the file is a guide — use guide.
  • Do not put bare www.example.com URLs in sources. Add https://.
  • Do not put unquoted numeric tags, years, or versions. Quote them.
  • Do not set draft: false before 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:

#CheckPass
1title ≤ 120 charactersYes
2description ≤ 200 charactersYes
3type is one of news, review, guide, takeYes
4category is one of ai, gadgets, computers, gamingYes
5Every sources[*].url starts with https:// or http://Yes
6Every tags entry is a quoted string (especially numbers and versions)Yes
7image path starts with /images/ and resolvesYes
8imageAlt describes the visible subject, not the promptYes
9pubDate is an ISO date on a single lineYes
10draft: false only after page render is verifiedYes

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

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).

Sources

#frontmatter#schema#zod#astro#validators#publish-contract

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.

Your email address will not be published. Required fields are marked.