Next.js App Router: Ultimate Guide for WordPress Developers
Muhammad Bilal Azhar
Co-Founder & Technical Lead · Google Cloud Certified Professional
Next.js App Router: Ultimate Guide for WordPress Developers
The Next.js App Router is a game-changer for building modern websites. If you're coming from WordPress, this guide translates familiar concepts into the App Router paradigm.
WordPress vs Next.js Concepts
| WordPress | Next.js App Router |
| Theme files | Components + pages |
header.php + footer.php | layout.tsx |
| Template hierarchy | File-based routing |
functions.php | Server/Client components |
| Loop + WP_Query | Data fetching |
| Plugins | npm packages |
wp-content/uploads | public/ folder |
| The Customizer | Environment variables + config |
Project Structure
WordPress:
wp-content/
├── themes/
│ └── my-theme/
│ ├── header.php
│ ├── footer.php
│ ├── index.php
│ ├── single.php
│ ├── page.php
│ └── functions.php
Next.js App Router:
app/
├── layout.tsx # Global layout (header/footer)
├── page.tsx # Homepage
├── blog/
│ ├── page.tsx # Blog index
│ └── [slug]/
│ └── page.tsx # Individual blog post
├── globals.css # Global styles
components/
├── Header.tsx
├── Footer.tsx
└── PostCard.tsx
lib/
└── posts.ts # Data fetching functions
File-Based Routing
How It Works
Every folder in app/ becomes a route. The page.tsx file renders that route.
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/my-post
└── contact/
└── page.tsx → /contact
WordPress Equivalent
| WordPress Template | Next.js Equivalent |
front-page.php | app/page.tsx |
page.php | app/[slug]/page.tsx |
archive.php | app/blog/page.tsx |
single.php | app/blog/[slug]/page.tsx |
category.php | app/category/[slug]/page.tsx |
Layouts (Like header.php + footer.php)
Root Layout
app/layout.tsx wraps your entire application:
// app/layout.tsx
import { Inter } from 'next/font/google';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
This is like combining header.php and footer.php. The {children} is where your page content renders.
Nested Layouts
Create layouts for specific sections:
// app/blog/layout.tsx
export default function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
All pages under /blog/* will have this sidebar.
Server Components (Default)
In the App Router, components are Server Components by default. They run on the server and send HTML to the browser.
Example: Fetching Posts
// app/blog/page.tsx
import { getAllPosts } from '@/lib/posts';
import PostCard from '@/components/PostCard';
export default async function BlogPage() {
// This runs on the server, not in the browser
const posts = await getAllPosts();
return (
Blog
{posts.map((post) => (
))}
);
}
WordPress Equivalent
// In WordPress, similar to:
$posts = new WP_Query(['post_type' => 'post']);
while ($posts->have_posts()) : $posts->the_post();
// Output post
endwhile;
Benefits of Server Components
- No JavaScript sent to browser (smaller bundle)
- Direct database/API access
- Secure (secrets never exposed)
- Fast (no hydration needed)
Client Components (When You Need Interactivity)
For interactivity (clicks, forms, state), use Client Components:
// components/LikeButton.tsx
'use client'; // This makes it a Client Component
import { useState } from 'react';
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return (
❤️ {likes}
);
}
Use 'use client' only when you need:
- Event handlers (
onClick,onChange)
- State (
useState,useReducer)
- Effects (
useEffect)
- Browser APIs
Data Fetching
Fetching in Server Components
// app/blog/[slug]/page.tsx
import { getPostBySlug } from '@/lib/posts';
import { notFound } from 'next/navigation';
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const post = await getPostBySlug(params.slug);
if (!post) {
notFound(); // Shows 404 page
}
return (
{post.title}
);
}
Static Generation (Build Time)
Generate pages at build time with generateStaticParams:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// This page will be generated at build time for each slug
This is like WordPress generating static HTML for each post.
Metadata (SEO)
Static Metadata
// app/page.tsx
export const metadata = {
title: 'My Website',
description: 'Welcome to my website',
openGraph: {
title: 'My Website',
description: 'Welcome to my website',
images: ['/og-image.jpg'],
},
};
Dynamic Metadata
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params
}: {
params: { slug: string }
}) {
const post = await getPostBySlug(params.slug);
return {
title: post?.title || 'Post Not Found',
description: post?.excerpt,
openGraph: {
title: post?.title,
description: post?.excerpt,
},
};
}
This is like Yoast SEO but built into your code.
Creating a Blog Post Data Layer
The Data File
// lib/posts.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const postsDirectory = path.join(process.cwd(), 'content/posts');
export interface Post {
slug: string;
title: string;
excerpt: string;
content: string;
date: string;
author: string;
}
export async function getAllPosts(): Promise {
const files = fs.readdirSync(postsDirectory);
const posts = files
.filter((file) => file.endsWith('.md') || file.endsWith('.mdx'))
.map((file) => {
const slug = file.replace(/\.mdx?$/, '');
const fullPath = path.join(postsDirectory, file);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
return {
slug,
title: data.title,
excerpt: data.excerpt,
content,
date: data.date,
author: data.author,
};
})
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return posts;
}
export async function getPostBySlug(slug: string): Promise {
const fullPath = path.join(postsDirectory, ${slug}.mdx);
if (!fs.existsSync(fullPath)) {
return null;
}
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
return {
slug,
title: data.title,
excerpt: data.excerpt,
content,
date: data.date,
author: data.author,
};
}
This is like WP_Query, but for Markdown files.
Handling Dynamic Routes
Dynamic Segments
[slug] → Matches one segment: /blog/hello-world
[...slug] → Matches multiple: /docs/getting-started/installation
[[...slug]] → Matches zero or more: / or /category or /category/sub
Example: Category Pages
// app/category/[...slug]/page.tsx
export default function CategoryPage({
params
}: {
params: { slug: string[] }
}) {
// /category/tech → params.slug = ['tech']
// /category/tech/nextjs → params.slug = ['tech', 'nextjs']
return
Category: {params.slug.join(' / ')};
}
Loading and Error States
Loading UI
Create loading.tsx for loading states:
// app/blog/loading.tsx
export default function Loading() {
return (
Loading posts...
);
}
This shows while the page is loading.
Error Handling
Create error.tsx for error states:
// app/blog/error.tsx
'use client'; // Error component must be Client Component
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
Something went wrong!
);
}
Not Found
Create not-found.tsx for 404 pages:
// app/not-found.tsx
export default function NotFound() {
return (
404 - Page Not Found
The page you're looking for doesn't exist.
);
}
API Routes (Like admin-ajax.php)
Create API endpoints in the App Router:
// app/api/subscribe/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { email } = await request.json();
// Process subscription
// Add to database, send to email service, etc.
return NextResponse.json({ success: true });
}
Access at: POST /api/subscribe
Migrating from WordPress
Step 1: Export Content
Use our export tool or WP REST API to get your content.
Step 2: Create Data Layer
Set up functions to read your exported content (Markdown files, JSON, or API).
Step 3: Build Pages
Create pages using the file-based routing system.
Step 4: Add Functionality
Replace plugins with npm packages and custom code.
Step 5: Deploy
Push to GitHub and deploy to Vercel (free).
Key Differences to Remember
| WordPress | Next.js App Router |
| PHP + MySQL | JavaScript + Any data source |
| Everything loads on each request | Pages pre-built or cached |
| Plugins for features | npm packages |
| Admin dashboard | Code-based configuration |
| Updates break things | Version control + testing |
FAQ
Q: Is this harder than WordPress?
Learning is harder. Maintaining is easier. No plugin conflicts, no security patches, no database management.
Q: Can I use a CMS?
Yes! Use Sanity, Contentlayer, or even WordPress as a headless CMS. See our headless CMS comparison →
Q: Where do I host this?
Vercel (free tier is generous), Netlify, or Cloudflare Pages. Compare hosting options →
Q: What about TypeScript?
Next.js has built-in TypeScript support. See our TypeScript guide →
Next Steps
1. Create a Next.js project: npx create-next-app@latest
2. Read the official docs: nextjs.org/docs/app
3. Build something: Start with a simple blog
4. Migrate gradually: Export WordPress content and rebuild
Related guides:
Related Articles
View allWordPress to Next.js Migration: The Complete 2026 Guide
Everything you need to know about migrating from WordPress to Next.js. From planning to deployment, this guide covers the entire migration process.
WP to Next.js: Complete Migration Guide (2026)
The complete guide to migrating from WP (WordPress) to Next.js. Learn how to move your WordPress site to Next.js in under 10 minutes with our free tool.
Convert WordPress to React: Complete Guide (2026)
Learn how to convert your WordPress site to React/Next.js. Step-by-step guide with free tools to migrate WordPress to React-based frameworks.
Headless WordPress with Next.js: Complete Tutorial [2026]
Learn how to use WordPress as a headless CMS with Next.js. Get the best of both worlds - WordPress content editing with Next.js performance.