Code Talk: Adding Tagging Functionality to My Blog

Cover Image for Code Talk: Adding Tagging Functionality to My Blog
Mal Nushi
Mal Nushi

I wanted to add a tagging functionality into my blog so that navigating to different topics would be much easier. For educational purposes, I want to blog about how it was done.

1. Defining The Tag Data Structure

First, I needed to decide how to store tags for each post. I updated the Post type (essentially a blueprint for an object for those unfamiliar with JS/TS) to include an optional array of strings for tags:

// filepath: src/interfaces/post.ts
export type Post = {
  slug: string;
  title: string;
  date: string;
  coverImage: string;
  author: Author;
  excerpt: string;
  ogImage: {
    url: string;
  };
  content: string;
  preview?: boolean;
  draft: boolean;
  tags?: string[];
};

2. Adding Tags to Markdown Content

With the data structure updated, the next step was to add the tags to the actual blog posts which are written in markdown. In the frontmatter of each .md file, I added a tags array. For example, for this post:

---
title: "Code Talk: Adding Tagging Functionality to My Blog"
excerpt: "..."
coverImage: "/assets/blog/adding-tagging-functionality-to-my-blog/cover.webp"
date: "2025-05-28T21:45:00Z"
author:
  name: Mal Nushi
  picture: "/assets/blog/authors/mn.jpeg"
ogImage:
  url: "/assets/blog/adding-tagging-functionality-to-my-blog/cover.webp"
draft: false
tags: ["tech", "programming"]
---

3. Updating the API to Handle Tags

The existing API functions in api.ts needed to be aware of these new tags.

Fetching Tags for a Single Post: The getPostBySlug function was updated to ensure tags are parsed from the frontmatter. If no tags are present, it defaults to an empty array.

// filepath: src/lib/api.ts
export function getPostBySlug(slug: string) {
  const realSlug = slug.replace(/\.md$/, "");
  const fullPath = join(postsDirectory, `${realSlug}.md`);
  const fileContents = fs.readFileSync(fullPath, "utf8");
  const { data, content } = matter(fileContents);

  return { ...data, slug: realSlug, content, tags: data.tags || [] } as Post;
}

Getting All Unique Tags: I added a new function, getAllTags, to collect all unique tags from all posts. This is useful for creating a page that lists all available tags.

// filepath: src/lib/api.ts
export function getAllTags(): string[] {
  const posts = getAllPosts();
  const allTags = new Set<string>();
  posts.forEach(post => {
    if (post.tags) {
      post.tags.forEach(tag => allTags.add(tag));
    }
  });
  return Array.from(allTags).sort();
}

Getting Posts by a Specific Tag: Another new function was created, getPostsByTag, to filter posts that include a specific tag. This is crucial for the individual tag pages.

// filepath: src/lib/api.ts
export function getPostsByTag(tag: string): Post[] {
  const lowercaseTag = tag.toLowerCase();
  return getAllPosts().filter(post =>
    post.tags?.some(t => t.toLowerCase() == lowercaseTag)
  );
}

4. Displaying the Tags on Individual Post Pages

To show tags on each blog post, I modified src/app/posts/[slug]/page.tsx. After fetching the post data, I mapped over the post.tags array and rendered each tag as a link to its respective tag page:

// filepath: src/app/posts/[slug]/page.tsx
// ...more code...
          <PostBody content={content} />
          {post.tags && post.tags.length > 0 && (
            <div className="max-w-2xl mx-auto mt-8">
              <h3 className="text-lg font-semibold mb-2">Tags:</h3>
              <div className="flex flex-wrap gap-2">
                {post.tags.map((tag) => (
                  <Link
                    key={tag}
                    href={`/tags/${encodeURIComponent(tag.toLowerCase())}`}
                    className="bg-sky-200 dark:bg-sky-800 text-sky-600 dark:text-sky-300 px-3 py-1 rounded-full text-sm hover:bg-sky-300 dark:hover:bg-sky-700"
                  >
                    {tag}
                  </Link>
                ))}
              </div>
            </div>
          )}
        </article>
// ...more code...

5. Creating Dynamic Pages for Each Tag

I implemented a dynamic route, src/app/tags/[tag]/page.tsx, to display all posts associated with a specific tag.

Generating Static Paths: The generateStaticParams function uses getAllTags to pre-render a page for each unique tag at build time.

// filepath: src/app/tags/[tag]/page.tsx
export async function generateStaticParams() {
  const tags = getAllTags();
  return tags.map((tag) => ({
    tag: encodeURIComponent(tag.toLowerCase()),
  }));
}

Generating Metadata: The generateMetadata function dynamically sets the page title based on the current tag.

// filepath: src/app/tags/[tag]/page.tsx
export async function generateMetadata(props: PageProps): Promise<Metadata> {
  const resolvedParams = await props.params;
  const tagName = decodeURIComponent(resolvedParams.tag);
  return {
    title: `Posts tagged "${tagName}" | ${SITE_NAME}`,
    description: `Blog posts related to ${tagName}.`,
  };
}

Displaying Tagged Posts: The TagPage component fetches the tag from the URL, calls getPostsByTag, and then maps over the resulting posts, rendering each one using the PostPreview component.

