LESSON 6 ⏱️ 10 min read

Categories and Filtering

Category-Based Navigation

Most blogs organize content by categories. Let's add:

  • Category listing in the sidebar/header
  • Individual category archive pages
  • Active state for current category

Creating Category Pages

Create the category route structure:

mkdir -p src/app/category/[slug]
touch src/app/category/[slug]/page.tsx

Category Archive Page

// src/app/category/[slug]/page.tsx
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { 
  getPosts, 
  getCategoryBySlug, 
  getAllCategorySlugs 
} from '@/lib/api';
import { PostCard } from '@/components/PostCard';
import type { Metadata } from 'next';

interface CategoryPageProps {
  params: { slug: string };
  searchParams: { page?: string };
}

// Generate static params
export async function generateStaticParams() {
  const slugs = await getAllCategorySlugs();
  return slugs.map((slug) => ({ slug }));
}

// Generate metadata
export async function generateMetadata({ 
  params 
}: CategoryPageProps): Promise<Metadata> {
  const category = await getCategoryBySlug(params.slug);
  
  if (!category) {
    return { title: 'Category Not Found' };
  }
  
  return {
    title: `${category.name} | Blog Categories`,
    description: category.description || `Browse all posts in ${category.name}`,
  };
}

export default async function CategoryPage({ 
  params, 
  searchParams 
}: CategoryPageProps) {
  const category = await getCategoryBySlug(params.slug);
  
  if (!category) {
    notFound();
  }
  
  const currentPage = Number(searchParams.page) || 1;
  const { posts, totalPages } = await getPosts({
    perPage: 9,
    page: currentPage,
    categoryId: category.id,
  });
  
  return (
    <main className="container mx-auto px-4 py-8">
      {/* Category Header */}
      <header className="mb-8">
        <Link 
          href="/blog" 
          className="text-blue-600 hover:underline mb-2 inline-block"
        >
          ← All Posts
        </Link>
        <h1 className="text-4xl font-bold">{category.name}</h1>
        {category.description && (
          <p className="text-gray-600 mt-2">{category.description}</p>
        )}
        <p className="text-sm text-gray-500 mt-1">
          {category.count} {category.count === 1 ? 'post' : 'posts'}
        </p>
      </header>
      
      {/* Posts Grid */}
      {posts.length === 0 ? (
        <p className="text-gray-600">No posts found in this category.</p>
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {posts.map((post) => (
            <Link key={post.id} href={`/blog/${post.slug}`}>
              <PostCard post={post} />
            </Link>
          ))}
        </div>
      )}
      
      {/* Pagination */}
      {totalPages > 1 && (
        <nav className="mt-12 flex justify-center gap-4">
          {currentPage > 1 && (
            <Link
              href={`/category/${params.slug}?page=${currentPage - 1}`}
              className="px-4 py-2 bg-gray-200 rounded"
            >
              ← Previous
            </Link>
          )}
          <span className="px-4 py-2">
            Page {currentPage} of {totalPages}
          </span>
          {currentPage < totalPages && (
            <Link
              href={`/category/${params.slug}?page=${currentPage + 1}`}
              className="px-4 py-2 bg-gray-200 rounded"
            >
              Next →
            </Link>
          )}
        </nav>
      )}
    </main>
  );
}

Category List Component

Create a reusable component to display categories:

// src/components/CategoryList.tsx
import Link from 'next/link';
import { getCategories } from '@/lib/api';

interface CategoryListProps {
  currentSlug?: string;
}

export async function CategoryList({ currentSlug }: CategoryListProps) {
  const categories = await getCategories();
  
  if (categories.length === 0) {
    return null;
  }
  
  return (
    <nav className="mb-8">
      <h2 className="text-lg font-semibold mb-4">Categories</h2>
      <ul className="flex flex-wrap gap-2">
        <li>
          <Link
            href="/blog"
            className={`px-3 py-1 rounded-full text-sm ${
              !currentSlug 
                ? 'bg-blue-600 text-white' 
                : 'bg-gray-200 hover:bg-gray-300'
            }`}
          >
            All
          </Link>
        </li>
        {categories.map((category) => (
          <li key={category.id}>
            <Link
              href={`/category/${category.slug}`}
              className={`px-3 py-1 rounded-full text-sm ${
                currentSlug === category.slug
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-200 hover:bg-gray-300'
              }`}
            >
              {category.name} ({category.count})
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  );
}

Adding Categories to Post Cards

Update PostCard to show categories:

// src/components/PostCard.tsx
import { WPPostWithEmbed, getFeaturedImageUrl } from '@/lib/types';

interface PostCardProps {
  post: WPPostWithEmbed;
}

export function PostCard({ post }: PostCardProps) {
  const imageUrl = getFeaturedImageUrl(post, 'medium_large');
  
  // Get categories from embedded data
  const categories = post._embedded?.['wp:term']?.[0] ?? [];
  
  return (
    <article className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
      {imageUrl && (
        <div className="aspect-video overflow-hidden">
          <img
            src={imageUrl}
            alt={post.title.rendered}
            className="w-full h-full object-cover"
          />
        </div>
      )}
      <div className="p-4">
        {/* Categories */}
        {categories.length > 0 && (
          <div className="flex gap-2 mb-2">
            {categories.slice(0, 2).map((cat) => (
              <span 
                key={cat.id}
                className="text-xs px-2 py-1 bg-blue-100 text-blue-800 rounded"
              >
                {cat.name}
              </span>
            ))}
          </div>
        )}
        
        <h2 
          className="text-xl font-bold mb-2"
          dangerouslySetInnerHTML={{ __html: post.title.rendered }}
        />
        
        <div
          className="text-gray-600 text-sm line-clamp-2"
          dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
        />
      </div>
    </article>
  );
}

Using Categories in Blog Page

// src/app/blog/page.tsx
import { CategoryList } from '@/components/CategoryList';

export default async function BlogPage({ searchParams }: BlogPageProps) {
  // ... existing code
  
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>
      
      {/* Add category filter */}
      <CategoryList />
      
      {/* Rest of the content */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {/* ... posts */}
      </div>
    </main>
  );
}

Summary

In this lesson, we:

  • ✅ Created category archive pages
  • ✅ Built a reusable CategoryList component
  • ✅ Added category badges to post cards
  • ✅ Implemented active state for filtering

Next up: Optimizing images for performance.