Build Phases
Phase 3 — Dynamic Pages
Blog listing, individual posts, and the hero news banner.
Blog Listing Page
// src/app/blog/page.tsx
import { getAllPosts, getFeaturedImageUrl, getFeaturedImageAlt } from "@/lib/wordpress";
import Image from "next/image";
import Link from "next/link";
export const metadata = { title: "Blog | Your Site Name" };
export default async function BlogPage() {
const posts = await getAllPosts(12);
return (
<main>
<h1>Blog</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{posts.map((post) => (
<article key={post.id}>
<Link href={`/blog/${post.slug}`}>
<Image
src={getFeaturedImageUrl(post)}
alt={getFeaturedImageAlt(post)}
width={600} height={400} loading="lazy"
/>
<h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
<time dateTime={post.date}>
{new Date(post.date).toLocaleDateString("en-US", {
year:"numeric", month:"long", day:"numeric"
})}
</time>
</Link>
</article>
))}
</div>
</main>
);
}Dynamic Blog Post Page
// src/app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPosts, getFeaturedImageUrl, getFeaturedImageAlt } from "@/lib/wordpress";
import { notFound } from "next/navigation";
import Image from "next/image";
// Pre-generate the 20 most recent posts at build time
export async function generateStaticParams() {
const posts = await getAllPosts(20);
return posts.map((post) => ({ slug: post.slug }));
}
export const dynamicParams = true; // Other pages generate on-demand
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) return { title: "Post Not Found" };
return {
title: post.title.rendered,
description: post.excerpt.rendered.replace(/<[^>]+>/g, "").slice(0, 160),
openGraph: { images: [getFeaturedImageUrl(post, "full")] },
};
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
if (!post) notFound();
return (
<article>
<h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
<Image src={getFeaturedImageUrl(post,"full")} alt={getFeaturedImageAlt(post)}
width={1200} height={630} priority />
{/* Use prose class from @tailwindcss/typography to style WP content */}
<div className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</article>
);
}WordPress content is raw HTML. Add className="prose prose-lg max-w-none" from @tailwindcss/typography to automatically style headings, paragraphs, links, images, and lists inside WordPress content.
Hero News Banner
// src/components/HeroNewsBanner.tsx
import { getLatestPost } from "@/lib/wordpress";
import Link from "next/link";
export default async function HeroNewsBanner() {
const post = await getLatestPost();
if (!post) return null;
return (
<div className="bg-primary text-white py-2 px-4 text-sm">
<span className="font-semibold mr-2">Latest:</span>
<Link href={`/blog/${post.slug}`} className="underline hover:no-underline"
dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
</div>
);
}Key Concepts
| Concept | What it does |
|---|---|
generateStaticParams | Pre-renders the N most recent pages at build time for instant load |
dynamicParams = true | Any slug not in static params generates on first request then caches |
generateMetadata | Per-page SEO title, description, and Open Graph image from WordPress |
dangerouslySetInnerHTML | Renders raw WordPress HTML — always pair with the prose class |