Initializing
Back to Projects
Year2026
DomainFrontend
AccessPrivate Repo
Complexity7.8 / 10
ScreenshotsArchitecture Docs
Next.js 14React 18TypeScriptGraphQLCSS ModulesWordPress APIHonoDocker
Frontendwip

NGO-4 Website Migration — WordPress to Next.js Headless Conversion

A high-performance headless refactoring of a legacy WordPress NGO website into a fast, accessible Next.js 14 web application.

Lighthouse Performance0+
Static Pages Generated0+
Asset Size Reduced0%
FCP Duration0.0s

Table of Contents


The Challenge

Similar to other legacy platforms, NGO-4's web portal suffered from standard monolithic system degradation. Running on old WordPress hosting, the site suffered from slow loading speeds, high server overhead, and an accessible layout structure that did not satisfy modern WCAG 2.1 AA accessibility regulations.

The main challenge was converting this complex, media-heavy portal into an ultra-fast headless Next.js frontend, ensuring complete accessibility compliance for all users, including those using screen readers and keyboard-only navigation. Additionally, form handlers (contact submissions, program registrations) had to be securely isolated from the public frontend and routed to the WordPress database.

The requirement: Implement a secure, highly accessible (WCAG AA compliant) headless Next.js 14 frontend. Migrate legacy media assets into an optimized, responsive pipeline, and provide a secure contact form relay gateway.


Architecture & Solution

The system separates the content management workspace from the delivery network. Editors continue to use WordPress as a data entry database. Build pipelines query the API and build static files served globally via a fast Edge CDN.

Parsing system architecture diagram...

Modular Headless Segments

SegmentTechnologyResponsibility
CMS WorkspaceWordPress AdminContent editing, taxonomies, and layout administration.
API MiddlewareHono API GatewayCORS validation, input validation, and secure form relays.
Edge PresentationNext.js + CSS ModulesFully static, accessible HTML render paths with instant hydration.

Tech Stack

LayerTechnologyRole
Frontend FrameworkNext.js 14Edge-rendered pages, SSR fallback routes, and localized routing.
API MiddlewareHonoLightweight HTTP gateway and input validation handler.
StylingVanilla CSS ModulesCompletely scoped layout rules with zero CSS bloat.
Accessibility ToolsAxe Core & ESLintAutomatic WCAG 2.1 accessibility auditing.
Data FetchingAxios ClientTyped API fetch wrappers.
DeploymentDocker & PM2Containerized local testing and VPS process supervision.

Key Engineering Decisions

1. Hono-Secured Form Relay Gateway

To prevent bots from directly abusing legacy WordPress endpoint resources, we introduced a Hono API proxy. The proxy validates user requests with Zod schemas before transmitting the data to WordPress.

typescript
// src/api/forms.ts
import { Hono } from 'hono';
import { z } from 'zod';

const contactSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  message: z.string().min(10).max(1000),
});

const app = new Hono();

app.post('/contact-relay', async (c) => {
  const body = await c.req.json();
  const parsed = contactSchema.safeParse(body);
  if (!parsed.success) {
    return c.json({ success: false, errors: parsed.error.format() }, 422);
  }

  // Forward to WordPress Contact Form endpoint safely
  const response = await fetch(`${process.env.WP_API_URL}/contact-form-7/v1/contact-forms/1/feedback`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(parsed.data),
  });

  return c.json({ success: response.ok });
});
export default app;

2. Accessible (a11y) Keyboard-Trapped Hamburger Menu

We built a custom React drawer component that satisfies strict WCAG AA rules. It captures keyboard focus when open and closes smoothly with the Escape key.

typescript
// src/components/common/NavbarDrawer.tsx
import React, { useEffect, useRef } from 'react';

export function NavbarDrawer({ isOpen, onClose, children }: { isOpen: boolean; onClose: () => void; children: React.ReactNode }) {
  const drawerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!isOpen) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
      // Handle focus trapping inside drawerRef
      if (e.key === 'Tab') {
        const focusables = drawerRef.current?.querySelectorAll('a, button') || [];
        const first = focusables[0] as HTMLElement;
        const last = focusables[focusables.length - 1] as HTMLElement;

        if (e.shiftKey && document.activeElement === first) {
          last.focus();
          e.preventDefault();
        } else if (!e.shiftKey && document.activeElement === last) {
          first.focus();
          e.preventDefault();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);

  return (
    <div ref={drawerRef} role="dialog" aria-modal="true" aria-hidden={!isOpen} className="drawer">
      {children}
    </div>
  );
}

3. Progressive Image Srcset Migration

WordPress posts often contain raw <img> tags pointing to absolute URLs. We designed an isomorphic parser that intercepts these tags and serves them via highly responsive WebP sources.

typescript
// src/shared/utils/image-srcset.ts
export function parseWordPressHtmlImages(htmlContent: string): string {
  // Replace legacy img sources with responsive source attributes
  return htmlContent.replace(
    /<img([\s\S]*?)src="https:\/\/wp-domain\.org\/wp-content\/uploads\/(.*?)"([\s\S]*?)>/gi,
    (match, before, filePath, after) => {
      const optimizedUrl = `https://cdn.ngo4.org/uploads/${filePath}`;
      return `<img ${before} src="${optimizedUrl}" srcset="${optimizedUrl}?w=400 400w, ${optimizedUrl}?w=800 800w" sizes="(max-width: 600px) 480px, 800px" ${after} loading="lazy" />`;
    }
  );
}

4. Zero-Collision Layout Styles

We adopted strict CSS Modules. This eliminates the risk of styling rules leaking between components and maintains perfect rendering speeds.

css
/* src/components/projects/ProjectCard.module.css */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--border-radius-card);
  padding: var(--spacing-grid);
  transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

.card:hover {
  transform: translateY(-4px);
  border-color: var(--color-accent);
}

Deployment

Quick Start

Launch Next.js development server:

bash
# Set environment variables
cp .env.example .env.local

# Install pnpm workspace dependencies
pnpm install

# Start the dev build script
pnpm dev

Local Dev Ports

ComponentPort
Next.js Web ServerPORT_WEB (3005)
Hono API ProxyPORT_API (4000)

Architecture Feedback

Spotted a potential optimization or antipattern? Let me know.

Submit a Technical Suggestion