Build Phases

Phase 2 — API Layer

All WordPress data fetching lives in one file: src/lib/wordpress.ts

TypeScript Types

// src/lib/wordpress.ts
const WP_API = process.env.NEXT_PUBLIC_WP_API_URL;

export interface WPPost {
  id: number;
  slug: string;
  title: { rendered: string };
  excerpt: { rendered: string };
  content: { rendered: string };
  date: string;
  modified: string;
  featured_media: number;
  _embedded?: {
    "wp:featuredmedia"?: Array<{
      source_url: string;
      alt_text: string;
      media_details: { sizes: {
        medium_large?: { source_url: string };
        large?: { source_url: string };
        full: { source_url: string };
      }};
    }>;
  };
}

export interface WPProject extends WPPost {
  acf: {
    project_category?: string;
    project_partner?: string;
    project_overview?: string;
    project_location?: string;
  };
}

Fetch Functions

export async function getAllPosts(perPage = 12): Promise<WPPost[]> {
  const res = await fetch(
    `${WP_API}/posts?_embed&per_page=${perPage}&status=publish`,
    { next: { revalidate: 3600 } }
  );
  if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`);
  return res.json();
}

export async function getPostBySlug(slug: string): Promise<WPPost | null> {
  const res = await fetch(`${WP_API}/posts?slug=${slug}&_embed`,
    { next: { revalidate: 3600 } });
  if (!res.ok) return null;
  const posts: WPPost[] = await res.json();
  return posts[0] ?? null;
}

export async function getLatestPost(): Promise<WPPost | null> {
  const res = await fetch(`${WP_API}/posts?_embed&per_page=1&status=publish`,
    { next: { revalidate: 1800 } });
  if (!res.ok) return null;
  const posts: WPPost[] = await res.json();
  return posts[0] ?? null;
}

export async function getAllProjects(): Promise<WPProject[]> {
  const res = await fetch(`${WP_API}/projects?_embed&per_page=100&status=publish`,
    { next: { revalidate: 3600 } });
  if (!res.ok) throw new Error(`Failed to fetch projects: ${res.status}`);
  return res.json();
}

export async function getProjectBySlug(slug: string): Promise<WPProject | null> {
  const res = await fetch(`${WP_API}/projects?slug=${slug}&_embed`,
    { next: { revalidate: 3600 } });
  if (!res.ok) return null;
  const projects: WPProject[] = await res.json();
  return projects[0] ?? null;
}

Image Helper Functions

export function getFeaturedImageUrl(
  post: WPPost,
  size: "large" | "full" = "large"
): string {
  const media = post._embedded?.["wp:featuredmedia"]?.[0];
  if (!media) return "/images/fallback.jpg";
  if (size === "large") {
    return media.media_details?.sizes?.large?.source_url
        ?? media.media_details?.sizes?.medium_large?.source_url
        ?? media.source_url;
  }
  return media.source_url;
}

export function getFeaturedImageAlt(post: WPPost): string {
  return post._embedded?.["wp:featuredmedia"]?.[0]?.alt_text
    ?? post.title.rendered;
}
TIP

Always use ?_embed in API requests to get featured images in a single request. Without it you would need a second request per post to get the image URL.

Test from Terminal

curl "https://cms.yourdomain.com/wp-json/wp/v2/posts?_embed&per_page=1"
# If you get JSON -> connected
# If you get HTML -> check WordPress permalink settings
PreviousPhase 1 — ScaffoldNextPhase 3 — Dynamic Pages