Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 33

Displaying and Linking Categories and Tags

Recommended resources

Hello! Welcome back to your course on building a professional accounting website with Astro.

In the last lesson, you did the crucial work of defining the "shape" of your content by adding category and tags fields to your content collection schemas. You now have a type-safe way to organize every blog post.

Today, we'll take that structured data and bring it to the forefront of your website. This lesson's goal is to display and link categories and tags on your blog posts and listing pages. By making these taxonomies visible and clickable, you'll significantly improve your site's navigation and help users discover relevant content—a key feature for the knowledge/blog section of your accounting site.

1. Displaying Taxonomies on the Blog Post Page

Let's start with an individual blog post. Since you've already added the category and tags to your frontmatter, this data is automatically available on your dynamic blog post pages. Our first step is to render it.

A common design pattern for tags and categories is to display them as small, styled "badges" or "pills". To keep our code clean and reusable—a core principle of this course—we'll create a dedicated component for this.

Activity 1: Create a Reusable Tag Component

Let's create a small, presentational component whose only job is to display a single tag or category as a styled link.

  1. Create a new file at src/components/ui/Tag.astro. Your experience with component-based architecture in React will make this feel familiar.
  2. This component will accept two props: name (the text to display) and basePath (either categories or tags to build the link).
  3. Add the following code to your new file:
---
// src/components/ui/Tag.astro

// A simple slugify function to make URLs clean
function slugify(text) {
  return text
    .toString()
    .toLowerCase()
    .replace(/\s+/g, '-')       // Replace spaces with -
    .replace(/[^\w\-]+/g, '')   // Remove all non-word chars
    .replace(/\-\-+/g, '-')     // Replace multiple - with single -
    .replace(/^-+/, '')         // Trim - from start of text
    .replace(/-+$/, '');        // Trim - from end of text
}

const { name, basePath = 'tags' } = Astro.props;
const href = `/${basePath}/${slugify(name)}`;
---

<a href={href} class="tag-badge">
  {name}
</a>

<style>
  .tag-badge {
    display: inline-block;
    height: fit-content;
    padding: 0.25rem 0.75rem;
    border-radius: 9999px; /* Pill shape */
    background-color: #f3f4f6; /* A light gray, change as needed */
    color: #4b5563;
    font-size: 0.875rem; /* 14px */
    font-weight: 500;
    text-decoration: none;
    transition: background-color 0.2s ease, color 0.2s ease;
  }

  .tag-badge:hover {
    background-color: #e5e7eb; /* A slightly darker gray on hover */
    color: #1f2937;
  }
</style>

This component is simple but powerful. It handles URL slugification and provides a consistent, styled link for any tag or category name you pass to it.

Activity 2: Integrate the Tag Component

Now, let's use this new component on your main blog post template.

  1. Open your dynamic post page, likely located at src/pages/blog/[...slug].astro.
  2. Import the Tag component at the top of the frontmatter script.
  3. Locate the post object you retrieve using getEntry. The taxonomy data is inside post.data.
  4. Right below your post title (<h1>) or date, add the following code to display the category and tags:
---
// src/pages/blog/[...slug].astro
// ... other imports
import Tag from '../../components/ui/Tag.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection, getEntry } from 'astro:content';

export async function getStaticPaths() { /* ...your existing code... */ }

const { slug } = Astro.params;
const post = await getEntry('blog', slug);
const { Content } = await post.render();
---

<BaseLayout pageTitle={post.data.title}>
  <h1>{post.data.title}</h1>
  <p>Published on: {post.data.pubDate.toDateString()}</p>
  
  <!-- Add this section -->
  <div class="taxonomy-container">
    <Tag name={post.data.category} basePath="categories" />
    {post.data.tags?.map(tag => (
      <Tag name={tag} basePath="tags" />
    ))}
  </div>

  <Content />
</BaseLayout>

<style>
  .taxonomy-container {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
  }
</style>

Notice we use post.data.tags?.map(...). The optional chaining ?. is a good practice here, as you defined the tags field as optional in your schema in the last lesson.

If you visit one of your blog posts now, you should see the category and tags beautifully rendered as clickable badges!


2. Adding Taxonomies to the Blog Listing Page

A user shouldn't have to click into a post to see what it's about. Let's reuse our Tag component on the main blog index page to display the taxonomies for each post in the list.

Astro Blog Homepage with Project and Post Tagging
This image shows a great example of what we're aiming for: tags are displayed directly on the post summary cards, giving users a quick overview of the post's topics.