// filepath: src/app/tags/[tag]/page.tsx
export default async function TagPage(props: PageProps) {
  const resolvedParams = await props.params; // Await the promise to get route parameters
  const tagName: string = decodeURIComponent(resolvedParams.tag); // Decode the tag name from the URL, as it might be URL-encoded (e.g., "c%23" for "c#")
  const posts = getPostsByTag(tagName); // Retrieve all posts that are tagged with the decoded tag name

  return (
    <main>
      <Container>
        <Header />
        <h1 className="text-5xl md:text-6xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
          Posts tagged: <span className="capitalize">{tagName}</span>
        </h1>
        {/* Check if there are any posts for the given tag */}
        {posts.length > 0 ? (
          // If posts exist, display them in a grid
          <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
            {/* Map over the posts array and render a PostPreview component for each post */}
            {posts.map((post) => (
              <PostPreview
                key={post.slug} // Unique key for each post, essential for list rendering
                title={post.title}
                coverImage={post.coverImage}
                date={post.date}
                author={post.author}
                slug={post.slug}
                excerpt={post.excerpt}
                tags={post.tags}
              />
            ))}
          </div>
        ) : (
          // If no posts are found for the tag, display a message
          <p className="text-center text-xl">No posts found for this tag.</p>
        )}
      </Container>
    </main>
  );
}

6. Creating an "All Tags" Page

To allow users to browse all available tags, I created page.tsx. This page fetches all unique tags using getAllTags and displays them as links to their respective tag pages.

// filepath: src/app/tags/page.tsx
xport default function AllTagsPage() {
  // Fetch all unqiue tags from the blog posts
  const tags = getAllTags();

  return (
    <main>
      <Container>
        <Header />
        <h1 className="text-5xl md:text-6xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
          All Tags
        </h1>
        {/* Conditional rendering: if there are tags, display them */}
        {tags.length > 0 ? (
          <div className="flex flex-wrap gap-4">
            {/* Map through the tags and render a link for each */}
            {tags.map((tag) => (
              <Link
                key={tag}
                href={`/tags/${encodeURIComponent(tag.toLowerCase())}`}
                className="text-lg font-semibold inline-block py-2 px-4 rounded-full text-sky-600 bg-sky-200 dark:bg-sky-800 dark:text-sky-300 hover:bg-sky-300 dark:hover:bg-sky-700"
              >
                {tag} {/* Display the tag name */}
              </Link>
            ))}
          </div>
        ) : (
          <p className="text-center text-xl">No tags found.</p>
        )}
      </Container>
    </main>
  );
}

7. Updating UI Components to Display Tags

The PostPreview component, used on the homepage and tag pages, was updated to accept and display tags, linking each tag to its page:

// filepath: src/app/_components/post-preview.tsx
export function PostPreview({
  // ...other props...
  tags,
}: Props) {
  return (
    <div>
      {/* ...other post preview elements... */}
      <p className="text-lg leading-relaxed mb-4">{excerpt}</p>
      {tags && tags.length > 0 && (
        <div className="mb-4">
          {tags.map((tag) => (
            <Link
             key={tag}
             href={`/tags/${encodeURIComponent(tag.toLowerCase())}`}
             className="text-sm font-semibold inline-block py-1 px-2 rounded-full text-sky-600 bg-sky-200 dark:bg-sky-800 dark:text-sky-300 last:mr-0 mr-1 hover:bg-sky-300 dark:hover:bg-sky-700"
            >
            {tag}
          </Link>
          ))}
        </div>
      )}
      <Avatar name={author.name} picture={author.picture} />
    </div>
  );
}

Similarily, the HeroPost component could also be updated I think if I wanted to display tags there.

8. Customizing the Homepage Introduction

For the homepage, I wanted to go a different direction with the presentation. Instead of relying on the global Header for the "All Tags" link, I integrated it directly into the Intro component. This places the site title and the "All Tags" link together at the top of the homepage, followed by the site's subtitle.

// filepath: src/app/_components/intro.tsx
import { SITE_NAME } from "@/lib/constants";
import Link from "next/link";

export function Intro() {
  return (
    <section className="flex flex-col mt-16 mb-16 md:mb-12">
      {/* Wrapper for Title and All Tags link */}
      <div className="flex flex-col md:flex-row items-center md:justify-between mb-6 md:mb-8">
        <h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight mb-4 md:mb-0 md:pr-8">
          {/* Link the site name to the homepage */}
          <Link href="/" className="hover:underline">
            {SITE_NAME}
          </Link>
        </h1>
        <nav>
          <Link href="/tags" className="text-lg md:text-xl font-semibold hover:underline whitespace-nowrap">
            All Tags
          </Link>
        </nav>
      </div>
      {/* Subtitle */}
      <h4 className="text-center md:text-left text-lg">
        A blog by Mal Nushi—<i>where ideas wander from circuits to sentences.</i>
      </h4>
    </section>
  );
}

9. Adding Navigation

Finally, I added a link to the new "All Tags" page in the main site to navigate within the Header component.

// filepath: src/app/_components/header.tsx
// ...more code...
          <nav>
            <Link href="/tags" className="text-lg md:text-xl font-semibold hover:underline ml-4">
              All Tags
            </Link>
          </nav>
// ...more code...

And that is how I added a tagging system to the blog! This makes it much easier to find blog posts based on topics.