The visible half of a theme is the design. The half that decides whether you still like the theme in eighteen months is the content layer, and in Astro that means content collections.

A schema is a contract with your future self

A collection schema declares what a piece of content is allowed to contain. A blog post has a title, a description, a date, exactly one category, exactly one author, an optional image, and a boolean for whether it is featured. Anything else fails the build.

That sounds restrictive until the first time someone adds a post with the date written as a string instead of a date, or misspells a category slug. Without a schema those errors ship silently and surface as a blank page in production. With one they surface as a build failure with the file name in it.

const blog = defineCollection({
  type: 'content',
  schema: ({ image }) => z.object({
    title: z.string(),
    description: z.string(),
    date: z.date(),
    category: reference('categories'),
    author: reference('authors'),
    image: image().optional(),
    featured: z.boolean().default(false),
  }),
});

References are what make the site a site

reference('authors') is doing more work than it looks like. It means the author of a post is not a string that happens to match a name somewhere else — it is a pointer to an entry in another collection, validated at build time. Rename an author file and every post that pointed at it fails loudly instead of rendering an empty byline.

Practically, this is what lets a theme generate author pages and category pages from the same data that renders the article. One source, three route templates, no duplicated lists to keep in sync. It is also the structure search engines can actually follow: every article links to its author and its category, and both of those pages link back to the full set.

The image() helper earns its place

Declaring image: image().optional() in the schema means the frontmatter path is resolved and validated like any other field, and the result is a typed image object rather than a string. Passing that to Astro’s <Image /> gives you generated WebP, the widths you asked for, and the intrinsic dimensions written into the markup so the layout does not shift while it loads.

None of that requires anyone on the team to remember to convert a file before committing it, which is the only image workflow that survives contact with a real project.

Where the structure shows

You do not see any of this on the homepage. You see it on the eleventh article, when adding it takes four minutes and touches one file.