Astro
Astro 2.0: Content Collections
Updated:
This tutorial will teach you what content collections are and how they work in Astro 2.0.
After creating a new Astro 2.0 project from scratch, we will create a content collection for blog posts. We will also define a collection schema that will validate the Frontmatter of our blog posts, making it typesafe. Finally, we will learn how to generate dynamic routes for each post in the blog content collection.
This tutorial assumes you are already familiar with how dynamic routes work in Astro. If this concept is new to you, we highly recommend you read Astro’s official docs to familiarize yourself with them. Throughout the tutorial, we will have links to various sections of the docs and external resources where you can learn more.
We’ve got a lot to cover, so let’s get started.
What are content collections?
Content collections are one of the latest features released in Astro 2.0. They allow you to better organize your markdown and MDX content via the new src/content
directory and provide type safety for your Frontmatter and content. You can also query content collections to pull them into your templates and generate routes for each piece of content.
This may all sound a bit abstract at the moment, but don’t worry. We will cover everything in detail throughout this tutorial, and by the end, you will be ready to create content collections in your own Astro projects.
Creating a new Astro 2.0 project
We can create an Astro 2.0 project by entering the following into our terminal.
npm create astro@latest
When asked if you would like to install the latest Astro package, press y
on your keyboard for yes.
- Next, you will be prompted to give your project a name. I will use the name
astro-content-collections
, but you can name it whatever you want. Press Enter to continue.
Larger screenshots
You can click on the screenshots to see larger versions.
- Then, select “a few best practices (recommended).”
- Next, press
y
to install NPM dependencies - Next, you are asked to initialize a new Git repository. Press
n
for no, as we won’t need it for this tutorial. - Finally, select “strict” for the TypeScript setup.
After your project has been created, cd
into your project directory.
cd <project name>
# For example
# cd astro-content-collections
Running the dev server
To start the Astro dev server, enter the following in the terminal.
npm run dev
The dev server will serve your app at http://localhost:3000. You should see the following if you open this URL in your browser.
We are ready to create our blog content collection now that we have created our project.
Define a new collection schema
The first thing we will need to do is to create two new folders. Within the src
folder, create a new folder called content
. Within the content
folder, create a new folder called blog
. The entire file path should be src/content/blog
.
Creating our blog posts
Next, we will need to create some blog posts.
For demonstration purposes, these blog posts will be very simple. The most important part is the Frontmatter, as we will soon learn how to make all of the data contained in it typesafe.
src/content/blog/post-1.md
---
title: Blog post 1
author: John Doe
isDraft: false
publishedDate: 02-01-2023
tags:
- Web Development
- JavaScript
- Astro
image: https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80
canonicalURL: https://localhost:3000/blog/blog-post-1
---
# Blog post 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc fermentum dignissim fermentum. Nunc eget semper dui. Fusce commodo placerat dictum. Curabitur nec mauris eu mi condimentum fermentum. Donec a magna accumsan, sodales leo consequat, venenatis ligula. Vivamus a pharetra est. Nam posuere dolor sed tortor suscipit, ac feugiat magna convallis.
src/content/blog/post-2.md
---
title: Blog post 2
author: John Doe
isDraft: false
publishedDate: 02-01-2023
tags:
- Web Development
- JavaScript
- Astro
image: https://images.unsplash.com/photo-1498050108023-c5249f4df085?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1472&q=80
canonicalURL: https://localhost:3000/blog/blog-post-2
---
# Blog post 2
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc fermentum dignissim fermentum. Nunc eget semper dui. Fusce commodo placerat dictum. Curabitur nec mauris eu mi condimentum fermentum. Donec a magna accumsan, sodales leo consequat, venenatis ligula. Vivamus a pharetra est. Nam posuere dolor sed tortor suscipit, ac feugiat magna convallis.
src/content/blog/post-3.md
---
title: Blog post 3
author: John Doe
isDraft: false
publishedDate: 02-01-2023
tags:
- Web Development
- JavaScript
- Astro
image: https://images.unsplash.com/photo-1537884944318-390069bb8665?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80
canonicalURL: https://localhost:3000/blog/blog-post-2
---
# Blog post 3
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc fermentum dignissim fermentum. Nunc eget semper dui. Fusce commodo placerat dictum. Curabitur nec mauris eu mi condimentum fermentum. Donec a magna accumsan, sodales leo consequat, venenatis ligula. Vivamus a pharetra est. Nam posuere dolor sed tortor suscipit, ac feugiat magna convallis.
Defining our blog collection schema
Now that we have some blog posts, we need to define a collection schema for our blog content collection.
Create a file called config.ts
inside of src/content
.
We first need to import the Zod validation library and the defineCollection()
function from Astro:content
.
// src/content/config.ts
import { z, defineCollection } from 'astro:content'
Next, we define the schema for the content collection we want to validate. In our case, we need to define a schema for our blog collection.
// src/content/config.ts
import { z, defineCollection } from 'astro:content'
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string(),
isDraft: z.boolean(),
publishDate: z.string().transform((str) => new Date(str)),
tags: z.array(z.string()),
image: z.string().optional(),
canonicalURL: z.string().url(),
}),
})
First, we are creating a new constant called blogCollection
and using the defineCollection
function to define our schema.
const blogCollection = defineCollection({
Next, we pass the defineCollection
function an object
with a schema
property.
schema: z.object({
title: z.string(),
author: z.string(),
isDraft: z.boolean(),
publishDate: z.string().transform(str => new Date(str)),
tags: z.array(z.string()),
image: z.string().optional(),
canonicalURL: z.string().url(),
}),
The schema property is an object containing all of the Frontmatter data in our blog posts with their respective types. This is how Astro can validate and ensure our Frontmatter is typesafe!
Zod library
The z
object comes from the Zod, a TypeScript first schema validation library. Please check out their docs for more info.
A couple of interesting things worth pointing out.
publishDate
is of type string, but we are also using the .transform() method from Zod to transform it into a JavaScript Date object.tags
is an array of strings denoted byz.array(z.string())
.image
is of type string and also optional. We can make any type optional by using the .optional() method.
Required by default
By default, Zod makes everything required. You have to explicitly tell it when something is optional.
Finally, we need to export our schema from our config file. Here is the entire src/content/config.ts
file in its entirety.
import { z, defineCollection } from 'astro:content'
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string(),
isDraft: z.boolean(),
publishDate: z.string().transform((str) => new Date(str)),
tags: z.array(z.string()),
image: z.string().optional(),
canonicalURL: z.string().url(),
}),
})
export const collections = {
blog: blogCollection,
}
Querying our blog collection
Now that we have defined our collection schema, we are ready to learn how to query our blog content collection and render its content.
Update src/pages/index.astro
with the following code.
---
import { getCollection } from "astro:content";
const blogPosts = await getCollection("blog");
---
{blogPosts}
First, we import the getCollection method from astro:content
.
import { getCollection } from 'astro:content'
Next, we await
the getCollection method, which retrieves all the items within a given content collection. Our collection is called “blog,” so we pass the name of our collection as a string to getCollection()
.
const blogPosts = await getCollection('blog')
Finally, we render our blog posts.
{
blogPosts
}
Now if you open your browser to http://localhost:3000, you should see the following error.
Our schema validation is working!
The error states that publishDate
is required, but our Frontmatter has publishedDate
. Hopefully, you can now begin to see how powerful and useful having typesafe frontmatter is.
We can solve this by updating our src/content/config.ts
like so.
import { z, defineCollection } from 'astro:content'
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string(),
isDraft: z.boolean(),
publishedDate: z.string().transform((str) => new Date(str)), // updated from publishDate
tags: z.array(z.string()),
image: z.string().optional(),
canonicalURL: z.string().url(),
}),
})
export const collections = {
blog: blogCollection,
}
Now if you visit http://localhost:3000, you should see the following.
We can render our blog post content properly by updating src/pages/index.astro
with the following.
---
import { getCollection } from "astro:content";
const blogPosts = await getCollection("blog");
---
{
blogPosts.map((post) => (
<h2>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</h2>
))
}
We use .map() to iterate over each post inside our blog content collection to create a link for each blog post title.
If you try and click on one of the titles, you will get a 404 error page.
This is happening because we have not told Astro how to create dynamic routes for each of our blog posts.
Let’s do that now!
Dynamic routes from content collections
Now that we have our blog posts on the home page, we need to tell Astro how to generate a route for every post with dynamic routes.
Dynamic routes
If you are unfamiliar with how dynamic routes work in Astro, please check read the docs: https://docs.astro.build/en/core-concepts/routing/#dynamic-routes
First, create a folder called blog
inside of the src/pages
directory. Inside the newly created blog
folder, create a file called [slug].astro
.
Paste the following.
---
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const blogEntries = await getCollection("blog");
return blogEntries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Content />
First, we are importing the getCollection
function from astro:content
. We learned about this function in the previous section, where we learned how to query for our blog collection content.
import { getCollection } from 'astro:content'
Next, we are exporting an async function called getStaticPaths() which Astro provides. This function is what Astro uses to generate dynamic routes.
export async function getStaticPaths() {}
Inside getStaticPaths()
, we await
the getCollection()
function, which returns all of the posts from the blog content collection.
export async function getStaticPaths() {
const blogEntries = await getCollection('blog')
}
Then, we use .map() to iterate over the blog posts, returning an object with two properties.
- The first is a
params
property which contains the slug of our blog post. Astro expects the slug from each post since we named this file[slug].astro
.- By default, Astro will use the file name of each post for the slug, which in our case is
post-1
,post-2
, andpost-3
.
- By default, Astro will use the file name of each post for the slug, which in our case is
- The second property,
props
, returns each blog post's content.
export async function getStaticPaths() {
const blogEntries = await getCollection('blog')
return blogEntries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}))
}
Finally, we get the data returned from the getStaticPaths()
function and render the blog content using a special Content component provided by Astro.
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Content />
Now if you visit http://localhost:3000 and click on one of the blog post titles, you should see the post’s content.
Wrap up
We covered a lot of material in this tutorial, so let’s quickly recap everything you learned.
- First, you learned what content collections are.
- Next, you learned how to create a new Astro 2.0 project.
- Then, you learned how to define a collection schema.
- Then, you learned how to query the blog content collection to render each blog post onto the home page.
- Finally, you learned how to create dynamic routes for each blog post within the blog content collection.