Skip to content

Categories

Categories are broad groupings attached to blog posts. Where tags are specific keywords, categories are high-level sections — tutorial, news, guides, contact.

Every category you add to a post frontmatter automatically creates a category archive page at /categories/<category>/. That page lists every post in the category. No manual configuration needed.

Categories and tags work side by side. A post usually has one or two categories and several tags.


Categories are not stored in a separate file. They live inside the frontmatter of every blog post:

categories: ["contact"]

The category system reads every blog post, collects all categories, and builds:

  • A category index page at /categories/ — lists every category in use
  • A category archive page at /categories/<category>/ — lists every post in that category

Both are generated automatically.


The category index lives at src/pages/categories/index.astro:

---
import BaseLayout from "../../design/base.astro";
import Data from "../../data/configuration/blog/blogpage.json";
const allPosts = Object.values(import.meta.glob("../blog/*.md", { eager: true })) as any[];
const categories = [...new Set(allPosts.map((blog) => blog.frontmatter.categories).flat().filter(Boolean))];
const pageTitle = "Category Index";
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<BaseLayout pageTitle={pageTitle}>
<div class="col-md-12 p-3 text-center">
<h1><strong><a href={canonicalURL}>{Data.categories_page_title}</a></strong></h1>
<h2>{Data.categories_page_description}</h2>
</div>
<div class="row mt-5">
{categories.map((category: any) => (
<h3 class="col-md-4 p-3"><a href={`/categories/${category}`}>{category}</a></h3>
))}
</div>
</BaseLayout>
  1. Loads every blog post — import.meta.glob("../blog/*.md", { eager: true }) reads all .md files in src/pages/blog/ at build time.
  2. Extracts all categories — maps over every post, takes frontmatter.categories, flattens the arrays, and filters out empty values.
  3. Deduplicates — new Set(...) removes duplicates so each category appears once.
  4. Builds a canonical URL — new URL(Astro.url.pathname, Astro.site) produces an absolute URL for the current page.
  5. Renders the list — loops through unique categories and displays each as a link to /categories/<category>/.
  6. Reads page title and description — from blogpage.json (categories_page_title and categories_page_description).

A simple page listing every category used across your blog. Clicking any category goes to that category’s archive.


Each category has its own page generated dynamically by src/pages/categories/[category].astro:

---
import BaseLayout from "../../design/base.astro";
import BlogPost from "../../widget/BlogPost.astro";
export async function getStaticPaths() {
const allPosts = Object.values(import.meta.glob("../blog/*.md", { eager: true })) as any[];
const uniqueCategories = [...new Set(allPosts.map((post) => post.frontmatter.categories).flat())];
return uniqueCategories.map((category) => ({
params: { category },
props: { posts: allPosts.filter((post) => post.frontmatter.categories?.includes(category)) },
}));
}
const { category } = Astro.params;
const { posts } = Astro.props as any;
---
<BaseLayout>
<div class="col-md-12 p-3 text-center">
<h1>{category}</h1>
<h2 class="lead">List of categories for {category}</h2>
</div>
{posts.map((post: any) => (
<BlogPost url={post.url} title={post.frontmatter.title} description={post.frontmatter.description} image={post.frontmatter.image} />
))}
</BaseLayout>
  1. getStaticPaths() — runs at build time. It reads every blog post, collects unique categories, and generates one page per category.
  2. For each category, it filters all posts to find only those that include the category.
  3. Astro.params.category — the current category being rendered (from the URL).
  4. Astro.props.posts — the list of posts for this category.
  5. Renders each post using the <BlogPost /> widget.

Each category gets its own page at /categories/<category>/, listing all posts in that category. For example, /categories/contact/ lists every post in the contact category.


Categories are added in the frontmatter of any blog post:

---
layout: ../../design/blog/blogpost.astro
title: "My Post"
description: "Short summary"
image: /src/assets/cover.png
pubDate: 2025-01-15
tags: ["astro", "tutorial"]
categories: ["guides"]
---

The categories array can contain one or more categories. Once saved, the category archive pages for guides are regenerated automatically at the next build.

  • Lowercase — guides, not Guides
  • Broad, one-word — tutorial, news, contact
  • Consistent spelling — if you use guides in one post, do not use guide or Guides in another
  • Few per post — one or two is typical. More than three dilutes the meaning
  • No duplicates — ["guides", "guides"] is redundant

The category index reads from src/data/configuration/blog/blogpage.json:

"categories_page_title": "Categories Astro Themes",
"categories_page_description": "Update blog Docus Astro JS Starlight Themes template with complete - categories"

Edit those values to change the title and description on /categories/.

The category archive uses src/design/base.astro as its layout. Edit that file to change how individual category pages look.

Change the category archive post rendering

Section titled “Change the category archive post rendering”

Each post in a category archive is rendered by <BlogPost /> from src/widget/BlogPost.astro. Edit that widget to change how posts appear in the list.


CategoriesTags
BroadSpecific
Few per postMany per post
tutorial, newsastro, contact form, seo
High-level groupingFine-grained discovery
URL: /categories/<category>/URL: /tags/<tag>/

You can use both on the same post. A post about setting up a contact form might have:

tags: ["astro", "contact form"]
categories: ["tutorial"]

Categories help readers browse by section. Tags help readers find related content.


  1. Categories live in post frontmatter — no separate config file.
  2. The category index and archives are auto-generated — no manual listing.
  3. Keep spelling consistent — inconsistent categories create duplicate archives.
  4. Use lowercase — for cleaner URLs and sorting.
  5. Do not rename categories lightly — renaming a category creates a new archive and breaks existing links.
  6. Empty category arrays are fine — posts without categories simply do not appear in any category archive.

Blog post frontmatter:
categories: ["guides", "tutorial"]
│
▼
/categories/ → index of all categories
/categories/guides/ → all posts in "guides"
/categories/tutorial/ → all posts in "tutorial"

Files involved:

FilePurpose
src/pages/categories/index.astroCategory index page
src/pages/categories/[category].astroIndividual category archive
src/design/base.astroCategory archive layout
src/widget/BlogPost.astroPost card in archives
src/data/configuration/blog/blogpage.jsonCategory index title/description

Add a category — edit a post’s frontmatter. That’s it. Everything else is automatic.