Next.js Integration

Build production-ready Next.js sites with TypeScript and App Router

Setup

1. Environment Variables

Create .env.local:

NEXT_PUBLIC_WP_API_URL=https://yoursite.com/wp-json
WP_API_KEY=hb_your_key_here

2. Create WordPress Client

Create lib/wordpress.ts:

const API_URL = `${process.env.NEXT_PUBLIC_WP_API_URL}/bridge/v1`;

export async function getPage(slug: string) {
  const res = await fetch(`${API_URL}/page?slug=${slug}`, {
    next: { revalidate: 3600 }
  });
  
  if (!res.ok) {
    throw new Error('Failed to fetch page');
  }
  
  return res.json();
}

export async function getPages(limit = 20, offset = 0) {
  const res = await fetch(
    `${API_URL}/pages?type=post&limit=${limit}&offset=${offset}`,
    { next: { revalidate: 3600 } }
  );
  
  return res.json();
}

export async function getAllPostSlugs() {
  const data = await getPages(100);
  return data.items.map((post: any) => post.slug);
}

App Router Examples

Blog Post Page

Create app/blog/[slug]/page.tsx:

import { getPage, getAllPostSlugs } from '@/lib/wordpress';
import { Metadata } from 'next';

type Props = {
  params: { slug: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPage(params.slug);
  
  return {
    title: post.seo.title,
    description: post.seo.description,
    openGraph: {
      images: [post.seo.ogImage],
    },
  };
}

export async function generateStaticParams() {
  const slugs = await getAllPostSlugs();
  return slugs.map((slug: string) => ({ slug }));
}

export default async function BlogPost({ params }: Props) {
  const post = await getPage(params.slug);
  
  return (
    <article className="max-w-4xl mx-auto px-4 py-16">
      <header className="mb-8">
        <h1 className="text-5xl font-bold mb-4">{post.title}</h1>
        <div className="flex items-center gap-4 text-gray-600">
          <span>{post.author.name}</span>
          <span>•</span>
          <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
        </div>
      </header>
      
      {post.featuredImage && (
        <img 
          src={post.featuredImage.url}
          alt={post.featuredImage.alt}
          srcSet={post.featuredImage.srcset}
          sizes={post.featuredImage.sizes}
          className="w-full rounded-lg mb-8"
        />
      )}
      
      <div 
        className="prose prose-lg max-w-none"
        dangerouslySetInnerHTML={{ __html: post.content }}
      />
    </article>
  );
}

Blog List Page

Create app/blog/page.tsx:

import { getPages } from '@/lib/wordpress';
import Link from 'next/link';

export default async function BlogIndex() {
  const data = await getPages(20);
  
  return (
    <div className="max-w-6xl mx-auto px-4 py-16">
      <h1 className="text-5xl font-bold mb-12">Blog</h1>
      <div className="grid md:grid-cols-3 gap-8">
        {data.items.map((post: any) => (
          <Link
            key={post.uuid}
            href={`/blog/${post.slug}`}
            className="border rounded-lg overflow-hidden hover:shadow-lg transition"
          >
            {post.featuredImage && (
              <img
                src={post.featuredImage.url}
                alt={post.featuredImage.alt}
                className="w-full h-48 object-cover"
              />
            )}
            <div className="p-6">
              <h2 className="text-xl font-bold mb-2">{post.title}</h2>
              <p className="text-gray-600">{post.excerpt}</p>
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

TypeScript Types

Create types/wordpress.ts:

export interface Post {
  uuid: string;
  slug: string;
  type: string;
  status: string;
  title: string;
  excerpt: string;
  content: string;
  publishedAt: string;
  modifiedAt: string;
  author: Author;
  seo: SEO;
  featuredImage?: FeaturedImage;
  categories: Category[];
  tags: Tag[];
  acf?: Record<string, any>;
}

export interface Author {
  id: number;
  name: string;
  slug: string;
  avatar: string;
  bio: string;
  url: string;
}

export interface SEO {
  title: string;
  description: string;
  canonical: string;
  noindex: boolean;
  ogImage: string;
}

export interface FeaturedImage {
  id: number;
  url: string;
  alt: string;
  width: number;
  height: number;
  srcset: string;
  sizes: string;
}

ISR (Incremental Static Regeneration)

Revalidate every hour:

export const revalidate = 3600; // 1 hour

export default async function Page() {
  const posts = await getPages();
  return <div>{/* ... */}</div>;
}

On-Demand Revalidation

Create webhook endpoint app/api/revalidate/route.ts:

import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const slug = body.slug;
  
  revalidatePath(`/blog/${slug}`);
  revalidatePath('/blog');
  
  return Response.json({ revalidated: true });
}

Deploy to Vercel

  1. Push your code to GitHub
  2. Import project in Vercel
  3. Add environment variables
  4. Deploy!

Next Steps

  • Add pagination to blog list
  • Implement category/tag pages
  • Set up webhooks for auto-revalidation (Pro)
  • Add search with Algolia (Pro)