Guide for working with Next.js App Router (Next.js 13+). Use when migrating from Pages Router to App Router, creating layouts, implementing routing, handling metadata, or building Next.js 13+ applications. Activates for App Router migration, layout creation, routing patterns, or Next.js 13+ development tasks.
npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-app-router-fundamentals
Provide comprehensive guidance for Next.js App Router (Next.js 13+), covering migration from Pages Router, file-based routing conventions, layouts, metadata handling, and modern Next.js patterns.
any TypeCRITICAL RULE: This codebase has @typescript-eslint/no-explicit-any enabled. Using any will cause build failures.
❌ WRONG:
function handleSubmit(e: any) { ... }
const data: any[] = [];
✅ CORRECT:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
// Server actions
async function myAction(formData: FormData) { ... }
Use this skill when:
pages/ directory) to App Router (app/ directory)Pages Router (Legacy - Next.js 12 and earlier):
pages/
├── index.tsx # Route: /
├── about.tsx # Route: /about
├── _app.tsx # Custom App component
├── _document.tsx # Custom Document component
└── api/ # API routes
└── hello.ts # API endpoint: /api/hello
App Router (Modern - Next.js 13+):
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Route: /
├── about/ # Route: /about
│ └── page.tsx
├── blog/
│ ├── layout.tsx # Nested layout
│ └── [slug]/
│ └── page.tsx # Dynamic route: /blog/:slug
└── api/ # Route handlers
└── hello/
└── route.ts # API endpoint: /api/hello
Special Files in App Router:
layout.tsx - Shared UI for a segment and its children (preserves state, doesn't re-render)page.tsx - Unique UI for a route, makes route publicly accessibleloading.tsx - Loading UI with React Suspenseerror.tsx - Error UI with Error Boundariesnot-found.tsx - 404 UItemplate.tsx - Similar to layout but re-renders on navigationroute.ts - API endpoints (Route Handlers)Colocation:
app/page.tsx and route.ts files create public routesExamine existing Pages Router setup:
pages/ directory structure_app.tsx - handles global state, layouts, providers_document.tsx - customizes HTML structurenext/head, <Head> component)Create app/layout.tsx - REQUIRED for all App Router applications:
// app/layout.tsx
export const metadata = {
title: 'My App',
description: 'App description',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Migration Notes:
_document.tsx HTML structure to layout.tsx_app.tsx global providers/wrappers to layout.tsx<Head> metadata to metadata export<html> and <body> tagsSimple Page Migration:
// Before: pages/index.tsx
import Head from 'next/head';
export default function Home() {
return (
<>
<Head>
<title>Home Page</title>
</Head>
<main>
<h1>Welcome</h1>
</main>
</>
);
}
// After: app/page.tsx
export default function Home() {
return (
<main>
<h1>Welcome</h1>
</main>
);
}
// Metadata moved to layout.tsx or exported here
export const metadata = {
title: 'Home Page',
};
Nested Route Migration:
// Before: pages/blog/[slug].tsx
export default function BlogPost() { ... }
// After: app/blog/[slug]/page.tsx
export default function BlogPost() { ... }
Replace anchor tags with Next.js Link:
// Before (incorrect in App Router)
<a href="/about">About</a>
// After (correct)
import Link from 'next/link';
<Link href="/about">About</Link>
After migration:
pages/ directorypages/api/ if you're not migrating API routes yet_app.tsx and _document.tsx (functionality moved to layout)pages/ directory// app/page.tsx or app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description',
keywords: ['nextjs', 'react'],
openGraph: {
title: 'My Page',
description: 'Page description',
images: ['/og-image.jpg'],
},
};
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
// app/layout.tsx - Root layout
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/blog/layout.tsx - Blog layout
export default function BlogLayout({ children }) {
return (
<div>
<BlogSidebar />
<main>{children}</main>
</div>
);
}
Layout Behavior:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return <article>Post: {params.slug}</article>;
}
// app/shop/[...slug]/page.tsx - Matches /shop/a, /shop/a/b, etc.
export default function Shop({
params
}: {
params: { slug: string[] }
}) {
return <div>Path: {params.slug.join('/')}</div>;
}
// app/shop/[[...slug]]/page.tsx - Matches /shop AND /shop/a, /shop/a/b
Group routes without affecting URL:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
└── (shop)/
└── products/
└── page.tsx # /products
Wrong:
export default function RootLayout({ children }) {
return <div>{children}</div>; // Missing <html> and <body>
}
Correct:
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
next/head in App RouterWrong:
import Head from 'next/head';
export default function Page() {
return (
<>
<Head><title>Title</title></Head>
<main>Content</main>
</>
);
}
Correct:
export const metadata = { title: 'Title' };
export default function Page() {
return <main>Content</main>;
}
After migrating routes, remove the old pages/ directory files to avoid confusion. The build will fail if you have conflicting routes.
page.tsx FilesRoutes are NOT accessible without a page.tsx file. Layouts alone don't create routes.
app/
├── blog/
│ ├── layout.tsx # NOT a route
│ └── page.tsx # This makes /blog accessible
Wrong:
<a href="/about">About</a> // Works but causes full page reload
Correct:
import Link from 'next/link';
<Link href="/about">About</Link> // Client-side navigation
All components in app/ are Server Components by default:
// app/page.tsx - Server Component (default)
export default async function Page() {
const data = await fetch('https://api.example.com/data');
const json = await data.json();
return <div>{json.title}</div>;
}
Benefits:
Use 'use client' directive when you need:
// app/components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default async function Page() {
// Fetch in parallel
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json()),
]);
return (/* render */);
}
generateStaticParams is the App Router equivalent of getStaticPaths from the Pages Router. It generates static pages at build time for dynamic routes.
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
// Return array of params to pre-render
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
export default function BlogPost({
params
}: {
params: { id: string }
}) {
return <article>Blog post {params.id}</article>;
}
Key Points:
generateStaticParams'use client' directive)getStaticPaths// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// app/products/[category]/[id]/page.tsx
export async function generateStaticParams() {
const categories = await getCategories();
const params = [];
for (const category of categories) {
const products = await getProducts(category.slug);
for (const product of products) {
params.push({
category: category.slug,
id: product.id,
});
}
}
return params;
}
export default function ProductPage({
params
}: {
params: { category: string; id: string }
}) {
return <div>Category: {params.category}, Product: {params.id}</div>;
}
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }];
}
// Control behavior for non-pre-rendered paths
export const dynamicParams = true; // default - allows runtime generation
// export const dynamicParams = false; // returns 404 for non-pre-rendered paths
export default function BlogPost({
params
}: {
params: { id: string }
}) {
return <article>Post {params.id}</article>;
}
Options:
dynamicParams = true (default): Non-pre-rendered paths generated on-demanddynamicParams = false: Non-pre-rendered paths return 404Pattern 1: Simple ID-based routes
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
{ id: '3' },
];
}
Pattern 2: Fetch from API
export async function generateStaticParams() {
const items = await fetch('https://api.example.com/items').then(r => r.json());
return items.map(item => ({ id: item.id }));
}
Pattern 3: Database query
export async function generateStaticParams() {
const posts = await db.post.findMany();
return posts.map(post => ({ slug: post.slug }));
}
Before (Pages Router):
// pages/blog/[id].tsx
export async function getStaticPaths() {
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } },
],
fallback: false,
};
}
export async function getStaticProps({ params }) {
return { props: { id: params.id } };
}
After (App Router):
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
return [
{ id: '1' },
{ id: '2' },
];
}
export const dynamicParams = false; // equivalent to fallback: false
export default function BlogPost({ params }: { params: { id: string } }) {
return <div>Post {params.id}</div>;
}
❌ Wrong: Using 'use client'
'use client'; // ERROR! generateStaticParams only works in Server Components
export async function generateStaticParams() {
return [{ id: '1' }];
}
❌ Wrong: Using Pages Router pattern
export async function getStaticPaths() { // Wrong API!
return { paths: [...], fallback: false };
}
❌ Wrong: Missing export keyword
async function generateStaticParams() { // Must be exported!
return [{ id: '1' }];
}
✅ Correct: Clean Server Component
// app/blog/[id]/page.tsx
// No 'use client' directive
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }];
}
export default function Page({ params }: { params: { id: string } }) {
return <div>Post {params.id}</div>;
}
CRITICAL IMPLEMENTATION NOTE:
When asked to "write" or "implement" generateStaticParams:
When migrating or building with App Router, verify:
app/ directory existslayout.tsx exists with <html> and <body>page.tsx filenext/head imports in App RouterMetadata typeLink component from next/link<a> tags for internal navigationpages/ directory_app.tsx and _document.tsx removed| Pages Router | App Router | Purpose |
|-------------|-----------|---------|
| pages/index.tsx | app/page.tsx | Home route |
| pages/about.tsx | app/about/page.tsx | About route |
| pages/[id].tsx | app/[id]/page.tsx | Dynamic route |
| pages/_app.tsx | app/layout.tsx | Global layout |
| pages/_document.tsx | app/layout.tsx | HTML structure |
| pages/api/hello.ts | app/api/hello/route.ts | API route |
# Create new Next.js app with App Router
npx create-next-app@latest my-app
# Run development server
npm run dev
# Build for production
npm run build
# Start production server
npm start
For more advanced routing patterns (parallel routes, intercepting routes, route handlers), refer to the nextjs-advanced-routing skill.
For Server vs Client component best practices and anti-patterns, refer to the nextjs-server-client-components and nextjs-anti-patterns skills.
Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.
Frontend development guidelines for React/TypeScript applications. Modern patterns including Suspense, lazy loading, useSuspenseQuery, file organization with features directory, MUI v7 styling, TanStack Router, performance optimization, and TypeScript best practices. Use when creating components, pages, features, fetching data, styling, routing, or working with frontend code.
Next.js 16 Cache Components - PPR, use cache directive, cacheLife, cacheTag, updateTag
| Build Shopify apps, extensions, themes using GraphQL Admin API, Shopify CLI, Polaris UI, and Liquid. "shopify theme", "liquid template", "polaris", "shopify graphql", "shopify webhook", "shopify billing", "app subscription", "metafields", "shopify functions"
Build Gradio web UIs and demos in Python. Use when creating or editing Gradio apps, components, event listeners, layouts, or chatbots.
MANDATORY prerequisite — load this skill BEFORE every `generate_diagram` tool call. NEVER call `generate_diagram` directly without loading this skill first. Trigger whenever the user asks to create, generate, draw, render, sketch, or build a diagram — flowchart, architecture diagram, sequence diagram, ERD or entity-relationship diagram, state diagram or state machine, gantt chart, or timeline. Also trigger when the user mentions Mermaid syntax or wants a system architecture, decision tree, dependency graph, API call flow, auth handshake, schema, or pipeline visualized in FigJam. Routes to type-specific guidance, sets universal Mermaid constraints, and tells you when to use a different diagram type or skip the tool entirely (mindmaps, pie charts, class diagrams, etc.).
Analyzes web performance using Chrome DevTools MCP. Measures Core Web Vitals (LCP, INP, CLS) and supplementary metrics (FCP, TBT, Speed Index), identifies render-blocking resources, network dependency chains, layout shifts, caching issues, and accessibility gaps. Use when asked to audit, profile, debug, or optimize page load performance, Lighthouse scores, or site speed. Biases towards retrieval from current documentation over pre-trained knowledge.
Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms).
Take wsimmonds/nextjs-app-router-fundamentals from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.