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.tsFetching 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.