tutorialNext.jsReactApp Router

Next.js App Router: Ultimate Guide for WordPress Developers

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

WordPressNext.js App Router
Theme filesComponents + pages
header.php + footer.phplayout.tsx
Template hierarchyFile-based routing
functions.phpServer/Client components
Loop + WP_QueryData fetching
Pluginsnpm packages
wp-content/uploadspublic/ folder
The CustomizerEnvironment 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 TemplateNext.js Equivalent
front-page.phpapp/page.tsx
page.phpapp/[slug]/page.tsx
archive.phpapp/blog/page.tsx
single.phpapp/blog/[slug]/page.tsx
category.phpapp/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 (

);

}

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

WordPressNext.js App Router
PHP + MySQLJavaScript + Any data source
Everything loads on each requestPages pre-built or cached
Plugins for featuresnpm packages
Admin dashboardCode-based configuration
Updates break thingsVersion 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:

Export your WordPress content to get started →

Share:

Related Articles

View all

Ready to Migrate Your WordPress Site?

Use our free tool to export your WordPress content in minutes.

Start Free Migration