Activity 3: Update Your Post Summary Component

You likely have a blog listing page at src/pages/blog/index.astro that loops through all your posts and renders a summary for each one, possibly using a component like BlogPostCard.astro.

  1. Open your blog listing page (src/pages/blog/index.astro). Ensure you are fetching all posts with getCollection('blog').

  2. In the loop that renders each post, make sure you are passing the category and tags data as props to your summary component.

    <!-- src/pages/blog/index.astro -->
    {allPosts.map(post => (
      <BlogPostCard
        url={`/blog/${post.slug}`}
        title={post.data.title}
        category={post.data.category}
        tags={post.data.tags}
      />
    ))}
    
  3. Now, open your summary component (e.g., src/components/BlogPostCard.astro).

  4. Import your Tag component.

  5. Accept category and tags as props.

  6. Render them within the card's structure, just as you did on the single post page.

---
// src/components/BlogPostCard.astro
import Tag from './ui/Tag.astro';
const { url, title, category, tags } = Astro.props;
---

<article class="card">
  <h2><a href={url}>{title}</a></h2>
  <!-- Add this section to your card -->
  <div class="taxonomy-container">
    <Tag name={category} basePath="categories" />
    {tags?.slice(0, 2).map(tag => ( <!-- Show first 2 tags -->
      <Tag name={tag} basePath="tags" />
    ))}
  </div>
</article>

<style>
  /* ... your existing card styles ... */
  .taxonomy-container {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.75rem;
  }
</style>

By reusing the Tag component, you ensure visual consistency across your entire site with minimal effort.

Test your understanding!

Suppose you want the "category" badge to have a different background color to distinguish it from the "tag" badges. How could you modify the Tag.astro component and your page templates to achieve this?

Show answer

The best way is to pass another prop to indicate the type and use it to apply a conditional class.

  1. Modify Tag.astro: Add a variant prop and use it to apply a class.

    ---
    // src/components/ui/Tag.astro
    // ... slugify function ...
    const { name, basePath = 'tags', variant = 'tag' } = Astro.props;
    const href = `/${basePath}/${slugify(name)}`;
    ---
    <a href={href} class:list={["tag-badge", { "is-category": variant === 'category' }]}>
      {name}
    </a>
    <style>
      /* ... existing .tag-badge styles ... */
      .is-category {
        background-color: #dbeafe; /* A distinct light blue */
        color: #1e40af;
      }
      .is-category:hover {
        background-color: #bfdbfe;
      }
    </style>
    
  2. Update the implementation: Pass the variant prop when rendering the category.

    <!-- In [...slug].astro or BlogPostCard.astro -->
    <Tag name={post.data.category} basePath="categories" variant="category" />
    

This approach keeps the logic encapsulated within the component and makes the styling intent clear.


Linking to... a 404 Page?

If you click on your new tag or category links, you'll see Astro's 404 page. This is expected!

We've created the links, but we haven't yet created the pages they point to (e.g., /tags/small-business). We're seeing a real-world example of how developers build features incrementally. For a practical perspective on this process, it's helpful to see how others have solved the same problem.

Some Tags

The article 'Some Tags' by Scott Willsey is a great walkthrough of how a developer implemented a tag system on their personal Astro blog. It shows the thought process behind creating reusable components and dynamic links.

Please read the section 'Tags Index Page and Tag Cloud'. Pay close attention to the code for TagCloud.astro. Notice how he fetches all posts, processes the tags, and then maps over them to create <a> tags with dynamic href attributes, very similar to what we just did. His component is more advanced as it also counts the tags, but the core principle of creating reusable, linked tag elements is the same.


Conclusion

Excellent work today! You have successfully bridged the gap between your backend content schema and the frontend presentation. Your blog is now far more organized and navigable for your users.

Key Takeaways:

  • You can access frontmatter data like category and tags via the post.data object in your Astro pages.
  • Creating small, reusable UI components like Tag.astro is a best practice for maintaining a clean and scalable codebase.
  • You can render lists of data (like tags) by using the .map() method directly within your Astro template's JSX-like syntax.
  • We've created links that point to where our future archive pages will live.

Up Next:

In our next lesson, we will make those links fully functional. You will learn how to create dynamic archive pages for each category and tag. We'll dive into Astro's powerful getStaticPaths function to automatically generate a unique page for every tag you've used, listing all the associated posts.