Content Collections
Added in:
astro@2.0.0
New
Content collections are the best way to work with Markdown and MDX in any Astro project. Content collections are a feature of Astro that help manage your content files in a project. Collections help to organize your content, validate your frontmatter, and provide automatic TypeScript type-safety for all of your content.
What are Content Collections?
Section titled What are Content Collections?A content collection is any directory inside the reserved src/content
project directory, such as src/content/newsletter
and src/content/blog
. Only content collections are allowed inside the src/content
directory. This directory cannot be used for anything else.
A content entry is any piece of content stored inside of your content collection directory. Content entries are stored as either Markdown (.md
) or MDX (.mdx
) files. You can use any filename you want, but we recommend using a consistent naming scheme (lower-case, dashes instead of spaces) to make it easier to find and organize your content.
src/content/
newsletter/ the “newsletter” collection
- week-1.md a collection entry
- week-2.md a collection entry
- week-3.md a collection entry
Once you have a collection, you can start querying your content using Astro’s built-in content APIs.
The “.astro” Directory
Section titled The “.astro” DirectoryAstro stores important metadata for content collections in an .astro
directory in your project. No action is needed on your part to maintain or update this directory. You are encouraged to ignore it entirely while working in your project.
The .astro
directory will be updated for you automatically anytime you run the astro dev
, astro build
commands. You can run astro sync
at any time to update the .astro
directory manually.
Organizing with multiple collections
Section titled Organizing with multiple collectionsIf two files represent different kinds of content (e.g. a blog post and an author profile), they most likely belong in different collections. This is important because many features (frontmatter validation, automatic TypeScript type-safety) require that all entries in a collection share a similar frontmatter structure.
If you find yourself working with different types of content, you should create multiple collections to represent each type. You can create as many different collections in your project as you’d like.
src/content/
newsletter/
- week-1.md
- week-2.md
- week-3.md
authors/ split different content types into new collections
- grace-hopper.md
- alan-turing.md
- batman.md
Organizing with subdirectories
Section titled Organizing with subdirectoriesA content collection is always a top-level folder inside of the src/content/
directory. You cannot nest one collection inside of another. However, you can use subdirectories to organize your content within a collection.
For example, you can use the following directory structure to organize i18n translations within a single docs
collection. When you query this collection, you’ll be able to filter the result by language using the file path.
src/content/
docs/ this collection uses subdirectories to organize by language
en/
- …
es/
- …
de/
- …
Defining Collections
Section titled Defining CollectionsTo get the most out of your content collections, create a src/content/config.ts
file in your project (.js
and .mjs
extensions are also supported). This is a special file that Astro will automatically load and use to configure your content collections.
// 1. Import utilities from `astro:content`
import { defineCollection } from 'astro:content';
// 2. Define your collection(s)
const blogCollection = defineCollection({ /* ... */ });
// 3. Export a single `collections` object to register your collection(s)
// This key should match your collection directory name in "src/content"
export const collections = {
'blog': blogCollection,
};
Setting up TypeScript
Section titled Setting up TypeScriptIf you do not already extend Astro’s strict
or strictest
recommended TypeScript settings in your tsconfig.json
file, you may need to update your tsconfig.json
to enable strictNullChecks
.
{
// Note: No change needed if you use "astro/tsconfigs/strict" or "astro/tsconfigs/strictest"
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"strictNullChecks": true
}
}
Defining a collection schema
Section titled Defining a collection schemaSchemas enforce consistent frontmatter within a collection. A schema guarantees that your frontmatter exists in a predictable form when you need to reference or query it. If any file violates its collection schema, Astro will provide a helpful error to let you know.
Schemas also power Astro’s automatic TypeScript typings for your content. When you define a schema for your collection, Astro will automatically generate and apply a TypeScript interface to it. The result is full TypeScript support when you query your collection, including property autocompletion and type-checking.
To create your first content schema, create a src/content/config.ts
file if one does not already exist (.js
and .mjs
extensions are also supported). This file should:
- Import the proper utilities from
astro:content
. - Define each collection that you’d like to validate with a schema.
- Export a single
collections
object to register your collections.
// 1. Import utilities from `astro:content`
import { z, defineCollection } from 'astro:content';
// 2. Define a schema for each collection you'd like to validate.
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
}),
});
// 3. Export a single `collections` object to register your collection(s)
export const collections = {
'blog': blogCollection,
};
Defining multiple collections
Section titled Defining multiple collectionsYou can use defineCollection()
as many times as you want to create multiple schemas. All collections must be exported from inside the single collections
object.
export const collections = {
'blog': defineCollection({ /* ... */ }),
'newsletter': defineCollection({ /* ... */ }),
'profile-authors': defineCollection({ /* ... */ }),
};
As your project grows, you are also free to reorganize your codebase and move logic out of the src/content/config.ts
file. Defining your schemas separately can be useful for reusing schemas across multiple collections and sharing schemas with other parts of your project.
// 1. Import your utilities and schemas
import { defineCollection } from 'astro:content';
import {blogSchema, newsletterSchema} from '../schemas';
// 2. Define your collections
const blogCollection = defineCollection({ schema: blogSchema });
const newsletterCollection = defineCollection({ schema: newsletterSchema });
// 3. Export multiple collections to register them
export const collections = {
'blog': blogCollection,
'newsletter': newsletterCollection,
};
Using third-party collection schemas
Section titled Using third-party collection schemasYou can import collection schemas from anywhere, including external npm packages. This can be useful when working with themes and libraries that provide their own collection schemas for you to use.
import {blogSchema} from 'my-blog-theme';
const blogCollection = defineCollection({ schema: blogSchema });
// Export the blog collection, using an external schema from 'my-blog-theme'
export const collections = {
'blog': blogCollection,
};
Defining datatypes with Zod
Section titled Defining datatypes with ZodAstro uses Zod to power its content schemas. With Zod, Astro is able to validate every file’s frontmatter within a collection and provide automatic TypeScript types when you go to query content from inside your project.
To use Zod in Astro, import the z
utility from "astro:content"
. This is a re-export of the Zod library, and it supports all of the features of Zod. See Zod’s README for complete documentation on how Zod works and what features are available.
// Example: A cheatsheet of many common Zod datatypes
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(),
}),
author: z.string().default('Anonymous'),
language: z.enum(['en', 'es']),
tags: z.array(z.string()),
// An optional frontmatter property. Very common!
footnote: z.string().optional(),
// Convert a standard date-string into a `Date` object
publishDate: z.string().transform(str => new Date(str)),
// Advanced: Validate that the string is also an email
authorContact: z.string().email(),
// Advanced: Validate that the string is also a URL
canonicalURL: z.string().url(),
})
})
Defining custom slugs
Section titled Defining custom slugsEvery content entry generates a URL-friendly slug
property from its file id
. The slug is used to query the entry directly from your collection. It is also useful when creating new pages and URLs from your content.
You can override an entry’s generated slug by adding your own slug
property to the file frontmatter. This is similar to the “permalink” feature of other web frameworks. "slug"
is a special, reserved property name that is not allowed in your custom collection schema
and will not appear in your entry’s data
property.
---
title: My Blog Post
slug: my-custom-slug/supports/slashes
---
Your blog post content here.
Querying Collections
Section titled Querying CollectionsAstro provides two functions to query a collection and return one (or more) content entries: getCollection()
and getEntryBySlug()
.
import { getCollection, getEntryBySlug } from 'astro:content';
// Get all entries from a collection. Requires the name of the collection as an argument.
const allBlogPosts = await getCollection('blog');
// Get a single entry from a collection. Requires the name of the collection and the entry's slug as arguments.
const oneBlogPost = await getEntryBySlug('blog', 'enterprise');
Both functions return content entries as defined by the CollectionEntry
type.
Filtering collection queries
Section titled Filtering collection queriesgetCollection()
takes an optional “filter” callback that allows you to filter your query based on an entry’s id
, slug
, or data
(frontmatter) properties.
You can use this to filter by any content criteria you like. For example, you can filter by frontmatter properties like draft
to prevent any draft blog posts from publishing to your blog:
// Example: Filter content entries with `draft: true` frontmatter
import { getCollection } from 'astro:content';
const draftBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
});
The filter argument also supports filtering by 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:
// Example: Filter entries by sub-directory in the collection
import { getCollection } from 'astro:content';
const englishDocsEntries = await getCollection('docs', ({ id }) => {
return id.startsWith('en/');
});
Using content in Astro templates
Section titled Using content in Astro templatesOnce you have queried your collection entries, you can access each entry directly inside of your Astro component template. This lets you to render HTML for things like links to your content (using the content slug
) or information about your content (using the data
property).
For information about rendering your content to HTML, see Rendering Content to HTML below.
---
import { getCollection } from 'astro:content';
const blogEntries = await getCollection('blog');
---
<ul>
{blogEntries.map(blogPostEntry => (
<li>
<a href={`/my-blog-url/${blogPostEntry.slug}`}>{blogPostEntry.data.title}</a>
<time datetime={blogPostEntry.data.publishedDate.toISOString()}>
{blogPostEntry.data.publishedDate.toDateString()}
</time>
</li>
))}
</ul>
Passing content as props
Section titled Passing content as propsA component can also pass an entire content entry as a prop.
If you do this, you can use the CollectionEntry
utility to correctly type your components props using TypeScript. This utility takes a string argument that matches the name of your collection schema, and will inherit all of the properties of that collection’s schema.
---
import type { CollectionEntry } from 'astro:content';
interface Props {
post: CollectionEntry<'blog'>;
}
// `post` will match your 'blog' collection schema type
const { post } = Astro.props;
---
Rendering content to HTML
Section titled Rendering content to HTMLOnce queried, you can render a collection entry to HTML using the entry render()
function property. Calling this function gives you access to rendered content and metadata, including both a <Content />
component and a list of all rendered headings.
---
import { getEntryBySlug } from 'astro:content';
const entry = await getEntryBySlug('blog', 'post-1');
const { Content, headings } = await entry.render();
---
<p>Written by: {entry.data.author}</p>
<Content />
Generating Routes from Content
Section titled Generating Routes from ContentContent collections are stored outside of the src/pages/
directory. You will need to create a new dynamic route to generate HTML pages from your collection entries. Your dynamic route will map the incoming request param (ex: Astro.params.slug
in src/pages/blog/[slug].astro
) to fetch the correct entry inside a collection.
The exact method for generating routes will depend on your build output
mode: ‘static’ (the default) or ‘server’ (for SSR).
Building for static output (default)
Section titled Building for static output (default)If you are building a static website (Astro’s default behavior), you would use the getStaticPaths()
function to create multiple pages from a single src/pages/
component during your build.
Call getCollection()
inside of getStaticPaths()
to query your content. Then, create your new URL paths using the slug
property of each content entry.
---
import { getCollection } from 'astro:content';
// 1. Generate a new path for every collection entry
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}
// 2. When it's time to render, you can get the entry directly from the prop
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<h1>{entry.data.title}</h1>
<Content />
This will generate a new page for every entry in the blog
collection. For example, an entry at src/content/blog/hello-world.md
will have a slug of hello-world
, and therefore its final URL will be /posts/hello-world/
.
Building for server output (SSR)
Section titled Building for server output (SSR)If you are building a dynamic website (using Astro’s SSR support), you are not expected to generate any paths ahead-of-time during the build. Instead, your page should examine the request (using Astro.request
or Astro.params
) to find the slug
on-demand, and then fetch it using getEntryBySlug()
.
---
import { getEntryBySlug } from 'astro:content';
// 1. Get the slug from the incoming server request
const { slug } = Astro.params;
// 2. Query for the entry directly using the request slug
const entry = await getEntryBySlug('blog', slug);
// 3. (Optional) render the entry to HTML in the template
const { Content } = await entry.render();
---
<h1>{entry.data.title}</h1>
<Content />
Modifying Frontmatter with Remark
Section titled Modifying Frontmatter with RemarkAstro supports remark or rehype plugins that modify your frontmatter directly. You can access this modified frontmatter inside of a content entry by using the remarkPluginFrontmatter
property returned from render()
:
---
import { getEntryBySlug } from 'astro:content';
const blogPost = await getEntryBySlug('blog', 'post-1');
const { remarkPluginFrontmatter } = await blogPost.render();
---
<!--
This example assumes `readingTime` was injected by a remark plugin.
See our "reading time" example for more details:
https://docs.astro.build/en/guides/markdown-content/#example-calculate-reading-time
-->
<p>{blogPost.data.title} — {remarkPluginFrontmatter.readingTime}</p>
The remark and rehype pipelines only runs when your content is rendered, which explains why remarkPluginFrontmatter
is only available after you call render()
on your content entry. In contrast, getCollection()
and getEntryBySlug()
cannot return these values directly because they do not render your content.