@shinyaz

Adding Category and Tag Index Pages to a Next.js Blog

2 min read
Table of Contents

The Problem

This blog had individual category pages (/en/category/programming) and tag pages (/en/tag/nextjs), but no way to discover what categories and tags exist. You could reach them via badges on posts, but there was no overview — no "what does this blog cover?" page.

I wanted to add category and tag index pages for both locales.

Zero New Data-Layer Code

What made this implementation satisfying was that every function needed already existed in src/lib/posts.ts:

  • getAllCategories() — returns all category objects
  • getCategoryName() / getCategoryDescription() — bilingual name and description
  • getPostsByCategory() — posts by category (for counts)
  • getAllTags(locale) — all tags for a locale
  • getPostsByTag() — posts by tag (for counts)

No new utilities, no new components, no new data fetching patterns. Just composing existing functions into new pages. This is the payoff of building a well-factored data layer upfront.

Card Grid vs. Tag Cloud: Why Different Layouts

The two index pages use different layouts, and the reason comes down to the shape of the data.

Categories use a card grid (grid gap-4 sm:grid-cols-2). Each category has a name, description, and post count — structured data that fits naturally into a card. There are few categories (currently just one), so each deserves visual weight:

TypeScript
<Link className="rounded-lg border border-border p-4 transition-colors hover:bg-accent">
  <h2 className="font-semibold">{name}</h2>
  <p className="mt-1 text-sm text-muted-foreground">{description}</p>
  <p className="mt-2 text-xs text-muted-foreground">{postCount} posts</p>
</Link>

Tags use a flex-wrap tag cloud. Tags are flat strings with no description — just a name and count. There are many of them, so a dense layout makes more sense:

TypeScript
<Link className="inline-flex items-center gap-1.5 rounded-full border border-border px-3 py-1.5 text-sm">
  #{tag} <span className="text-xs">({postCount})</span>
</Link>

The tag cloud badges are deliberately larger than the existing TagBadge component (px-3 py-1.5 text-sm vs px-2.5 py-0.5 text-xs) for better readability and tap targets on the index page.

The Locale Asymmetry

One subtle design difference: getAllTags(locale) takes a locale argument, but getAllCategories() doesn't. This reflects how the two are defined:

  • Categories are static YAML files — the same categories exist regardless of locale, with bilingual name / nameJa and description / descriptionJa fields
  • Tags are free-form strings in post frontmatter — Japanese and English posts may have completely different tag sets

This means the tag index can show different content per locale, while the category index shows the same categories with translated names. It's a subtle but important distinction when building bilingual content systems.

Summary

  • Existing functions compose well — the fact that zero new data-layer code was needed validates the helper function design in posts.ts. When your abstractions are right, new features are just composition.
  • Let data shape drive layout — categories (structured, few) → card grid; tags (flat, many) → tag cloud. Don't force the same layout on fundamentally different data.
  • Static vs. dynamic metadata has design implications — YAML-defined categories are locale-independent; frontmatter tags are locale-dependent. This asymmetry matters for bilingual index pages.

Share this post

Shinya Tahara

Shinya Tahara

Solutions Architect @ AWS

I'm a Solutions Architect at AWS, providing technical guidance primarily to financial industry customers. I share learnings about cloud architecture and AI/ML on this site.The views and opinions expressed on this site are my own and do not represent the official positions of my employer.

Related Posts