Table of Contents
The Challenge
The digital interface of NGO-3, a prominent non-profit organization, was built on an aging, monolithic WordPress system. Over time, the installation became clogged with third-party plugins, database overhead, and heavy layout themes. The result was severe page bloat, slow page load speeds exceeding 6 seconds on mobile, and constant security vulnerabilities that required frequent patching.
The primary objective of the migration was to extract all content, program records, and historical articles from WordPress and feed them into a blazing-fast, modern, headless frontend. The new platform had to achieve near-perfect Lighthouse performance scores to prevent user bounce, while keeping the client's editing team entirely inside their familiar WordPress admin dashboard.
The requirement: Refactor and migrate the monolithic WordPress platform into a headless, secure Next.js 14 application using CSS Modules. Ensure perfect core web vitals and dynamic incremental data synchronization during static site generation.
Architecture & Solution
We architected a headless CMS structure. The existing WordPress installation serves solely as a content API exposed via WPGraphQL. During build time, Next.js queries this GraphQL endpoint to generate fully optimized, static pages distributed at the edge.
Decoupled Data Pipeline
| Layer | System | Responsibility |
|---|---|---|
| Headless CMS | WordPress + WPGraphQL | Content editing, image media library storage, and taxonomies management. |
| Next.js Core | Next.js 14 App Router | Incremental static build compiles, Server-Side components, and API routing. |
| Styling | Vanilla CSS Modules | Clean, structured layouts without bloated class definitions. |
Tech Stack
| Layer | Technology | Role |
|---|---|---|
| Framework | Next.js 14 (App Router) | Static Site Generation (SSG) with On-Demand Incremental Static Regeneration (ISR). |
| UI Library | React 18 | Declarative component generation. |
| Language | TypeScript | Strong typings for API response schemas and component props. |
| Data Ingestion | Apollo Client / GraphQL | Batch querying WordPress custom post types. |
| Styling | CSS Modules | Localized, high-performance styling eliminating CSS collisions. |
| Deployment | Docker | Containerized runner for running Next.js dynamically in production. |
Key Engineering Decisions
1. Incremental Static Regeneration (ISR) with Webhook Sync
To prevent content editors from having to trigger manual site builds, we designed a webhook receiver endpoint in Next.js. This endpoint accepts updates from WordPress and programmatically triggers page revalidation in milliseconds.
// src/app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath } from 'next/cache';
export async function POST(req: NextRequest) {
try {
const { slug, type } = await req.json();
if (!slug) {
return NextResponse.json({ message: 'Missing slug parameter' }, { status: 400 });
}
const targetPath = type === 'blog' ? `/blog/${slug}` : `/${slug}`;
revalidatePath(targetPath);
return NextResponse.json({ revalidated: true, path: targetPath, now: Date.now() });
} catch (err: any) {
return NextResponse.json({ message: 'Internal revalidation error', error: err.message }, { status: 500 });
}
}2. Batch-Queried GraphQL Ingestion
WordPress relationships can easily create N+1 query bottlenecks during static builds. We designed batched query routines to compile hundreds of pages in a single trip.
// src/lib/wp-graphql.ts
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: process.env.WP_GRAPHQL_ENDPOINT,
cache: new InMemoryCache(),
defaultOptions: {
query: {
fetchPolicy: 'no-cache', // Prevents stale builds in server context
},
},
});
export async function fetchAllPostSlugs() {
const { data } = await client.query({
query: gql`
query GetPostSlugs {
posts(first: 100) {
nodes {
slug
}
}
}
`,
});
return data.posts.nodes.map((node: { slug: string }) => node.slug);
}3. Modular Theme Custom Properties
To maintain perfect brand alignment with the legacy NGO design, we translated the brand system into highly optimized CSS custom variables within a root tokens stylesheet.
/* src/styles/tokens.module.css */
:root {
--color-primary: #1b4d3e; /* NGO forest green */
--color-secondary: #f4a261; /* Warm accent gold */
--color-background: #fafaf9; /* Soft cream canvas */
--color-text: #1c1917; /* Highly readable stone gray */
--font-family-display: 'Lora', Georgia, serif;
--font-family-body: 'Plus Jakarta Sans', sans-serif;
--spacing-grid: 16px;
--border-radius-card: 12px;
}4. Exponential Backoff API Fetch Wrapper
Monolithic WordPress servers are prone to connection timeouts under high build load. We introduced a wrapper to automatically retry transient failed fetches with exponential delay.
// src/shared/utils/fetch-retry.ts
export async function fetchWithRetry(url: string, options: RequestInit, retries = 3, delay = 1000): Promise<Response> {
try {
const res = await fetch(url, options);
if (!res.ok && retries > 0) throw new Error(`HTTP Error ${res.status}`);
return res;
} catch (err) {
if (retries === 0) throw err;
console.warn(`Fetch failed. Retrying in ${delay}ms... (${retries} left)`);
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchWithRetry(url, options, retries - 1, delay * 2);
}
}Deployment
Quick Start
Run the Next.js development server:
# Clone the repository and configure env files
cp .env.example .env.local
# Install monorepo dependencies
pnpm install
# Start the Next.js development router
pnpm devLocal Dev Ports
| Service | Port |
|---|---|
| Next.js Web Portal | PORT_WEB (3005) |
| WPGraphQL Endpoint | http://localhost:8080/graphql |
Architecture Feedback
Spotted a potential optimization or antipattern? Let me know.