Day18 - Astro Series: Content Collection Category

A beautiful gradient background with a heading: "Implementing Collection Category Feature"

Introduction

Previously we learned about content collections and tabs. Today’s exercise uses the data retrieved from collections to build category pages.

Defining the problem

As you publish more posts in a content collection, you may have many posts of the same nature that you want to group and categorize, and automatically generate pages for each category for easier browsing. The goals are:

  • Add category data to each post
  • Create a page that displays all categories present in the current collection
  • Create individual category pages that show all posts for that category

Adding category data to each post

Define the category field in the collection’s configuration. If no value is provided, default to the string unsorted. (Remember to return the post collection.)

const post = defineCollection({
schema: z.object({
isDraft: z.boolean().default(true),
category: z.string().default('unsorted'),
}),
});

Once the collection is defined, you can start writing Markdown or MDX files in content.

Creating a page that lists all categories in the current collection

Create index.astro inside post/categories, and retrieve the category arrays from the posts available in the post collection. Flatten and put them into a Set (since multiple posts may share the same category, duplicates must be removed), then sort alphabetically to obtain categories, which are all existing unique category names in the current collection.

const publishedPosts = await getCollection('post', (post) => (import.meta.env.PROD ? !post.data.isDraft : true));
const categories = [...new Set(publishedPosts.map((post) => post.data.category).flat())].sort((a, b) =>
a.localeCompare(b, 'en', { sensitivity: 'base' }),
);

Then simply render this data in the template.

{
categories.map((category) => (
<ul>
<li class="text-3xl">
<Card>
<a href={`/post/categories/${category}`}>{category}</a>
</Card>
</li>
</ul>
));
}

Creating individual category pages that show all posts for that category

This requires dynamic routes and the getStaticPaths function you learned earlier. First create a dynamic route file: pages/post/categories/[slug].astro, and fetch all existing unique category names in the collection:

export async function getStaticPaths() {
/* 1. Retrieve all unique category names that exist in the current collection; the data will roughly look like this:
[
'A-Category Name', 'B-Category Name', 'C-Category Name',
]
*/
const publishedPosts = await getCollection('post', (post) => (import.meta.env.PROD ? !post.data.isDraft : true));
const categories = [...new Set(publishedPosts.map((post) => post.data.category).flat())];
/* 2. Finally, the `getStaticPaths` function is created to receive the expected data.
[
{ params: { slug: 'A-分類名稱' }, props: { publishedPosts: [Array] } },
{ params: { slug: 'B-分類名稱' }, props: { publishedPosts: [Array] } },
{ params: { slug: 'C-分類名稱' }, props: { publishedPosts: [Array] } },
]
*/
return categories.flatMap((category) => [{ params: { slug: category }, props: { publishedPosts } }]);
}

Using the above, getStaticPaths will generate pages per category and also pass publishedPosts as props for each page. Then filter the posts that match the slug and sort them by date.

const { slug } = Astro.params;
const { publishedPosts } = Astro.props;
const avaliableCurrentCategoryPosts = publishedPosts
.filter((post) => post.data.category === slug)
.sort((a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime());

Conclusion

This category feature implementation uses dynamic routes and content collections to generate category pages for posts. You can view how I implemented category pages on my blog on GitHub: https://github.com/riceball-tw/astro-blog/blob/main/src/pages/post/categories/%5Bslug%5D.astro🔗

Further reading