LESSON 4 ⏱️ 12 min read

Fetching Posts and Pages

Building a Data Fetching Layer

Let's create reusable functions for fetching WordPress content. We'll focus on:

  • Proper error handling
  • TypeScript type safety
  • Request caching
  • Embedded data for related content

Creating the API Module

Create a dedicated API module:

touch src/lib/api.ts

Fetching Posts

// src/lib/api.ts
import { wp } from './wordpress';
import { WPPost, WPPostWithEmbed, WPCategory } from './types';

/**
 * Fetch all published posts with embedded data
 */
export async function getPosts(options?: {
  perPage?: number;
  page?: number;
  categoryId?: number;
  search?: string;
}): Promise<{ posts: WPPostWithEmbed[]; totalPages: number }> {
  const { perPage = 10, page = 1, categoryId, search } = options ?? {};
  
  try {
    let query = wp.posts()
      .perPage(perPage)
      .page(page)
      .embed();  // Include author, featured image, terms
    
    if (categoryId) {
      query = query.categories([categoryId]);
    }
    
    if (search) {
      query = query.search(search);
    }
    
    const response = await query.get();
    
    return {
      posts: response.data as WPPostWithEmbed[],
      totalPages: response.totalPages,
    };
  } catch (error) {
    console.error('Error fetching posts:', error);
    return { posts: [], totalPages: 0 };
  }
}

/**
 * Fetch a single post by slug
 */
export async function getPostBySlug(
  slug: string
): Promise<WPPostWithEmbed | null> {
  try {
    const { data } = await wp.posts()
      .slug(slug)
      .embed()
      .get();
    
    return (data[0] as WPPostWithEmbed) ?? null;
  } catch (error) {
    console.error(`Error fetching post with slug "${slug}":`, error);
    return null;
  }
}

/**
 * Fetch post by ID
 */
export async function getPostById(
  id: number
): Promise<WPPostWithEmbed | null> {
  try {
    const post = await wp.posts().id(id).embed().get();
    return post as WPPostWithEmbed;
  } catch (error) {
    console.error(`Error fetching post with ID ${id}:`, error);
    return null;
  }
}

Fetching Pages

/**
 * Fetch a page by slug
 */
export async function getPageBySlug(
  slug: string
): Promise<WPPostWithEmbed | null> {
  try {
    const { data } = await wp.pages()
      .slug(slug)
      .embed()
      .get();
    
    return (data[0] as WPPostWithEmbed) ?? null;
  } catch (error) {
    console.error(`Error fetching page with slug "${slug}":`, error);
    return null;
  }
}

/**
 * Fetch all pages
 */
export async function getAllPages(): Promise<WPPostWithEmbed[]> {
  try {
    const { data } = await wp.pages()
      .perPage(100)  // Adjust as needed
      .embed()
      .get();
    
    return data as WPPostWithEmbed[];
  } catch (error) {
    console.error('Error fetching pages:', error);
    return [];
  }
}

Fetching Categories

/**
 * Fetch all categories
 */
export async function getCategories(): Promise<WPCategory[]> {
  try {
    const { data } = await wp.categories()
      .perPage(100)
      .get();
    
    // Filter out empty categories
    return (data as WPCategory[]).filter(cat => cat.count > 0);
  } catch (error) {
    console.error('Error fetching categories:', error);
    return [];
  }
}

/**
 * Fetch category by slug
 */
export async function getCategoryBySlug(
  slug: string
): Promise<WPCategory | null> {
  try {
    const { data } = await wp.categories()
      .slug(slug)
      .get();
    
    return (data[0] as WPCategory) ?? null;
  } catch (error) {
    console.error(`Error fetching category with slug "${slug}":`, error);
    return null;
  }
}

Adding Request Caching

Next.js 14 has built-in fetch caching, but let's make it explicit:

// Add to the top of api.ts

// Revalidation options
export const REVALIDATE_INTERVAL = 60; // seconds

// Example with explicit cache control
export async function getPostsCached(options?: {
  perPage?: number;
  page?: number;
}): Promise<WPPostWithEmbed[]> {
  const { perPage = 10, page = 1 } = options ?? {};
  
  const url = `${process.env.NEXT_PUBLIC_WORDPRESS_URL}/wp-json/wp/v2/posts?per_page=${perPage}&page=${page}&_embed`;
  
  const response = await fetch(url, {
    next: { revalidate: REVALIDATE_INTERVAL },
  });
  
  if (!response.ok) {
    throw new Error(`Failed to fetch posts: ${response.status}`);
  }
  
  return response.json();
}

Static Generation Helpers

For static site generation, we need to know all possible slugs:

/**
 * Get all post slugs for static generation
 */
export async function getAllPostSlugs(): Promise<string[]> {
  try {
    // Fetch all posts (paginated if necessary)
    const slugs: string[] = [];
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
      const { data } = await wp.posts()
        .perPage(100)
        .page(page)
        .fields(['slug'])  // Only fetch what we need
        .get();
      
      if (data.length === 0) {
        hasMore = false;
      } else {
        slugs.push(...data.map((post: { slug: string }) => post.slug));
        page++;
      }
    }
    
    return slugs;
  } catch (error) {
    console.error('Error fetching post slugs:', error);
    return [];
  }
}

/**
 * Get all category slugs for static generation
 */
export async function getAllCategorySlugs(): Promise<string[]> {
  try {
    const categories = await getCategories();
    return categories.map(cat => cat.slug);
  } catch (error) {
    console.error('Error fetching category slugs:', error);
    return [];
  }
}

Using the API Functions

Now use these in your pages:

// src/app/page.tsx
import { getPosts } from '@/lib/api';
import { PostCard } from '@/components/PostCard';

export default async function Home() {
  const { posts, totalPages } = await getPosts({ perPage: 6 });
  
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">Latest Posts</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {posts.map((post) => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>
      
      {totalPages > 1 && (
        <p className="mt-8 text-center text-gray-600">
          Page 1 of {totalPages}
        </p>
      )}
    </main>
  );
}

Summary

In this lesson, we:

  • ✅ Built reusable data fetching functions
  • ✅ Added proper error handling
  • ✅ Configured request caching
  • ✅ Created helpers for static generation

Next up: Dynamic routing for single post pages.