Content Collections

Added in: v2.0.0

Content collections help organize your Markdown or MDX and type-check your frontmatter with schemas. Collections may be helpful if you:

  • Plan to use Markdown content in multiple areas of your site (landing pages, footers, navigation, etc).
  • Want Astro to enforce frontmatter fields, and fail if fields are missing (e.g. every blog post should have a title and description).

First, run the astro dev, astro build, or astro sync commands to generate types. This will make the astro:content module available for querying and configuring your content collections.

Update TypeScript configuration

Section titled Update TypeScript configuration

To benefit from the full TypeScript and autocompletion features of using schemas with your collections, you may also need to update tsconfig.json. Follow the instructions below based on your Astro project’s current TypeScript configuration.

If you are extending Astro’s base TypeScript config (e.g. you chose “relaxed” TypeScript when creating your project with create astro), add "strictNullChecks": true under compilerOptions.

Or, you can change your base TypeScript config to strict or strictest to use a broader set of stricter type-checks in your project.

tsconfig.json
{
  "extends": "astro/tsconfigs/base",
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Astro generates TypeScript types from your content collections in the .astro directory. These types will be updated anytime you run the astro dev, astro build, or astro sync commands.

These commands will also generate a reference path to include .astro types in your project. A src/env.d.ts file will be created for you if one does not exist, and the following will be added:

src/env.d.ts
// using a relative path `../` to the `.astro` directory
/// <reference types="../.astro/types.d.ts" />

Astro treats the src/content/ directory as special. This is where collections (folders) of Markdown/MDX entries (files) can be stored, with a single configuration file to define each collection’s schema (frontmatter data types and shape). Files other than your .md/.mdx content are not permitted inside src/content/.

A collection is a directory in src/content/ containing Markdown or MDX files. Every Markdown or MDX file in src/content/ must belong to a collection directory, since Astro provides built-in functions for querying your content by the collection directory name.

Content within a collection should share the same frontmatter shape and types. You can optionally enforce these types by configuring a schema.

To create a collection, add a new directory to src/content/. Then, add Markdown or MDX entries that share frontmatter properties. The following example shows two collections: blog and newsletter.

  • src/content/
    • blog/ All blog posts have the same frontmatter properties
      • columbia.md
      • endeavour.md
      • enterprise.md
    • newsletter/ All newsletters have the same frontmatter properties
      • week-1.md
      • week-2.md
      • week-3.md

Collections with nested directories

Section titled Collections with nested directories

Collections are top-level folders within src/content/. You cannot nest collections, but you may use nested directories within a collection to better organize a collection’s content. All nested directories will share the same schema defined for the top-level collection.

For example, you can use this structure for internationalization:

  • src/content/
    • docs/ docs schema applies to all nested directories
      • en/
      • es/

You can configure your content collections with an optional src/content/config.ts file (.js and .mjs extensions are also supported).

This file currently allows you to define a collection schema, and to create custom slugs for your collections.

Schemas are an optional way to enforce frontmatter types in a collection. Astro uses Zod to validate your frontmatter with schemas in the form of Zod objects.

To configure schemas, create a src/content/config.ts file (.js and .mjs extensions are also supported). This file should:

  1. Import the defineCollection and z utilities from astro:content.
  2. Define a schema for each collection.
  3. Export a single collections object, with each object key corresponding to the collection’s folder name.

For example, say you maintain two collections: one for release announcements and one for blog content. Your entries at src/content/releases/ should include a title and version. Your src/content/engineering-blog/ collection entries should have a title, list of tags, and an optional image URL.

You can specify the expected shape of your frontmatter with the schema field of defineCollection:

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

const releases = defineCollection({
  schema: z.object({
    title: z.string(),
    version: z.number(),
  }),
});

const engineeringBlog = defineCollection({
  schema: z.object({
    title: z.string(),
    tags: z.array(z.string()),
    image: z.string().optional(),
  }),
});

export const collections = {
  releases: releases,
  // Don't forget 'quotes' for collection names containing dashes
  'engineering-blog': engineeringBlog,
};

Markdown and MDX frontmatter can contain booleans, strings, numbers, objects, and arrays. When defining a schema, you must include every frontmatter property along with its data type. To define and validate this schema, we use a library called Zod, which is available via the z import.

You can extend any of these types with .optional() if a frontmatter property is not always required or .defaultValue(value) to provide a value to use when the property is not set in frontmatter. If only a limited set of values is valid for a property, you can specify these using the .enum() method.

The following schema illustrates each of these data types in use:

import { z, defineCollection } from 'astro:content';

defineCollection({
  schema: z.object({
    isDraft: z.boolean(),
    title: z.string(),
    sortOrder: z.number(),
    image: z.object({
      src: z.string(),
      alt: z.string(),
    }),
    tags: z.array(z.string()), // An array of strings
    footnote: z.string().optional(),
    author: z.string().default('Anonymous'),
    language: z.enum(['en', 'es']),
  })
})

You can use all of Zod’s properties and methods with content schemas. This includes transforming a frontmatter value into another value, checking the shape of string values with built-in regexes, and more.

// Allow only strings representing email addresses
authorContact: z.string().email(),
// Allow URL strings only (e.g. `https://example.com`)
canonicalURL: z.string().url(),
// Parse publishDate as a browser-standard `Date` object
publishDate: z.string().transform(str => new Date(str)),

📚 See Zod’s documentation for a complete list of features.

By default, Astro will generate a slug for each content entry based on its id parsed by github-slugger.

If you want to generate custom slugs for each entry, you can provide a slug() function in defineCollection(). Your slug() function can use the entry ID, default slug, parsed frontmatter (as data), and the raw body of the entry to generate the slug.

For example, to use a frontmatter permalink property as the slug for your blog pages instead of the file path, you can use the following slug() function. For extra safety, conditionally return the entry’s defaultSlug as a fallback.

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

const blog = defineCollection({
  slug: ({ id, defaultSlug, data, body }) => {
    // Use `permalink` from the entry’s frontmatter as the slug, if it exists.
    // Otherwise, fall back to the default slug.
    return data.permalink || defaultSlug;
  },
  schema: z.object({
    permalink: z.string().optional(),
  }),
});

export const collections = { blog };

Astro provides two functions to query collections:

getCollection() returns multiple entries in a collection. It requires the name of a collection as a parameter. By default, it returns all items in the collection.

It can also take a second, optional parameter: a filter function based on schema properties. This allows you to query for only some items in a collection based on id, slug, or frontmatter values via the data object.

---
import { getCollection } from 'astro:content';

// Get all `src/content/blog/` entries
const allBlogPosts = await getCollection('blog');

// Only return posts with `draft: true` in the frontmatter
const draftBlogPosts = await getCollection('blog', ({ data }) => {
  return data.draft === true;
});
---

The filter function can also be used to query for nested directories within a collection. Since the id includes the full nested path, you can filter by the start of each id to only return items from a specific nested directory:

---
import { getCollection } from 'astro:content';
const enDocs = await getCollection('docs', ({ id }) => {
  // Return all entries in `src/content/docs/en/`
  return id.startsWith('en/');
});
---

getEntry() is function that returns a specific entry in a collection by entry ID (file path relative to the collection). Both of these are required parameters.

---
import { getEntry } from 'astro:content';

const enterprise = await getEntry('blog', 'enterprise.md');
---

Data returned from a collection query

Section titled Data returned from a collection query

getCollection() and getEntry() will return entries that include:

  • id - a unique ID using the file path relative to src/content/[collection].
  • slug - a URL-ready slug. Defaults to the id parsed by github-slugger.
  • data - an object of frontmatter properties inferred from your collection schema. Defaults to any if no schema is configured.
  • body - a string containing the raw, uncompiled body of the Markdown or MDX document.
  • render() - a function that returns the compiled body of the Markdown or MDX document via the <Content /> component (See complete documentation).

Querying your content files with getCollection()or getEntry() allows you to use frontmatter properties from an entry’s data object in JSX-like expressions or pass props to other components, such as a layout. You can optionally add type safety with a built-in utility.

For example, you can use a getCollection() query to filter and then display a list of links to all your published blog posts:

src/pages/index.astro
---
import { getCollection } from 'astro:content';

// Get all published blog posts
const blogPosts = await getCollection('blog', ({ data }) => {
  return data.status === 'published';
});
---
<ul>
  {blogPosts.map(post => (
    <li>
      <a href={post.slug}>{post.data.title}</a>
      <time datetime={post.data.publishedDate.toISOString()}>
        {post.data.publishedDate.toDateString()}
      </time>
    </li>
  ))}
</ul>

If a page or component uses content from a getCollection() or getEntry() query, you can use the CollectionEntry utility to type its props:

src/components/BlogCard.astro
---
import type { CollectionEntry } from 'astro:content';

interface Props {
  // Get type of a `blog` collection entry
  post: CollectionEntry<'blog'>;
}

// `post.data` will match your collection schema
const { post } = Astro.props;
---

Every collection entry includes a render() function that gives you access to the contents of the Markdown or MDX file. This includes a <Content /> component for rendering the document body, as well as the document headings and frontmatter modified via remark or rehype.

src/pages/render-example.astro
---
import { getEntry } from 'astro:content';
const entry = await getEntry('blog', 'post-1.md');
const { Content, headings, remarkPluginFrontmatter } = await entry.render();
---

Access the <Content /> component from render()

Section titled Access the &lt;Content /&gt; component from render()

To render the content of a Markdown or MDX entry, use the <Content /> component returned by its render() function. This allows you to generate pages from your content entries (see Generating pages from content collections), add post previews to your homepage, or display your content elsewhere on your site.

For example, this page renders the contents of content/announcements/welcome.md and uses some of its frontmatter properties:

src/pages/welcome-announcement.astro
---
import Layout from '../../layouts/Layout.astro';
import { getEntry } from 'astro:content';
const announcementPost = await getEntry('announcements', 'welcome.md');
const { Content } = await announcementPost.render();
---
<Layout>
    <h1>{announcementPost.data.title}</h1>
    <p>Written by: {announcementPost.data.author}</p>
    <Content />
</Layout>

Astro generates a list of headings for Markdown and MDX documents. You can access this list using the headings property from render():

---
import { getCollection } from 'astro:content';
const blogPosts = await getCollection('blog');
---

{blogPosts.map(async (post) => {
  const { headings } = await post.render();
  const h1 = headings.find(h => h.depth === 1);
  return <p>{h1}</p>
})}

Access modified frontmatter from render()

Section titled Access modified frontmatter from render()

Astro allows you to modify frontmatter using remark or rehype plugins. You can access this modified frontmatter using the remarkPluginFrontmatter property from render():

---
import { getCollection } from 'astro:content';
const blogPosts = await getCollection('blog');
---

{blogPosts.map(async (post) => {
  const { remarkPluginFrontmatter } = await post.render();
  return <p>{post.data.title}{remarkPluginFrontmatter.readingTime}</p>
})}

Assuming readingTime was injected (see our reading time example), it will be available on the remarkPluginFrontmatter object.

🙋 Why don’t getCollection() and getEntry() contain these values?

The remark and rehype pipelines are only run when your content is rendered. This lets render() access anything generated by these plugins like injected frontmatter. To stay performant, getCollection() and getEntry() do not have this capability.

Generating pages from content collections

Section titled Generating pages from content collections

You can create pages based on your content collections using dynamic routes.

Use getCollection() inside a getStaticPaths() function to query your content entries and provide the slug parameter for each page.

For example, you can dynamically create a page for each entry in a blog collection, including nested directories, by creating a .astro page with a rest parameter in its filename to match file paths of any depth.

src/pages/posts/[...slug].astro
---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const blog = await getCollection('blog');
  return blog.map(entry => ({
    params: { slug: entry.slug },
  }));
}
---

This will generate page routes for every entry in the blog collection, mapping each entry’s slug to a URL. For example, an entry at src/content/blog/hello-world.md will have a slug of hello-world and the collection entry src/content/blog/en/intro.md will have a slug of en/intro.

Because this dynamic route is in src/pages/posts/, the final URLs will be /posts/hello-world/ and /posts/en/intro/.

When you pass each page route an entry via props in your getStaticPaths() function, you have access to the entry from Astro.props. You can use its frontmatter values via the data object and use render() to render its content via a <Content /> component. You can optionally add type safety using the CollectionEntry utility.

src/pages/blog/[...slug].astro
---
import { getCollection, CollectionEntry } from 'astro:content';

export async function getStaticPaths() {
  const docs = await getCollection('docs');
  return docs.map(entry => ({
    // Pass blog entry via props
    params: { slug: entry.slug }, props: { entry },
  }));
}

interface Props {
  // Optionally use `CollectionEntry` for type safety
  entry: CollectionEntry<'docs'>;
}

const { entry } = Astro.props;
const { Content } = await entry.render();
---

<h1>{entry.data.title}</h1>
<Content />