In case you missed it, Astro launched 2.0 with a couple of exciting announcements, one of which is the new Content collection API with Type-safety powered by Zod.
In a previous post, I wrote a summary of What's new in Astro 2.0; you can check it out for more information.
This post will look at creating Content collections and how you can start building and sourcing content in your Astro application.
We will be doing so by building a simple newsletter application, exploring creating a collection, querying the collection in components and creating routes for each markdown file in the collection.
Project setup
To follow the project in the blogpost, you can fork my content collection example project on Codesandbox. Create a fork, and you are good to go.
Pull the repo from GitHub and set up if you prefer to do this locally.
The parts that are covered is the Content/
, Pages/newsletter
folders. The file structure looks like and t
/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/
│ │ └── Card.astro
│ ├── content/
│ │ └── newsletter
│ │ ├── post-1.md
│ │ └── post-2.md
| ├── config.ts
│ ├── layouts/
│ │ └── Layout.astro
│ └── pages/
│ ├── newsletter
│ │ ├── [slug].astro
│ └── index.astro
└── package.json
Setting up Content Collections in Astro
A content collection is a group of .mdx
or .md
files that are created under the src/content
folder in Astro. The content files are type-safe and allow you to group your content in a schema and type-safe way. Once you have a collection, you can start querying your content using Astro’s built-in Content APIs.
Define collection schema
First, create a config.ts
file in the src/content
folder, which is the config file where you define the "collections" for your project. We describe the frontmatter schema in this file and use Zod to define types.
From the code block below, you can notice that we define the schema with some types. Doing so ensures that when we create new .md
or .mdx
files, the frontmatter is type-safe, and we get the benefits of Typescript in markdown.
import { z, defineCollection } from "astro:content";
// Define a collection of newsletter posts
const newsletterCollection = defineCollection({
// Define the schema
schema: z.object({
title: z.string().max(100),
date: z.date(),
categories: z.array(z.string()),
summary: z.string(),
image: z.string().optional(), //Image can be optional
})
});
// Export
export const collections = {
// collectionName: collection
newsletter: newsletterCollection,
};
Creating Markdown content in Astro
Since we are creating content collections, the markdown files must follow a "collection" type format. So in the src/content
folder, create a newsletter
folder this is where we add markdown files and inside those files add the frontmatter, following the schema format that is was defined.
Under the newsletter folder create a post-1.md
and then define the frontmatter with respect to the schema
---
title: "Newsletter Post 1"
date: 2019-01-01T00:00:00.000Z
categories: [Newsletter, News]
summary: "This is the first newsletter post."
---
# Newsletter Post 1
// Content
The frontmatter is type-safe, so if you add any field that isn't defined in the schema, you will get a prompt to fix the error. Thankfully Astro has very helpful error messages to prompt. Also, notice how no image is defined because, in the schema, it is marked as optional.
Importing Content collections to components
Now that you have defined the schema and have your first post up. You can pull your content data using the getCollection
from astro:content
.
Then pass your collection name into the getCollection
function. Doing so will give you an array that you can map through and display in a component.
You might need to restart your dev server after importing so that astro:content can be accessed.
import { getCollection } from 'astro:content';
const newsletters = await getCollection('newsletter');
// ...
{ newsletters.map((newsletter) => {
return (
<Card
href={`/newsletter/${newsletter.slug}`}
title={newsletter.data.title}
body={newsletter.data.summary}/>
)
})
}
Notice that the href
for the newsletter cards opens up a path /newsletter/${newsletter.slug}
. This is because, in Astro, all pages are created under the pages folder so, we create a newsletter
folder under pages to make routes for each post.
You can make multiple routes for each collection that you have.
Generate Pages for Content collections
If you are familiar with creating dynamic routes, they are often depicted with []
and have an identifier to create the routes based on the identifier.
For example, it can be making each route based on the id:[name.id]
or on the folder's name [Professions.name]
.
To create a new page for each of the newsletter entries under the Pages folder, create newsletters/[slug].astro
; this will create a new page for each post.
Of course, you can also define custom slugs for your files, but we will stick with the generated ones.
First, get the collection; you will use the getCollection
function again.
import { getCollection } from "astro:content";
Then, because we want to create paths from these collections at build time, we make a function getStaticPaths
. In this function, we will pass the newsletter collection into getCollection
and await the response.
This response is then mapped out to create a slug for each post at build time, the params Object shows what file is rendered, as seen below:
// This function gets called at build time and generates the paths from the content folder
export async function getStaticPaths() {
// Define the colection you are creating pages for
const allNewsletters = await getCollection("newsletter");
return allNewsletters.map((newsletter) => {
return {
params: {slug: newsletter.slug},
//
props:{newsletter},
};
});
}
Access props and rendering markdown for Content collections.
Now that we have defined the newsletter, we would need to access the props in this particular component, and we can do that with Astro.props
to typecast this even further add a
{CollectionEntry<>}
type and pass in newsletter
as seen below:
---
import {CollectionEntry, getCollection } from "astro:content";
//..
// Access the props in this component
const {newsletter} = Astro.props as {newsletter: CollectionEntry<"newsletter">};
const {Content} = await newsletter.render();
---
To get the actual content from the post-1.md
file, call the render
function pass in the Content.
We can now render the content like so:
<main>
<h1>{newsletter.data.title}</h1>
<p>Category: {newsletter.data.categories.join(',')}</p>
<Content />
<p>
<a href="/">Back to previous newsletters</a>
</p>
</main>
Learn more
Astro's content collection API allows for so flexibility and type-safety when it comes to handling data. There is also a guide for migrating from File-based routing if you want to update your projects.
To get started with this example project, you can create a fork of my Content-collection API project on CodeSandbox. Happy coding 👋🏾
Top comments (4)
Glad to see this post. Recently started building an e-commerce front end with astro and its absolute 🔥
@obinnaspeaks great article. I'm finding difficult to create a [tag].astro page that allows me to filter the post base on their tag for a server mode. Do you know where can I find a guide for this?
Hey @nicolas I reckon you mean for SSR. I didn't cover it in this bit but I can update. It's similar to how you would handle a static render. Check this doc out docs.astro.build/en/guides/content...
this article is help me