Skip to content
Astro as content 4 min read

Defining Collections

A collection is a named group of related content entries that share the same shape — blog posts, docs pages, authors, products. In Astro you declare each collection in a single config file using defineCollection, wire up a loader that tells Astro where the data lives, and attach a schema that validates every entry at build time. Getting this configuration right means type-safe queries, autocomplete in your editor, and build-time errors instead of broken pages in production.

Where collections are defined

Astro looks for a single configuration file at the root of src/. Since Astro 5 the canonical location is src/content.config.ts (the older src/content/config.ts path still works for backwards compatibility, but prefer the new one).

This file must export a single collections object. Each key becomes a collection name you can query later, and each value is the result of calling defineCollection.

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: z.object({
    title: z.string(),
    pubDate: z.date(),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

The astro:content virtual module is provided by Astro itself — there is nothing to install. z is a re-exported Zod instance, so you never need a separate dependency for schemas.

The shape of defineCollection

defineCollection accepts an options object with two fields you will use constantly: loader and schema.

OptionRequiredPurpose
loaderYesTells Astro how to find and read entries (files, glob, or API data)
schemaNoA Zod schema (or function) that validates and types each entry

The loader is what makes Astro 5 collections flexible: it can read local Markdown with glob, a single structured file with file, or anything at all through a custom loader. The schema then guarantees that whatever the loader returns matches the structure your templates expect.

Tip: A collection without a schema still works — entries are loaded as-is and data is typed loosely. Add a schema as soon as the collection has more than one field; the build-time validation pays for itself immediately.

Attaching a loader

The loader is the bridge between your source data and the collection. The two built-in loaders cover the most common cases.

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders';

// Many files, one entry per file
const docs = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs' }),
  schema: z.object({
    title: z.string(),
    order: z.number(),
  }),
});

// One JSON/YAML file, one entry per array element
const authors = defineCollection({
  loader: file('src/data/authors.json'),
  schema: z.object({
    id: z.string(),
    name: z.string(),
    twitter: z.string().optional(),
  }),
});

export const collections = { docs, authors };

With glob, every matching file becomes an entry and its id is derived from the file path relative to base. With file, the file must contain an array (JSON) or a top-level map, and each item becomes one entry. For data that lives behind an API or a CMS, you write a custom loader instead — see the linked pages below.

Validating with a schema

The schema is a Zod object describing the front-matter (or data fields) of each entry. Astro runs it during astro dev and astro build; a mismatch fails the build with a precise, file-level error.

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: z.object({
    title: z.string().max(80),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    author: z.enum(['ada', 'linus', 'grace']),
  }),
});

export const collections = { blog };

If a post is missing description or sets an unknown author, you get an error like this rather than a runtime crash:

Output:

[InvalidContentEntryDataError] blog → my-post.md frontmatter does not match collection schema.
description: Required
author: Invalid enum value. Expected 'ada' | 'linus' | 'grace', received 'jane'

Schemas as a function (image helper)

When a field references a local image you want Astro to optimize, declare the schema as a function so you receive the image() helper.

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      cover: image(),
      coverAlt: z.string(),
    }),
});

The cover field is now validated to be a real image on disk and is typed as an ImageMetadata object you can pass straight to the <Image /> component.

Using the inferred types

Because the schema is Zod, Astro generates types automatically. Your .astro pages get full autocomplete on entry.data with no manual typing.

---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
---
<ul>
  {posts.map((post) => (
    <li>{post.data.title}{post.data.pubDate.toDateString()}</li>
  ))}
</ul>

This is the heart of Astro’s zero-JS-by-default model: all of this validation, querying, and rendering happens at build time, and the page above ships as static HTML with no client JavaScript.

Best practices

  • Keep a single src/content.config.ts and export one collections object — Astro only reads this one file.
  • Always attach a schema once a collection has more than a trivial shape; let build-time errors catch bad front-matter early.
  • Use z.coerce.date() for date fields so string front-matter parses cleanly into Date objects.
  • Prefer glob for file-per-entry content and file for a single structured data file; reach for a custom loader only when the data is remote.
  • Use the function form of schema whenever entries reference local images so you get the image() helper and optimized assets.
  • Store collection source files outside src/pages/ (e.g. src/data/ or src/content/) so they are not accidentally turned into routes.
Last updated June 14, 2026
Was this helpful?