Skip to content

Astro Content Layer & Schema Evolution

As documentation platforms grow, metadata requirements change. Engineering teams might need to introduce new attributes—such as lastUpdatedBy, difficultyLevel, or custom category taxonomies—without breaking hundreds of existing legacy articles.

In Astro 5, the content management pipeline was overhauled with the Astro Content Layer. This architecture decouples content storage from static routes, enabling high-performance content loading, strict schema enforcement, and safe metadata evolution.


Prior versions of Astro required collections to live inside strict file locations with limited loader customization. In Astro 5, content collections are defined centrally in src/content.config.ts using modern Loaders:

import { defineCollection, z } from 'astro:content';
import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({
// 1. Built-in Starlight Content Loader
loader: docsLoader(),
// 2. Extensible Zod Schema Validation
schema: docsSchema({
extend: z.object({
category: z.string().optional(),
lastUpdatedBy: z.string().optional(),
}),
}),
}),
i18n: defineCollection({
loader: i18nLoader(),
schema: i18nSchema(),
}),
};

When evolving content schemas across an active engineering organization, follow these four rules to avoid breaking CI pipelines or blocking writer contributions:

1. Always Default or Make Optional

Never add a required field without a default value to an existing schema. Making a field strictly required (z.string()) will instantly cause all existing articles lacking that field to fail the build.

2. Use Explicit Defaults

If a field is conceptually required for new pages, provide a fallback default: z.enum(['draft', 'published']).default('published')

3. Graceful Deprecation

When retiring a field, do not delete it immediately. Mark it optional or use .transform() to remap old values into the new structure.

4. Mirror in Sveltia CMS

Whenever you add a field to src/content.config.ts, immediately add the corresponding UI widget in public/admin/config.yml so writers can edit it visually.


Example: Adding a “Reviewer” and “Status” Field

Section titled “Example: Adding a “Reviewer” and “Status” Field”

Here is how to safely evolve the schema to track editorial reviews and draft statuses:

Update src/content.config.ts:

schema: docsSchema({
extend: z.object({
category: z.string().optional(),
lastUpdatedBy: z.string().optional(),
// New safe additions:
status: z.enum(['draft', 'in-review', 'published']).default('published'),
reviewedBy: z.string().optional(),
}),
})

Whenever you modify src/content.config.ts or add new documents, Astro generates TypeScript declaration files in .astro/types.d.ts.

  1. Trigger Sync:
    Run npx astro sync (or start the dev server with npm run dev).
  2. Type Safety in Custom Astro Components:
    If you create a custom page or component querying docs via getCollection('docs'), TypeScript will enforce and autocomplete your custom fields with zero runtime overhead:
    import { getCollection } from 'astro:content';
    const allDocs = await getCollection('docs');
    const published = allDocs.filter(doc => doc.data.status === 'published');