Skip to content

How to add categories to AstroPaper theme

Published: at 01:00 AM
Time to read: 10 min
Author: Mouad Nassri
Featured image for How to add categories to AstroPaper theme

AstroPaper is a fantastic Astro theme built for bloggers, and comes with a simple but flexible design along with accessibility and performance in mind. You can customize anything within the site according to your taste and add posts and content easily.

There is a missing ring within this theme, which is categories. In this article I want to share with you a really simple way to add categories to this theme, and divide your rich content into categories to organize your blog.

Requirements

Before doing anything, let’s see what we want to achieve exactly. We want the blog posts to be organized within categories by taking into consideration the following points:

Implementation

Now that we know what we want to chieve, let’s start. First let’s create a fresh project :

Setup the project

Go to the location where you want to initialize the project, and run the following command to setup a new project with AstroPaper theme:

npm create astro@latest -- --template satnaing/astro-paper

Then, name you project and click Enter.

Then, choose whether you want to use Typescript or no, in this tutorial we will not use typescript to make things simple, and I’ll create a code snippets in github to cover typescript steps and changes you need to add to make it work.

Install dependencies ? Click yes to allow him install everything for you.

Initialize a new git repository? Click on yes in case you want to use git for tracking changes on your project, otherwise click no and continue.

Then wait for a while till the dependencies and theme to be loaded and installed.

After that cd to your project and open it in your favorite code editor, and run the following command to start the local server:

npm run dev

At this point, you should be able to see the project up and running.

The changes should be made in the right order in order for the feature to work, otherwise, you may encounter some issues and erros.

Add categories header button

Go to src/components/Header.astro file, where you will find a ul element that include Posts, Tags and about buttons, add the categories button by injecting the following code after posts button:

<li>
  <a
    href="/categories/"
    class={activeNav === "categories" ? "active" : ""}
  >
    Categories
  </a>
</li>

After adding this code, you should see an error in activeNav === “categories” ! To fix it, within the same file, at the top, there is an interface called Props:

export interface Props {
  activeNav?:
    | "posts"
    | "tags"
    | "about"
    | "search"
    | "categories"; // Add categories like so
}

Now this button is responsible to take the user to categories index page, where he can see all the categories within the blog. But before creating categories index page, we need to know how to make a post belong to a category !? Easy follow the next steps

Categorizing posts

The first thing to do is to add categories entry in the frontmatter of the post you want to categorize. Open any post you already have inside src/content/blog, in the top of the file, add categories like following:

---
author: Sat Naing
pubDatetime: 2022-09-23T15:22:00Z
modDatetime: 2023-12-21T09:12:47.400Z
title: Adding new posts in AstroPaper theme
slug: adding-new-posts-in-astropaper-theme
featured: true
categories: ["Astro tutorials", "Web Developement"]
draft: false
tags:
  - docs
description:
  Some rules & recommendations for creating or adding new posts using AstroPaper
  theme.
---

This code between the three dashes called frontmatter, where you can set metadata for your post. This is where we will set the post categories. The value is an array of categories names. It is array because the post can belong to multiple categories at the same time.

Define description to categories :

Let’s create a file where we can define a brief description about each category we have defined in our posts:

export const descriptions = {
  Others:
    "Posts concern general topics and anything about anything :)",
  "Web Developement":
    "Posts relevent to web developement to cover tutorials, code-snippets, tips and best practices in different technologies and areas.",
  "Astro Tutorials":
    "Tutorials and how-to blog posts to master Astro",
};

This file, simply includes an object that takes the category name as key, and the description as its value. Now in your case, you need to look at your posts or later when we create the categories index file, and for each category you have, add a brief description to it.

Also, we will handle the case where the category does not have a description the category component to show a general description instead of empty string.

Get categories from posts

First we need to add categories to ther schema of the posts collection, and when we get the posts, we need to get the categories as well.

Go to src/content/config.ts, and add categories as following:

const blog = defineCollection({
  type: "content",
  schema: ({ image }) =>
    z.object({
      author: z.string().default(""),
      pubDatetime: z.date(),
      modDatetime: z.date().optional().nullable(),
      title: z.string(),
      featured: z.boolean().optional(),
      draft: z.boolean().optional(),
      categories: z.array(z.string()).default(["Others"]), // Add this line
      tags: z.array(z.string()).default(["others"]),
      ogImage: image()
        .refine(
          img => img.width >= 1200 && img.height >= 630,
          {
            message:
              "OpenGraph image must be at least 1200 X 630 pixels!",
          }
        )
        .or(z.string())
        .optional(),
      description: z.string(),
      canonicalURL: z.string().optional(),
      timeToRead: z.string().optional(),
    }),
});

Now we will extract unique categories from all blog posts, and get an object with category name, slug and the last post for each category.

import { slugifyStr } from "./slugify";
import {
  getCollection,
  type CollectionEntry,
} from "astro:content";
import postFilter from "./postFilter";

