Skip to content
AstroCraft Docs
On this theme

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.

NEXT STEPThe Car Records