Skip to content

Frontmatter reference

The blog collection’s schema is defined in src/content.config.ts (or src/content/config.ts in older themes). This page mirrors that schema and explains each field.

src/content.config.ts
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
author: z.string(),
category: z.string(),
image: z.string().optional(),
draft: z.boolean().default(false),
}),
});
title: "Getting Started with Aurora"
  • Type: string
  • Required: yes
  • Used as: post <h1>, blog index card title, <title> tag, OG / Twitter share title.

Keep under ~60 characters for clean display in search results.

description: "A 5-minute walk through Aurora's core concepts."
  • Type: string
  • Required: yes
  • Used as: card subtitle on the blog index, <meta name="description">, OG / Twitter share description.

~150 characters is the sweet spot. Beyond that, Google truncates with an ellipsis.

date: 2026-01-15
# or with time:
date: 2026-01-15T14:30:00.000Z
  • Type: YAML date (parsed as JS Date)
  • Required: yes
  • Used as: card date, post byline date, <time datetime="..."> for SEO, sort order on the blog index.

ISO format is safest. Astro’s content layer parses both YYYY-MM-DD and full ISO strings; bare strings like "Jan 15, 2026" will fail the schema.

author: "Alex Rivera"
  • Type: string
  • Required: yes
  • Used as: byline on the post and the card.

For multi-author posts, comma-separate ("Alex Rivera, Sarah Kim") or convert to an array — see Extending the schema.

category: "Engineering"
  • Type: string
  • Required: yes
  • Used as: the tag chip on cards, the filter facet on the blog index.

To enforce a fixed set, use a z.enum() — see Extending the schema.

image: "/blog/getting-started.jpg"
  • Type: string path (relative to public/)
  • Required: no
  • Used as: hero image on the post, card thumbnail, OG image for that specific post.

Recommended size: 1200×630 PNG or JPG so it doubles as the OG share card. If omitted, the card uses a placeholder color and the OG image falls back to siteConfig.seo.ogImage.

draft: true
  • Type: boolean
  • Default: false
  • Used as: build filter.
    • true — visible in dev (npm run dev), excluded from npm run build.
    • false — published.

The fields below aren’t in the default schema, but they’re commonly useful. Add them by extending src/content.config.ts.

For multi-label posts (different from a single category):

tags: z.array(z.string()).optional(),
tags: ["typescript", "performance", "deploy"]

For pinning to the top of the blog index:

featured: z.boolean().default(false),
featured: true

Then in src/pages/blog/index.astro, sort featured posts first:

const posts = (await getCollection("blog"))
.sort((a, b) => Number(b.data.featured ?? 0) - Number(a.data.featured ?? 0)
|| b.data.date.getTime() - a.data.date.getTime());

For posts you revise:

updated: z.date().optional(),
date: 2026-01-15
updated: 2026-03-08

For a longer card description that differs from the SEO meta:

excerpt: z.string().optional(),
excerpt: "We rebuilt Aurora's onboarding from scratch. Here's what we learned about UX research, prototyping, and shipping with confidence."

Open src/content.config.ts and add fields to the Zod schema:

src/content.config.ts
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
author: z.string(),
category: z.enum(["Tutorial", "Engineering", "Design", "Product"]),
image: z.string().optional(),
tags: z.array(z.string()).default([]),
featured: z.boolean().default(false),
updated: z.date().optional(),
draft: z.boolean().default(false),
}),
});

Type-safe everywhere — Astro errors at build time if a post is missing a required field or uses an invalid value.