title | description | i18nReady |
---|---|---|
Tutorial - Extend with Content Collections |
Convert the Build a Blog tutorial code from file-based routing to content collections. |
true |
import PackageManagerTabs from '/components/tabs/PackageManagerTabs.astro';
import Box from '/components/tutorial/Box.astro';
import MultipleChoice from '/components/tutorial/MultipleChoice.astro';
import PreCheck from '/components/tutorial/PreCheck.astro';
import Option from '~/components/tutorial/Option.astro';
import { Steps } from '@astrojs/starlight/components';
Content collections are a powerful way to manage groups of similar content, such as blog posts. Collections help to organize your documents, validate your YAML frontmatter, and provide automatic TypeScript type-safety for all of your content (even if you don't write any TypeScript yourself).
- Move your folder of blog posts into `src/content/` - Create a schema to define your blog post frontmatter - Use `getCollection()` to get blog post content and metadataYou will need an existing Astro project with Markdown or MDX files in the src/pages/
folder.
This tutorial uses the Build a Blog tutorial's finished project to demonstrate converting a blog to content collections. You can fork and use that codebase locally, or complete the tutorial in the browser by editing the blog tutorial's code in StackBlitz.
You can instead follow these steps with your own Astro project, but you will need to adjust the instructions for your codebase.
We recommend using our sample project to complete this short tutorial first. Then, you can use what you have learned to create content collections in your own project.
In the Build a Blog introductory tutorial, you learned about Astro's built-in file-based routing: any .astro
, .md
, or .mdx
file anywhere within the src/pages/
folder automatically became a page on your site.
To create your first blog post at https://example.com/posts/post-1/
, you created a /posts/
folder and added the file post-1.md
. You then added a new Markdown file to this folder every time you wanted to add a new blog post to your site.
Even when using content collections, you will still use the src/pages/
folder for individual pages, such as your About Me page. But, moving your blog posts to the special src/content/
folder will allow you to use more powerful and performant APIs to generate your blog post index and display your individual blog posts.
At the same time, you'll receive better guidance and autocompletion in your code editor because you will have a schema to define a common structure for each post that Astro will help you enforce. In your schema, you can specify when frontmatter properties are required, such as a description or an author, and which data type each property must be, such as a string or an array. This leads to catching many mistakes sooner, with descriptive error messages telling you exactly what the problem is.
Read more about Astro's content collections in our guide, or get started with the instructions below to convert a basic blog from src/pages/posts/
to src/content/posts/
.
-
Which type of page would you probably keep in
Blog posts that all contain the same basic structure and metadata Product pages in an eCommerce site A contact page, because you do not have multiple similar pages of this typesrc/pages/
? -
Which is not a benefit of moving blog posts to a content collection?
Pages are automatically created for each file Better error messages, because Astro knows more about each file Better data fetching, with a more performant function -
Content collections uses TypeScript...
To make me feel bad To understand my project, even if I don't write any TypeScript Only if I have the `strict` or `strictest` configuration set
The steps below show you how to extend the final product of the Build a Blog tutorial by creating a content collection for the blog posts.
1. Upgrade to the latest version of Astro, and upgrade all integrations to their latest versions by running the following commands in your terminal:<PackageManagerTabs>
<Fragment slot="npm">
```shell
# Upgrade to Astro v4.x
npm install astro@latest
# Example: upgrade the blog tutorial Preact integration
npm install @astrojs/preact@latest
```
</Fragment>
<Fragment slot="pnpm">
```shell
# Upgrade to Astro v4.x
pnpm add astro@latest
# Example: upgrade the blog tutorial Preact integration
pnpm add @astrojs/preact@latest
```
</Fragment>
<Fragment slot="yarn">
```shell
# Upgrade to Astro v4.x
yarn add astro@latest
# Example: upgrade the blog tutorial Preact integration
yarn add @astrojs/preact@latest
```
</Fragment>
</PackageManagerTabs>
:::tip
If you are using your own project, then be sure to update any dependencies you have installed. The example blog tutorial codebase only uses the Preact integration.
:::
-
The blog tutorial uses the
base
(least strict) TypeScript setting. In order to use content collections, you must set up TypeScript for content collections either by using thestrict
orstrictest
setting, or by adding two options intsconfig.json
.In order to use content collections without writing TypeScript in the rest of the blog tutorial example, add the following two TypeScript configuration options to the config file:
{ // Note: No change needed if you use "astro/tsconfigs/strict" or "astro/tsconfigs/strictest" "extends": "astro/tsconfigs/base", "compilerOptions": { "strictNullChecks": true, "allowJs": true } }
-
Move all your existing blog posts (
.md
files) fromsrc/pages/posts/
into this new collection. -
Create a
src/content/config.ts
file to define a schema for yourpostsCollection
. For the existing blog tutorial code, add the following contents to the file to define all the frontmatter properties used in its blog posts:// Import utilities from `astro:content` import { z, defineCollection } from "astro:content"; // Define a `type` and `schema` for each collection const postsCollection = defineCollection({ type: 'content', schema: z.object({ title: z.string(), pubDate: z.date(), description: z.string(), author: z.string(), image: z.object({ url: z.string(), alt: z.string() }), tags: z.array(z.string()) }) }); // Export a single `collections` object to register your collection(s) export const collections = { posts: postsCollection, };
-
In order for Astro to recognize your schema, quit the dev server (
CTRL + C
) and run the following command:npx astro sync
. This will define theastro:content
module for the Content Collections API. Restart the dev server to continue with the tutorial.
-
Add the following code to query your collection to make each blog post's slug and page content available to each page it will generate:
--- import { getCollection } from 'astro:content'; import MarkdownPostLayout from '../../layouts/MarkdownPostLayout.astro'; export async function getStaticPaths() { const blogEntries = await getCollection('posts'); return blogEntries.map(entry => ({ params: { slug: entry.slug }, props: { entry }, })); } const { entry } = Astro.props; const { Content } = await entry.render(); ---
-
Render your post
<Content />
within the layout for Markdown pages. This allows you to specify a common layout for all of your posts.--- import { getCollection } from 'astro:content'; import MarkdownPostLayout from '../../layouts/MarkdownPostLayout.astro'; export async function getStaticPaths() { const blogEntries = await getCollection('posts'); return blogEntries.map(entry => ({ params: { slug: entry.slug }, props: { entry }, })); } const { entry } = Astro.props; const { Content } = await entry.render(); --- <MarkdownPostLayout frontmatter={entry.data}> <Content /> </MarkdownPostLayout>
-
Remove the
layout
definition in each individual post's frontmatter. Your content is now wrapped in a layout when rendered, and this property is no longer needed.--- layout: ../../layouts/MarkdownPostLayout.astro title: 'My First Blog Post' pubDate: 2022-07-01 ... ---
```astro title="src/pages/blog.astro" "post.data" "getCollection(\"posts\")" "/posts/${post.slug}/" del={7} ins={2,8}
---
import { getCollection } from "astro:content";
import BaseLayout from "../layouts/BaseLayout.astro";
import BlogPost from "../components/BlogPost.astro";
const pageTitle = "My Astro Learning Blog";
const allPosts = Object.values(await import.meta.glob("../pages/posts/*.md", { eager: true }));
const allPosts = await getCollection("posts");
---
```
-
You will also need to update references to the data returned for each
post
. You will now find your frontmatter values on thedata
property of each object. Also, when using collections eachpost
object will have a pageslug
, not a full URL.--- import { getCollection } from "astro:content"; import BaseLayout from "../layouts/BaseLayout.astro"; import BlogPost from "../components/BlogPost.astro"; const pageTitle = "My Astro Learning Blog"; const allPosts = await getCollection("posts"); --- <BaseLayout pageTitle={pageTitle}> <p>This is where I will post about my journey learning Astro.</p> <ul> { allPosts.map((post) => ( <BlogPost url={post.url} title={post.frontmatter.title} />)} <BlogPost url={`/posts/${post.slug}/`} title={post.data.title} /> )) } </ul> </BaseLayout>
-
The tutorial blog project also dynamically generates a page for each tag using
src/pages/tags/[tag].astro
and displays a list of tags atsrc/pages/tags/index.astro
.Apply the same changes as above to these two files: - fetch data about all your blog posts using `getCollection("posts")` instead of using `import.meta.glob()` - access all frontmatter values using `data` instead of `frontmatter` - create a page URL by adding the post's `slug` to the `/posts/` path The page that generates individual tag pages now becomes: ```astro title="src/pages/tags/[tag].astro" "post.data.tags" "getCollection(\"posts\")" "post.data.title" ins={2} "/posts/${post.slug}/" --- import { getCollection } from "astro:content"; import BaseLayout from "../../layouts/BaseLayout.astro"; import BlogPost from "../../components/BlogPost.astro"; export async function getStaticPaths() { const allPosts = await getCollection("posts"); const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())]; return uniqueTags.map((tag) => { const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag) ); return { params: { tag }, props: { posts: filteredPosts }, }; }); } const { tag } = Astro.params; const { posts } = Astro.props; --- <BaseLayout pageTitle={tag}> <p>Posts tagged with {tag}</p> <ul> { posts.map((post) => <BlogPost url={`/posts/${post.slug}/`} title={post.data.title} />) } </ul> </BaseLayout> ``` <Box icon="puzzle-piece"> ### Try it yourself - Update the query in the Tag Index page Import and use `getCollection` to fetch the tags used in the blog posts on `src/pages/tags/index.astro`, following the [same steps as above](#replace-importmetaglob-with-getcollection). <details> <summary>Show me the code.</summary> ```astro title="src/pages/tags/index.astro" "post.data" "getCollection(\"posts\")" ins={2} --- import { getCollection } from "astro:content"; import BaseLayout from "../../layouts/BaseLayout.astro"; const allPosts = await getCollection("posts"); const tags = [...new Set(allPosts.map((post) => post.data.tags).flat())]; const pageTitle = "Tag Index"; --- ... ``` </details>
In the blog tutorial example, `pubDate` was a string. Now, according to the schema that defines types for the post frontmatter, `pubDate` will be a `Date`
object.
To render the date in the blog post layout, convert it to a string:
```astro title="src/layouts/MarkdownPostLayout.astro" ins="toString()"
...
<BaseLayout pageTitle={frontmatter.title}>
<p>{frontmatter.pubDate.toString().slice(0,10)}</p>
<p><em>{frontmatter.description}</em></p>
<p>Written by: {frontmatter.author}</p>
<img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
...
```
```js title="src/pages/rss.xml.js" del={2,11} ins={3,6,12-17}
import rss from '@astrojs/rss';
import { pagesGlobToRssItems } from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection("posts");
return rss({
title: 'Astro Learner | Blog',
description: 'My journey learning Astro',
site: context.site,
items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
items: posts.map((post) => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/posts/${post.slug}/`,
})),
customData: `<language>en-us</language>`,
})
}
```
For the full example of the blog tutorial using content collections, see the Content Collections branch of the tutorial repo.