interface Category {
  category: string;
  slug: string;
  lastPost: CollectionEntry<"blog">;
}

const getUniqueCategories = async () => {
  const posts = await getCollection("blog");

  const categories: Category[] = posts
    .filter(postFilter)
    .flatMap(post => post.data.categories)
    .map(category => ({
      slug: slugifyStr(category),
      category: category,
      lastPost: posts
        .filter(p => p.data.categories.includes(category))
        .sort(
          (a, b) =>
            new Date(b.data.modDatetime as Date).getTime() -
            new Date(a.data.modDatetime as Date).getTime()
        )[0],
    }))
    .filter(
      (value, index, self) =>
        self.findIndex(
          category => category.category === value.category
        ) === index
    )
    .sort((categoryA, categoryB) =>
      categoryA.category.localeCompare(categoryB.category)
    );

  return categories;
};

export default getUniqueCategories;

This helper function loops through posts within src/content/blog, and extract unique categories by filtring and sorting the result, and for eacg category, we get an object that hold the category name, slug and the reference to the last post to make it easier for us to show categories in the index page.

Create categories index page

Categories index page is responsible to show all the categories available within your blog posts. But before showing categories, let’s create a component that represents a category to make our code clean and neat.

import { descriptions } from "@pages/categories/descriptions";
import { slugifyStr } from "@utils/slugify";
import type { CollectionEntry } from "astro:content";
import React from "react";

type CategoryProps = {
  title: string;
  lastPost?: CollectionEntry<"blog">;
};

type CategoryKeys = keyof typeof descriptions;

function Category({ title, lastPost }: CategoryProps) {
  return (
    <li className="my-6">
      <a
        href={`/categories/${slugifyStr(title)}`}
        className="text-skin-accent inline-block text-lg font-medium decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
      >
        <h2>{title}</h2>
      </a>
      <div className="my-1 flex gap-1">
        <a
          href={`/posts/${lastPost?.slug}`}
          className="text-sm"
        >
          <b>Lastest post</b>: {lastPost?.data.title}
        </a>{" "}
      </div>
      <p className="text-sm">
        <b>Description</b>:{" "}
        {descriptions[title as CategoryKeys] ??
          `Posts relevant to everyting related to ${title.toLowerCase()} `}
      </p>
    </li>
  );
}

export default Category;

Now let’s create the categories index page:

---
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import getUniqueCategories from "@utils/getUniqueCategories";
import Category from "@components/Category";

let categories = await getUniqueCategories();
---

<Layout title="Categories">
  <Header activeNav="categories" />
  <Main
    pageTitle="Categories"
    pageDesc="All categories and areas our blog cover."
  >
    {
      categories.length ? (
        <ul>
          {categories.map(({ category, lastPost }) => (
            <Category
              client:load
              title={category}
              {lastPost}
            />
          ))}
        </ul>
      ) : (
        <div class="flex flex-col items-center justify-center gap-6">
          <h2 class="text-xl font-bold tracking-wide">
            Empty categories list !
          </h2>
          <p>
            We're working to add new content and cover
            different categories
          </p>
          <p class="block text-lg font-medium text-gray-500">
            Stay tunned !
          </p>
        </div>
      )
    }
  </Main>
  <Footer />
</Layout>

Now If you go to your blog posts and start assign categories to them, and go back to categories page after, you’ll see all categories shown.

We need to handle the action where the user click on category link, we need to render only posts within that category. To handle this situation, follow with me:

---
import { getCollection } from "astro:content";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import { SITE } from "@config";
import getUniqueCategories from "@utils/getUniqueCategories";
import Card from "@components/Card";

export const getStaticPaths = async () => {
  const categories = await getUniqueCategories();

  // First define routes for categories normally
  const categoriesPaths = categories.map(category => ({
    params: { slug: category.slug },
    props: { category: category.category },
  }));

  return [...categoriesPaths];
};

const { category } = Astro.props;

const posts = await getCollection("blog");
const categoryPosts = posts.filter(post =>
  post.data.categories.includes(category)
);
---

<Layout title={`${category} | ${SITE.title}`}>
  <Header activeNav="categories" />
  <Main
    pageTitle={category + "'s posts"}
    pageDesc={`All posts within "${category}" category`}
  >
    <ul>
      {
        categoryPosts.map(({ data, slug }) => (
          <Card
            href={`/posts/${slug}/`}
            frontmatter={data}
          />
        ))
      }
    </ul>
  </Main>
  <Footer />
</Layout>

Now, when you click on any category in the categories index page, you should see all the posts relevant to that category. In case you don’t have any category or all your posts do not have any category, you should see a category called Others that hold all the posts.

In the next tutorial, I will share with you how to add pagination to the categories in cases where a category have multiple posts.

I hope you find this blog post useful, and don’t forget to share it with you friends.

🔗 SOURCE CODE HERE 🎥 VIDEO TUTORIAL SOON