Install
Terminal · npx$
npx skills add https://github.com/wsimmonds/claude-nextjs-skills --skill nextjs-app-router-fundamentalsWorks with Paperclip
How Nextjs App Router Fundamentals fits into a Paperclip company.
Nextjs App Router Fundamentals drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
S
SaaS FactoryPaired
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
$27$59
Explore packSource file
SKILL.md784 linesExpandCollapse
---name: nextjs-app-router-fundamentalsdescription: 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.allowed-tools: Read, Write, Edit, Glob, Grep, Bash--- # Next.js App Router Fundamentals ## Overview 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. ## TypeScript: NEVER Use `any` Type **CRITICAL RULE:** This codebase has `@typescript-eslint/no-explicit-any` enabled. Using `any` will cause build failures. **❌ WRONG:**```typescriptfunction handleSubmit(e: any) { ... }const data: any[] = [];``` **✅ CORRECT:**```typescriptfunction handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }const data: string[] = [];``` ### Common Next.js Type Patterns ```typescript// Page propsfunction Page({ params }: { params: { slug: string } }) { ... }function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... } // Form eventsconst handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... } // Server actionsasync function myAction(formData: FormData) { ... }``` ## When to Use This Skill Use this skill when:- Migrating from Pages Router (`pages/` directory) to App Router (`app/` directory)- Creating Next.js 13+ applications from scratch- Working with layouts, templates, and nested routing- Implementing metadata and SEO optimizations- Building with App Router routing conventions- Handling route groups, parallel routes, or intercepting routes basics ## Core Concepts ### App Router vs Pages Router **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``` ### File Conventions **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 accessible- `loading.tsx` - Loading UI with React Suspense- `error.tsx` - Error UI with Error Boundaries- `not-found.tsx` - 404 UI- `template.tsx` - Similar to layout but re-renders on navigation- `route.ts` - API endpoints (Route Handlers) **Colocation:**- Components, tests, and other files can be colocated in `app/`- Only `page.tsx` and `route.ts` files create public routes- Other files (components, utils, tests) are NOT routable ## Migration Guide: Pages Router to App Router ### Step 1: Understand the Current Structure Examine existing Pages Router setup:- Read `pages/` directory structure- Identify `_app.tsx` - handles global state, layouts, providers- Identify `_document.tsx` - customizes HTML structure- Note metadata usage (`next/head`, `<Head>` component)- List all routes and dynamic segments ### Step 2: Create Root Layout Create `app/layout.tsx` - **REQUIRED** for all App Router applications: ```typescript// app/layout.tsxexport 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:**- Move `_document.tsx` HTML structure to `layout.tsx`- Move `_app.tsx` global providers/wrappers to `layout.tsx`- Convert `<Head>` metadata to `metadata` export- The root layout **MUST** include `<html>` and `<body>` tags ### Step 3: Migrate Pages to Routes **Simple Page Migration:**```typescript// Before: pages/index.tsximport Head from 'next/head'; export default function Home() { return ( <> <Head> <title>Home Page</title> </Head> <main> <h1>Welcome</h1> </main> </> );}``` ```typescript// After: app/page.tsxexport default function Home() { return ( <main> <h1>Welcome</h1> </main> );} // Metadata moved to layout.tsx or exported hereexport const metadata = { title: 'Home Page',};``` **Nested Route Migration:**```typescript// Before: pages/blog/[slug].tsxexport default function BlogPost() { ... }``` ```typescript// After: app/blog/[slug]/page.tsxexport default function BlogPost() { ... }``` ### Step 4: Update Navigation Replace anchor tags with Next.js Link: ```typescript// Before (incorrect in App Router)<a href="/about">About</a> // After (correct)import Link from 'next/link';<Link href="/about">About</Link>``` ### Step 5: Clean Up Pages Directory After migration:- Remove all page files from `pages/` directory- Keep `pages/api/` if you're not migrating API routes yet- Remove `_app.tsx` and `_document.tsx` (functionality moved to layout)- Optionally delete empty `pages/` directory ## Metadata Handling ### Static Metadata ```typescript// app/page.tsx or app/layout.tsximport 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'], },};``` ### Dynamic Metadata ```typescript// app/blog/[slug]/page.tsxexport async function generateMetadata({ params}: { params: { slug: string }}): Promise<Metadata> { const post = await getPost(params.slug); return { title: post.title, description: post.excerpt, };}``` ## Layouts and Nesting ### Creating Nested Layouts ```typescript// app/layout.tsx - Root layoutexport default function RootLayout({ children }) { return ( <html> <body> <Header /> {children} <Footer /> </body> </html> );} // app/blog/layout.tsx - Blog layoutexport default function BlogLayout({ children }) { return ( <div> <BlogSidebar /> <main>{children}</main> </div> );}``` **Layout Behavior:**- Layouts preserve state across navigation- Layouts don't re-render on route changes- Parent layouts wrap child layouts- Root layout is required and wraps entire app ## Routing Patterns ### Dynamic Routes ```typescript// app/blog/[slug]/page.tsxexport default function BlogPost({ params}: { params: { slug: string }}) { return <article>Post: {params.slug}</article>;}``` ### Catch-All Routes ```typescript// 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>;}``` ### Optional Catch-All ```typescript// app/shop/[[...slug]]/page.tsx - Matches /shop AND /shop/a, /shop/a/b``` ### Route Groups Group routes without affecting URL: ```app/├── (marketing)/│ ├── about/│ │ └── page.tsx # /about│ └── contact/│ └── page.tsx # /contact└── (shop)/ └── products/ └── page.tsx # /products``` ## Common Migration Pitfalls ### Pitfall 1: Forgetting Root Layout HTML Tags **Wrong:**```typescriptexport default function RootLayout({ children }) { return <div>{children}</div>; // Missing <html> and <body>}``` **Correct:**```typescriptexport default function RootLayout({ children }) { return ( <html lang="en"> <body>{children}</body> </html> );}``` ### Pitfall 2: Using `next/head` in App Router **Wrong:**```typescriptimport Head from 'next/head'; export default function Page() { return ( <> <Head><title>Title</title></Head> <main>Content</main> </> );}``` **Correct:**```typescriptexport const metadata = { title: 'Title' }; export default function Page() { return <main>Content</main>;}``` ### Pitfall 3: Not Removing Pages Directory After migrating routes, remove the old `pages/` directory files to avoid confusion. The build will fail if you have conflicting routes. ### Pitfall 4: Missing `page.tsx` Files Routes 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``` ### Pitfall 5: Incorrect Link Usage **Wrong:**```typescript<a href="/about">About</a> // Works but causes full page reload``` **Correct:**```typescriptimport Link from 'next/link';<Link href="/about">About</Link> // Client-side navigation``` ## Server Components vs Client Components ### Default: Server Components All components in `app/` are Server Components by default: ```typescript// 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:**- Can use async/await directly- Direct database/API access- Zero client-side JavaScript- Automatic code splitting ### Client Components Use `'use client'` directive when you need:- Interactive elements (onClick, onChange, etc.)- React hooks (useState, useEffect, useContext, etc.)- Browser APIs (window, localStorage, etc.)- Event listeners ```typescript// 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> );}``` ## Data Fetching Patterns ### Server Component Data Fetching ```typescript// app/posts/page.tsxasync 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> );}``` ### Parallel Data Fetching ```typescriptexport 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 */);}``` ## Static Site Generation with generateStaticParams ### Overview `generateStaticParams` is the App Router equivalent of `getStaticPaths` from the Pages Router. It generates static pages at build time for dynamic routes. ### Basic Usage ```typescript// app/blog/[id]/page.tsxexport 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:**- Returns an array of objects with route parameter keys- Each object represents one page to pre-render at build time- Function must be exported and named `generateStaticParams`- Works ONLY in Server Components (no `'use client'` directive)- Replaces Pages Router's `getStaticPaths` ### Fetching Data for Static Params ```typescript// app/blog/[slug]/page.tsxexport 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> );}``` ### Multiple Dynamic Segments ```typescript// app/products/[category]/[id]/page.tsxexport 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>;}``` ### Dynamic Behavior Configuration ```typescript// app/blog/[id]/page.tsxexport async function generateStaticParams() { return [{ id: '1' }, { id: '2' }];} // Control behavior for non-pre-rendered pathsexport 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-demand- `dynamicParams = false`: Non-pre-rendered paths return 404 ### Common Patterns **Pattern 1: Simple ID-based routes**```typescriptexport async function generateStaticParams() { return [ { id: '1' }, { id: '2' }, { id: '3' }, ];}``` **Pattern 2: Fetch from API**```typescriptexport 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**```typescriptexport async function generateStaticParams() { const posts = await db.post.findMany(); return posts.map(post => ({ slug: post.slug }));}``` ### Migration from Pages Router **Before (Pages Router):**```typescript// pages/blog/[id].tsxexport 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):**```typescript// app/blog/[id]/page.tsxexport 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>;}``` ### Common Mistakes to Avoid **❌ Wrong: Using `'use client'`**```typescript'use client'; // ERROR! generateStaticParams only works in Server Components export async function generateStaticParams() { return [{ id: '1' }];}``` **❌ Wrong: Using Pages Router pattern**```typescriptexport async function getStaticPaths() { // Wrong API! return { paths: [...], fallback: false };}``` **❌ Wrong: Missing export keyword**```typescriptasync function generateStaticParams() { // Must be exported! return [{ id: '1' }];}``` **✅ Correct: Clean Server Component**```typescript// 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`:- **DO** use the Edit or Write tool to modify the actual file- **DO** add the function to the existing page.tsx file- **DO** remove any TODO comments about generateStaticParams- **DON'T** just output code in markdown - actually implement it- **DON'T** show code without writing it to the file ## Testing and Validation When migrating or building with App Router, verify: 1. **Structure:** - `app/` directory exists - Root `layout.tsx` exists with `<html>` and `<body>` - Each route has a `page.tsx` file 2. **Metadata:** - No `next/head` imports in App Router - Metadata exported from pages or layouts - Metadata properly typed with `Metadata` type 3. **Navigation:** - Using `Link` component from `next/link` - Not using plain `<a>` tags for internal navigation 4. **Cleanup:** - No remaining page files in `pages/` directory - `_app.tsx` and `_document.tsx` removed - Old metadata patterns removed ## Quick Reference ### File Structure Mapping | 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 | ### Common Commands ```bash# Create new Next.js app with App Routernpx create-next-app@latest my-app # Run development servernpm run dev # Build for productionnpm run build # Start production servernpm start``` ## Additional Resources 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.Related skills
1password
Install 1password skill for Claude Code from steipete/clawdis.
3d Web Experience
Install 3d Web Experience skill for Claude Code from sickn33/antigravity-awesome-skills.
Ab Test Setup
This handles the full A/B testing workflow from hypothesis formation to statistical analysis. It walks you through proper test design, calculates sample sizes,