Skip to main content
The template includes a powerful tagging system that automatically aggregates content across all collections, creating dynamic tag pages and enabling content discovery.

How Tags Work

Tags are optional fields in all content collections that create relationships between different types of content. For example, a project and a blog post can both have the “React” tag, linking them together.

Adding Tags to Content

Tags are defined in the frontmatter of any content file:
src/content/projects/portfolio.mdx
---
title: "Portfolio"
description: "My portfolio website"
startDate: 2025-03-16
tags: ["Astro", "Shadcn UI", "Tailwind CSS"]
---

Tag Aggregation

The getAndGroupUniqueTags() utility function collects all tags from all collections and groups content by tag:
src/lib/utils.ts
export const getAndGroupUniqueTags = async (): Promise<Map<string, any[]>> => {
  const allProjects = await getCollection("projects");
  const allExperiences = await getCollection("experiences");
  const books = await getCollection("books");
  const posts = await getCollection("posts");

  const allItems = [...allProjects, ...allExperiences, ...books, ...posts];

  const uniqueTags: string[] = [
    ...new Set(allProjects.map((post: any) => post.data.tags).flat()),
    ...new Set(allExperiences.map((post: any) => post.data.tags).flat()),
    ...new Set(books.map((post: any) => post.data.tags).flat()),
    ...new Set(posts.map((post: any) => post.data.tags).flat()),
  ];

  const tagItemsMap = new Map<string, any[]>();

  uniqueTags.forEach((tag) => {
    const filteredItems = allItems.filter((item) =>
      item?.data?.tags?.includes(tag),
    );
    tagItemsMap.set(tag, filteredItems);
  });

  return tagItemsMap;
};
This function returns a Map where keys are tag names and values are arrays of all content items with that tag.

Tag Pages

The template automatically generates a page for each tag using Astro’s dynamic routing:
src/pages/tags/[tag].astro
---
import { getAndGroupUniqueTags } from "../../lib/utils";

export async function getStaticPaths() {
  const tagItemsMap = await getAndGroupUniqueTags();

  const result = []
  tagItemsMap.forEach((items, tag) => {
    result.push({
      params: {tag}, 
      props: {items},
    });
  });

  return result;
}

const {tag} = Astro.params;
const {items} = Astro.props;
---

<IndexPageLayout title={tag} description="A new dimension to access content of this website" subTitle={tag}>
  <div class="flex flex-col gap-3">
    {
      items?.map((post) => (
        <ProjectCard 
          url={`/posts/${post.slug}/`} 
          heading={post.data.title} 
          subheading={post.data.description}
          imagePath={post.data?.image?.url}
          altText={post.data?.image?.alt} 
          dateStart={post.data.startDate}
        />
      ))
    }
  </div>
</IndexPageLayout>

How Dynamic Tag Pages Work

1

Build Time Collection

getStaticPaths() runs at build time and calls getAndGroupUniqueTags()
2

Page Generation

For each unique tag, Astro generates a page at /tags/{tagName}
3

Content Filtering

Each tag page receives all content items that include that tag
4

Rendering

The page template displays all items with consistent formatting

Tag Display Components

The ContentTags component displays tags with links:
src/components/ContentTags.astro
---
import { Badge } from "./ui/badge";
const {tags} = Astro.props;
---

<div class="flex flex-0 gap-1 flex-wrap">
  {tags.map((i) => (
    <a href={`/tags/${i}`}>
      <Badge className="truncate">{i}</Badge>
    </a>
  ))}
</div>

Using ContentTags

---
import ContentTags from './ContentTags.astro';
const { tags } = project.data;
---

{tags && <ContentTags tags={tags} />}

Linking Skills to Tags

The About Me section cleverly links skills from your PROFILE to tag pages:
src/components/sections/AboutMe.astro
---
import { getAndGroupUniqueTags } from "../../lib/utils";
import { PROFILE } from "../../content/profileData";

const tagsMap = await getAndGroupUniqueTags();
const skills = PROFILE.skills;
---

<ul class="list-inside list-disc">
  {
    skills.map((skill) => {
      if (tagsMap.has(skill)) {
        return (
          <li>
            <a href=`/tags/${skill}` class="hover:font-bold">
              {skill}
            </a>
          </li>
        )
      } else {
        return (
          <li>
            <span>{skill}</span>
          </li>
        )
      }
    })
  }
</ul>
If you list “React” in your PROFILE.skills AND have content tagged with “React”, the skill becomes a clickable link to /tags/React showing all React-related content.

Tag Naming Best Practices

Use Consistent Casing

Choose “React” or “react” and stick with it across all content

Avoid Duplicates

Don’t use both “JavaScript” and “JS” - pick one canonical form

Be Specific

Use “Tailwind CSS” instead of just “CSS” for better organization

Match Skills

Use the exact same tag names as in your PROFILE.skills for automatic linking

Tag Index Page

You can create a tag index showing all available tags:
src/pages/tags/index.astro
---
import { getAndGroupUniqueTags } from "../../lib/utils";
import { Badge } from "../../components/ui/badge";

const tagItemsMap = await getAndGroupUniqueTags();
const tags = Array.from(tagItemsMap.keys()).sort();
---

<div class="flex flex-wrap gap-2">
  {tags.map((tag) => (
    <a href={`/tags/${tag}`}>
      <Badge>
        {tag} ({tagItemsMap.get(tag)?.length})
      </Badge>
    </a>
  ))}
</div>

Example Use Cases

Technology Tagging

---
title: "Building a Web App"
tags: ["React", "Typescript", "Node.js", "PostgreSQL"]
---
Creates 4 tag pages, each showing all content using that technology.

Topic Tagging

---
title: "Investment Strategies"
tags: ["Finance", "Investing", "IPO"]
---
Groups content by subject matter across different content types.

Mixed Tagging Strategy

---
title: "IPO Metrics Platform"
tags: ["Astro", "React", "Finance", "IPO", "Side Project"]
---
Combines technology, topic, and category tags for maximum discoverability.

Advanced: Tag Analytics

You can analyze which tags are most popular:
const tagItemsMap = await getAndGroupUniqueTags();

const tagStats = Array.from(tagItemsMap.entries())
  .map(([tag, items]) => ({
    tag,
    count: items.length
  }))
  .sort((a, b) => b.count - a.count);

// Top 5 most used tags
const topTags = tagStats.slice(0, 5);
The tagging system is entirely opt-in. If you don’t add tags to your content, no tag pages will be generated.

Troubleshooting

Make sure at least one content item has that exact tag spelling. Tags are case-sensitive.
Verify the skill name in PROFILE.skills exactly matches a tag used in your content.
Check that your content schema includes tags: z.array(z.string()).optional() and you’re using the ContentTags component.

Build docs developers (and LLMs) love