Content Collections
Indexa has two content collections, defined in src/content.config.ts with Zod. Entries live one folder deep under src/data/, and the folder name is the slug:
src/data/
├── blog/
│ ├── automatics-under-10000/index.md -> /blog/automatics-under-10000/
│ ├── corsa-gse-2026-review/index.md
│ └── … 10 posts
└── authors/
├── rae-holloway/index.md
├── priya-raman/index.md
└── tom-achebe/index.md
The loader glob is **/[^_]*{md,mdx}, so a file or folder starting with _ is ignored — useful for a draft you are not ready to give a draft: true yet. .mdx renders through @astrojs/mdx, already wired in astro.config.mjs.
Because the schema is Zod, bad frontmatter fails the build with the entry named and the field named. That is the point of the schema: content mistakes are build errors, not blank spaces on a live page.
The post schema
---
title: What used cars did in August 2026
description: One line for the card, the meta description and the OG tag.
authors: [rae-holloway] # references the authors collection
pubDate: 2026-09-02
heroImage: ../../../assets/images/blog-price-index.png
---
Four fields are required — title, description, authors and pubDate — plus heroImage, which is required because the blog is real: every post needs an image for its card and its OG tag, so the schema refuses a post without one rather than letting a shareless post ship.
authors is a Zod reference("authors"), so a typo in a byline slug fails the build instead of rendering an empty author box.
The optional fields are where the design’s details live:
| Field | Type | What renders it |
|---|---|---|
updatedDate |
date | article:modified_time and the JSON-LD dateModified |
heroCaption |
string | The caption under the post’s feature image, with the house navy rule |
categories |
string[] | The topic pills; "Review" is what the reviews hub selects on |
readingTime |
positive int | The “{n} min read” on cards — authored, because the renderer has no cheap word count for MDX |
cutCount |
positive int | “Drawn from N records” — the size of the index cut the article was written from |
figuresMoved |
boolean | The dashed pill that admits a figure in the article has moved since publication |
exit |
object | A per-post CTA band above the footer: heading, sub, two links and a set of figures |
draft |
boolean | Excluded from /blog/, the RSS feed and getStaticPaths |
cutCount and figuresMoved are unusual enough to explain. Indexa’s editorial voice is that its numbers come from a dated cut of the index, so a post states how many records it was drawn from, and admits when the index has since moved. Both are author-set booleans and integers rather than anything computed — the honesty is editorial, so the schema makes it authorable instead of faking it.
The author schema
name and about and email and authorLink are required; role, avatar and stats are optional. stats is a list of { value, label } pills for an author page — present in the schema, awaiting the real per-author counts.
Adding a post
Create src/data/blog/<slug>/index.md, fill the five required fields, and the post is live: it appears on /blog/, in the RSS feed, in the related rail of every other post, and at /blog/<slug>/. Nothing registers it.
Two behaviours are worth knowing before you write. The related rail is the three most recent other posts, not a topic match. And the in-article “In this article” rail only appears if you author your H2s in .mdx with explicit ids:
<h2 id="where-augusts-cars-went">Where August's cars went</h2>
The markdown pipeline does not slug headings, so a plain ## Heading gets no DOM id, and a table of contents built from it would link to nothing. src/js/toc.ts extracts only H2s that carry an id, which means a plain .md post simply gets no rail rather than a set of dead anchors. Blog covers the rest of the post page.
Why the cars are not a collection
Sixty-nine vehicle records sit in src/config/carsData.json.ts and hubsData.json.ts as typed TypeScript, not in a collection. That is deliberate, and it is a holding position rather than a principle.
A collection buys you file-per-entry authoring and a Zod schema. These records are not authored one at a time — they stand in for an index feed, they are cross-referenced by ref from four other modules, and three of those references are pinned by self-checks that import the data directly. TypeScript gives them the same validation a Zod schema would, plus the cross-module checks a collection could not express, and it keeps them importable under node --experimental-strip-types so pnpm test can assert on them without a build.
When a real feed arrives, the records stop being authored content altogether. The Car Records covers the shape they have now and the seam where a feed would replace them.