Content Collections
Added in: v2.0.0 Beta
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).
Getting started
Section titled Getting startedFirst, 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 configurationTo 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.
{
"extends": "astro/tsconfigs/base"
"compilerOptions": {
"strictNullChecks": true
}
}
No update necessary!
"strictNullChecks": true
is already enabled in your strict
or strictest
configuration.
{
"extends": "astro/tsconfigs/strict" // or `strictest`
}
If you have a custom TypeScript configuration that does not include strict: true
, add "strictNullChecks": true
under compilerOptions
if it does not already exist.
{
"compilerOptions": {
"strictNullChecks": true
}
}
Set up the .astro
directory
Section titled Set up the .astro directoryAstro 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:
// using a relative path `../` to the `.astro` directory
/// <reference types="../.astro/types.d.ts" />
The content directory
Section titled The content directoryAstro 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/
.
Collections
Section titled CollectionsA 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 directoriesCollections 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/
- …
- …
Configuring collections
Section titled Configuring collectionsYou 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.
Defining a collection schema
Section titled Defining a collection schemaSchemas 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:
- Import the
defineCollection
andz
utilities fromastro:content
. - Define a
schema
for each collection. - 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
:
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,
};
Schema data types with Zod
Section titled Schema data types with ZodMarkdown 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']),
})
})
Advanced schema features
Section titled Advanced schema featuresYou 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.
Custom entry slugs
Section titled Custom entry slugsBy 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.
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 };
Querying content collections
Section titled Querying content collectionsAstro provides two functions to query collections:
getCollection()
Section titled getCollection()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;
});
---
Querying nested directories
Section titled Querying nested directoriesThe 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()
Section titled getEntry()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 querygetCollection()
and getEntry()
will return entries that include:
id
- a unique ID using the file path relative tosrc/content/[collection]
.slug
- a URL-ready slug. Defaults to theid
parsed by github-slugger.data
- an object of frontmatter properties inferred from your collection schema. Defaults toany
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:
---
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>
Collection entry types
Section titled Collection entry typesIf a page or component uses content from a getCollection()
or getEntry()
query, you can use the CollectionEntry
utility to type its props:
---
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;
---
Rendering entry content
Section titled Rendering entry contentEvery 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.
---
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 <Content /> 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:
---
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>
Access headings from render()
Section titled Access headings from render()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 collectionsYou 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.
---
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/
.
Rendering post contents
Section titled Rendering post contentsWhen 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.
---
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 />