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
- Official Integrations - Astro DOCS
- Day18 - 實作集合分類功能 - Same article also published in the iThome Ironman Challenge