Skip to content

Content Model Guide - Schema Design Patterns

This guide covers best practices for designing content schemas in Astro using Content Collections. Well-designed schemas ensure type safety, content consistency, and excellent developer experience.

  1. Type Safety First: Leverage Zod for runtime validation
  2. Future-Proof: Design for content evolution
  3. DRY: Reuse schema components
  4. Validation: Catch errors at build time
  5. Flexibility: Support various content needs

Create reusable base schemas for common fields:

// src/content.config.ts — Content Layer config lives at the src root
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
// Reusable schema components
const seoSchema = z.object({
metaTitle: z.string().max(60).optional(),
metaDescription: z.string().max(160).optional(),
ogImage: z.string().optional(),
noindex: z.boolean().default(false),
});
const authorSchema = z.object({
name: z.string(),
email: z.email().optional(),
avatar: z.string().optional(),
bio: z.string().optional(),
social: z.object({
twitter: z.string().optional(),
github: z.string().optional(),
linkedin: z.string().optional(),
}).optional(),
});
const baseContentSchema = z.object({
title: z.string(),
description: z.string().max(160),
date: z.date(),
draft: z.boolean().default(false),
tags: z.array(z.string()).default([]),
seo: seoSchema.optional(),
});
// Compose base schemas into collections. Each collection declares a
// Content Layer glob loader — entries are keyed by `id` (there is no
// `slug` in the Content Layer API).
const blogCollection = defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/blog' }),
schema: baseContentSchema.extend({
author: authorSchema.optional(),
updated: z.date().optional(),
relatedPosts: z.array(z.string()).optional(), // ids of related posts
}),
});
export const collections = {
blog: blogCollection,
};

Composing collections from shared building blocks like seoSchema and authorSchema keeps field definitions consistent across content types and makes schema evolution a one-place change. See the starter’s src/content.config.ts for the full production schema set (projects, blog, navigation, bio, experience, adr).