SKILLS LAUNCH PARTY

nextjs

SKILL.md

Next.js App Router - Production Patterns

Version: Next.js 16.0.0 React Version: 19.2.0 Node.js: 20.9+ Last Verified: 2025-10-24


Table of Contents

  1. When to Use This Skill
  2. When NOT to Use This Skill
  3. Next.js 16 Breaking Changes
  4. Cache Components & Caching APIs
  5. Server Components
  6. Server Actions
  7. Route Handlers
  8. Proxy vs Middleware
  9. Parallel Routes & Route Groups
  10. React 19.2 Features
  11. Metadata API
  12. Image & Font Optimization
  13. Performance Patterns
  14. TypeScript Configuration
  15. Common Errors & Solutions
  16. Templates Reference
  17. Additional Resources

When to Use This Skill

Use this skill when you need:

  • Next.js 16 App Router patterns (layouts, loading, error boundaries, routing)
  • Server Components best practices (data fetching, composition, streaming)
  • Server Actions patterns (forms, mutations, revalidation, error handling)
  • Cache Components with "use cache" directive (NEW in Next.js 16)
  • New caching APIs: revalidateTag(), updateTag(), refresh() (Updated in Next.js 16)
  • Migration from Next.js 15 to 16 (async params, proxy.ts, parallel routes)
  • Route Handlers (API endpoints, webhooks, streaming responses)
  • Proxy patterns (proxy.ts replaces middleware.ts in Next.js 16)
  • Async route params (params, searchParams, cookies(), headers() now async)
  • Parallel routes with default.js (breaking change in Next.js 16)
  • React 19.2 features (View Transitions, useEffectEvent(), React Compiler)
  • Metadata API (SEO, Open Graph, Twitter Cards, sitemaps)
  • Image optimization (next/image with updated defaults in Next.js 16)
  • Font optimization (next/font patterns)
  • Turbopack configuration (stable and default in Next.js 16)
  • Performance optimization (lazy loading, code splitting, PPR, ISR)
  • TypeScript configuration (strict mode, path aliases)

When NOT to Use This Skill

Do NOT use this skill for:

  • Cloudflare Workers deployment → Use cloudflare-nextjs skill instead
  • Pages Router patterns → This skill covers App Router ONLY (Pages Router is legacy)
  • Authentication libraries → Use clerk-auth, auth-js, or other auth-specific skills
  • Database integration → Use cloudflare-d1, drizzle-orm-d1, or database-specific skills
  • UI component libraries → Use tailwind-v4-shadcn skill for Tailwind + shadcn/ui
  • State management → Use zustand-state-management, tanstack-query skills
  • Form libraries → Use react-hook-form-zod skill
  • Vercel-specific features → Refer to Vercel platform documentation
  • Next.js Enterprise features (ISR, DPR) → Refer to Next.js Enterprise docs
  • Deployment configuration → Use platform-specific deployment skills

Relationship with Other Skills:

  • cloudflare-nextjs: For deploying Next.js to Cloudflare Workers (use BOTH skills together if deploying to Cloudflare)
  • tailwind-v4-shadcn: For Tailwind v4 + shadcn/ui setup (composable with this skill)
  • clerk-auth: For Clerk authentication in Next.js (composable with this skill)
  • auth-js: For Auth.js (NextAuth) integration (composable with this skill)

Next.js 16 Breaking Changes

IMPORTANT: Next.js 16 introduces multiple breaking changes. Read this section carefully if migrating from Next.js 15 or earlier.

1. Async Route Parameters (BREAKING)

Breaking Change: params, searchParams, cookies(), headers(), draftMode() are now async and must be awaited.

Before (Next.js 15):

// ❌ This no longer works in Next.js 16
export default function Page({ params, searchParams }: {
  params: { slug: string }
  searchParams: { query: string }
}) {
  const slug = params.slug // ❌ Error: params is a Promise
  const query = searchParams.query // ❌ Error: searchParams is a Promise
  return <div>{slug}</div>
}

After (Next.js 16):

// ✅ Correct: await params and searchParams
export default async function Page({ params, searchParams }: {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ query: string }>
}) {
  const { slug } = await params // ✅ Await the promise
  const { query } = await searchParams // ✅ Await the promise
  return <div>{slug}</div>
}

Applies to:

  • params in pages, layouts, route handlers
  • searchParams in pages
  • cookies() from next/headers
  • headers() from next/headers
  • draftMode() from next/headers

Migration:

// ❌ Before
import { cookies, headers } from 'next/headers'

export function MyComponent() {
  const cookieStore = cookies() // ❌ Sync access
  const headersList = headers() // ❌ Sync access
}

// ✅ After
import { cookies, headers } from 'next/headers'

export async function MyComponent() {
  const cookieStore = await cookies() // ✅ Async access
  const headersList = await headers() // ✅ Async access
}

Codemod: Run npx @next/codemod@canary upgrade latest to automatically migrate.

See Template: templates/app-router-async-params.tsx


2. Middleware → Proxy Migration (BREAKING)

Breaking Change: middleware.ts is deprecated in Next.js 16. Use proxy.ts instead.

Why the Change: proxy.ts makes the network boundary explicit by running on Node.js runtime (not Edge runtime). This provides better clarity between edge middleware and server-side proxies.

Migration Steps:

  1. Rename file: middleware.tsproxy.ts
  2. Rename function: middlewareproxy
  3. Update config: matcherconfig.matcher (same syntax)

Before (Next.js 15):

// middleware.ts ❌ Deprecated in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  response.headers.set('x-custom-header', 'value')
  return response
}

export const config = {
  matcher: '/api/:path*',
}

After (Next.js 16):

// proxy.ts ✅ New in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const response = NextResponse.next()
  response.headers.set('x-custom-header', 'value')
  return response
}

export const config = {
  matcher: '/api/:path*',
}

Note: middleware.ts still works in Next.js 16 but is deprecated. Migrate to proxy.ts for future compatibility.

See Template: templates/proxy-migration.ts See Reference: references/proxy-vs-middleware.md


3. Parallel Routes Require default.js (BREAKING)

Breaking Change: Parallel routes now require explicit default.js files. Without them, routes will fail during soft navigation.

Structure:

app/
├── @auth/
│   ├── login/
│   │   └── page.tsx
│   └── default.tsx    ← REQUIRED in Next.js 16
├── @dashboard/
│   ├── overview/
│   │   └── page.tsx
│   └── default.tsx    ← REQUIRED in Next.js 16
└── layout.tsx

Layout:

// app/layout.tsx
export default function Layout({
  children,
  auth,
  dashboard,
}: {
  children: React.ReactNode
  auth: React.ReactNode
  dashboard: React.ReactNode
}) {
  return (
    <html>
      <body>
        {auth}
        {dashboard}
        {children}
      </body>
    </html>
  )
}

Default Fallback (REQUIRED):

// app/@auth/default.tsx
export default function AuthDefault() {
  return null // or <Skeleton /> or redirect
}

// app/@dashboard/default.tsx
export default function DashboardDefault() {
  return null
}

Why Required: Next.js 16 changed how parallel routes handle soft navigation. Without default.js, unmatched slots will error during client-side navigation.

See Template: templates/parallel-routes-with-default.tsx


4. Removed Features (BREAKING)

The following features are REMOVED in Next.js 16:

  1. AMP Support - Entirely removed. Migrate to standard pages.
  2. next lint command - Use ESLint or Biome directly.
  3. serverRuntimeConfig and publicRuntimeConfig - Use environment variables instead.
  4. experimental.ppr flag - Evolved into Cache Components. Use "use cache" directive.
  5. Automatic scroll-behavior: smooth - Add manually if needed.
  6. Node.js 18 support - Minimum version is now 20.9+.

Migration:

  • AMP: Convert AMP pages to standard pages or use separate AMP implementation.
  • Linting: Run npx eslint . or npx biome lint . directly.
  • Config: Replace serverRuntimeConfig with process.env.VARIABLE.
  • PPR: Migrate from experimental.ppr to "use cache" directive (see Cache Components section).

5. Version Requirements (BREAKING)

Next.js 16 requires:

  • Node.js: 20.9+ (Node.js 18 no longer supported)
  • TypeScript: 5.1+ (if using TypeScript)
  • React: 19.2+ (automatically installed with Next.js 16)
  • Browsers: Chrome 111+, Safari 16.4+, Firefox 109+, Edge 111+

Check Versions:

node --version    # Should be 20.9+
npm --version     # Should be 10+
npx next --version # Should be 16.0.0+

Upgrade Node.js:

# Using nvm
nvm install 20
nvm use 20
nvm alias default 20

# Using Homebrew (macOS)
brew install node@20

# Using apt (Ubuntu/Debian)
sudo apt update
sudo apt install nodejs npm

6. Image Defaults Changed (BREAKING)

Next.js 16 changed next/image defaults:

Setting Next.js 15 Next.js 16
TTL (cache duration) 60 seconds 4 hours
imageSizes [16, 32, 48, 64, 96, 128, 256, 384] [640, 750, 828, 1080, 1200] (reduced)
qualities [75, 90, 100] [75] (single quality)

Impact:

  • Images cache longer (4 hours vs 60 seconds)
  • Fewer image sizes generated (smaller builds, but less granular)
  • Single quality (75) generated instead of multiple

Override Defaults (if needed):

// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  images: {
    minimumCacheTTL: 60, // Revert to 60 seconds
    deviceSizes: [640, 750, 828, 1080, 1200, 1920], // Add larger sizes
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Restore old sizes
    formats: ['image/webp'], // Default
  },
}

export default config

See Template: templates/image-optimization.tsx


Cache Components & Caching APIs

NEW in Next.js 16: Cache Components introduce opt-in caching with the "use cache" directive, replacing implicit caching from Next.js 15.

1. Overview

What Changed:

  • Next.js 15: Implicit caching (all Server Components cached by default)
  • Next.js 16: Opt-in caching with "use cache" directive

Why the Change: Explicit caching gives developers more control and makes caching behavior predictable.

Cache Components enable:

  • Component-level caching (cache specific components, not entire pages)
  • Function-level caching (cache expensive computations)
  • Page-level caching (cache entire pages selectively)
  • Partial Prerendering (PPR) - Cache static parts, render dynamic parts on-demand

2. "use cache" Directive

Syntax: Add "use cache" at the top of a Server Component, function, or route handler.

Component-level caching:

// app/components/expensive-component.tsx
'use cache'

export async function ExpensiveComponent() {
  const data = await fetch('https://api.example.com/data')
  const json = await data.json()

  return (
    <div>
      <h1>{json.title}</h1>
      <p>{json.description}</p>
    </div>
  )
}

Function-level caching:

// lib/data.ts
'use cache'

export async function getExpensiveData(id: string) {
  const response = await fetch(`https://api.example.com/items/${id}`)
  return response.json()
}

// Usage in component
import { getExpensiveData } from '@/lib/data'

export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const product = await getExpensiveData(id) // Cached

  return <div>{product.name}</div>
}

Page-level caching:

// app/blog/[slug]/page.tsx
'use cache'

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: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json())

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  )
}

See Template: templates/cache-component-use-cache.tsx


3. Partial Prerendering (PPR)

PPR allows caching static parts of a page while rendering dynamic parts on-demand.

Pattern:

// app/dashboard/page.tsx

// Static header (cached)
'use cache'
async function StaticHeader() {
  return <header>My App</header>
}

// Dynamic user info (not cached)
async function DynamicUserInfo() {
  const cookieStore = await cookies()
  const userId = cookieStore.get('userId')?.value
  const user = await fetch(`/api/users/${userId}`).then(r => r.json())

  return <div>Welcome, {user.name}</div>
}

// Page combines both
export default function Dashboard() {
  return (
    <div>
      <StaticHeader /> {/* Cached */}
      <DynamicUserInfo /> {/* Dynamic */}
    </div>
  )
}

When to Use PPR:

  • Page has both static and dynamic content
  • Want to cache layout/header/footer but render user-specific content
  • Need fast initial load (static parts) + personalization (dynamic parts)

See Reference: references/cache-components-guide.md


4. revalidateTag() - Updated API

BREAKING CHANGE: revalidateTag() now requires a second argument (cacheLife profile) for stale-while-revalidate behavior.

Before (Next.js 15):

import { revalidateTag } from 'next/cache'

export async function updatePost(id: string) {
  await fetch(`/api/posts/${id}`, { method: 'PATCH' })
  revalidateTag('posts') // ❌ Only one argument in Next.js 15
}

After (Next.js 16):

import { revalidateTag } from 'next/cache'

export async function updatePost(id: string) {
  await fetch(`/api/posts/${id}`, { method: 'PATCH' })
  revalidateTag('posts', 'max') // ✅ Second argument required in Next.js 16
}

Built-in Cache Life Profiles:

  • 'max' - Maximum staleness (recommended for most use cases)
  • 'hours' - Stale after hours
  • 'days' - Stale after days
  • 'weeks' - Stale after weeks
  • 'default' - Default cache behavior

Custom Cache Life Profile:

revalidateTag('posts', {
  stale: 3600, // Stale after 1 hour (seconds)
  revalidate: 86400, // Revalidate every 24 hours (seconds)
  expire: false, // Never expire (optional)
})

Pattern in Server Actions:

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify({ title, content }),
  })

  revalidateTag('posts', 'max') // ✅ Revalidate with max staleness
}

See Template: templates/revalidate-tag-cache-life.ts


5. updateTag() - NEW API (Server Actions Only)

NEW in Next.js 16: updateTag() provides read-your-writes semantics for Server Actions.

What it does:

  • Expires cache immediately
  • Refreshes data within the same request
  • Shows updated data right after mutation (no stale data)

Difference from revalidateTag():

  • revalidateTag(): Stale-while-revalidate (shows stale data, revalidates in background)
  • updateTag(): Immediate refresh (expires cache, fetches fresh data in same request)

Use Case: Forms, user settings, or any mutation where user expects immediate feedback.

Pattern:

'use server'

import { updateTag } from 'next/cache'

export async function updateUserProfile(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string

  // Update database
  await db.users.update({ name, email })

  // Immediately refresh cache (read-your-writes)
  updateTag('user-profile')

  // User sees updated data immediately (no stale data)
}

When to Use:

  • updateTag(): User settings, profile updates, critical mutations (immediate feedback)
  • revalidateTag(): Blog posts, product listings, non-critical updates (background revalidation)

See Template: templates/server-action-update-tag.ts


6. refresh() - NEW API (Server Actions Only)

NEW in Next.js 16: refresh() refreshes uncached data only (complements client-side router.refresh()).

When to Use:

  • Refresh dynamic data without affecting cached data
  • Complement router.refresh() on server side

Pattern:

'use server'

import { refresh } from 'next/cache'

export async function refreshDashboard() {
  // Refresh uncached data (e.g., real-time metrics)
  refresh()

  // Cached data (e.g., static header) remains cached
}

Difference from revalidateTag() and updateTag():

  • refresh(): Only refreshes uncached data
  • revalidateTag(): Revalidates specific tagged data (stale-while-revalidate)
  • updateTag(): Immediately expires and refreshes specific tagged data

See Reference: references/cache-components-guide.md


Server Components

Server Components are React components that render on the server. They enable efficient data fetching, reduce client bundle size, and improve performance.

1. Server Component Basics

Default Behavior: All components in the App Router are Server Components by default (unless marked with 'use client').

Example:

// app/posts/page.tsx (Server Component by default)
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return (
    <div>
      {posts.map((post: { id: string; title: string }) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

Rules:

  • ✅ Can await promises directly in component body
  • ✅ Can access cookies(), headers(), draftMode() (with await)
  • ✅ Can use Node.js APIs (fs, path, etc.)
  • ❌ Cannot use browser APIs (window, document, localStorage)
  • ❌ Cannot use React hooks (useState, useEffect, etc.)
  • ❌ Cannot use event handlers (onClick, onChange, etc.)

2. Data Fetching in Server Components

Pattern: Use async/await directly in component body.

// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params

  // Fetch data directly in component
  const product = await fetch(`https://api.example.com/products/${id}`)
    .then(r => r.json())

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
    </div>
  )
}

Parallel Data Fetching:

export default async function Dashboard() {
  // Fetch in parallel with Promise.all
  const [user, posts, comments] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
  ])

  return (
    <div>
      <UserInfo user={user} />
      <PostsList posts={posts} />
      <CommentsList comments={comments} />
    </div>
  )
}

Sequential Data Fetching (when needed):

export default async function UserPosts({ params }: { params: Promise<{ userId: string }> }) {
  const { userId } = await params

  // Fetch user first
  const user = await fetch(`/api/users/${userId}`).then(r => r.json())

  // Then fetch user's posts (depends on user data)
  const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json())

  return (
    <div>
      <h1>{user.name}'s Posts</h1>
      <ul>
        {posts.map((post: { id: string; title: string }) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

See Template: templates/server-component-streaming.tsx


3. Streaming with Suspense

Pattern: Wrap slow components in <Suspense> to stream content as it loads.

import { Suspense } from 'react'

// Fast component (loads immediately)
async function Header() {
  return <header>My App</header>
}

// Slow component (takes 2 seconds)
async function SlowData() {
  await new Promise(resolve => setTimeout(resolve, 2000))
  const data = await fetch('/api/slow-data').then(r => r.json())
  return <div>{data.content}</div>
}

// Page streams content
export default function Page() {
  return (
    <div>
      <Header /> {/* Loads immediately */}

      <Suspense fallback={<div>Loading...</div>}>
        <SlowData /> {/* Streams when ready */}
      </Suspense>
    </div>
  )
}

When to Use Streaming:

  • Page has slow API calls
  • Want to show UI immediately (don't wait for all data)
  • Improve perceived performance

See Reference: references/server-components-patterns.md


4. Server vs Client Components

When to Use Server Components (default):

  • Fetch data from APIs/databases
  • Access backend resources (files, environment variables)
  • Keep large dependencies on server (reduce bundle size)
  • Render static content

When to Use Client Components ('use client'):

  • Need React hooks (useState, useEffect, useContext)
  • Need event handlers (onClick, onChange, onSubmit)
  • Need browser APIs (window, localStorage, navigator)
  • Need third-party libraries that use browser APIs (charts, maps, etc.)

Pattern: Use Server Components by default, add 'use client' only when needed.

// app/components/interactive-button.tsx
'use client' // Client Component

import { useState } from 'react'

export function InteractiveButton() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  )
}

// app/page.tsx (Server Component)
import { InteractiveButton } from './components/interactive-button'

export default async function Page() {
  const data = await fetch('/api/data').then(r => r.json())

  return (
    <div>
      <h1>{data.title}</h1>
      <InteractiveButton /> {/* Client Component inside Server Component */}
    </div>
  )
}

Composition Rules:

  • ✅ Server Component can import Client Component
  • ✅ Client Component can import Client Component
  • ✅ Client Component can render Server Component as children (via props)
  • ❌ Client Component cannot import Server Component directly

See Reference: references/server-components-patterns.md


Server Actions

Server Actions are asynchronous functions that run on the server. They enable server-side mutations, form handling, and data revalidation.

1. Server Action Basics

Definition: Add 'use server' directive to create a Server Action.

File-level Server Actions:

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // Mutate database
  await db.posts.create({ title, content })

  // Revalidate cache
  revalidateTag('posts', 'max')
}

Inline Server Actions:

// app/posts/new/page.tsx
export default function NewPostPage() {
  async function createPost(formData: FormData) {
    'use server'

    const title = formData.get('title') as string
    const content = formData.get('content') as string

    await db.posts.create({ title, content })
    revalidateTag('posts', 'max')
  }

  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <button type="submit">Create Post</button>
    </form>
  )
}

See Template: templates/server-actions-form.tsx


2. Form Handling

Basic Form:

// app/components/create-post-form.tsx
import { createPost } from '@/app/actions'

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <label>
        Title:
        <input type="text" name="title" required />
      </label>

      <label>
        Content:
        <textarea name="content" required />
      </label>

      <button type="submit">Create Post</button>
    </form>
  )
}

With Loading State (useFormStatus):

'use client'

import { useFormStatus } from 'react-dom'
import { createPost } from '@/app/actions'

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create Post'}
    </button>
  )
}

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input type="text" name="title" required />
      <textarea name="content" required />
      <SubmitButton />
    </form>
  )
}

With Validation:

// app/actions.ts
'use server'

import { z } from 'zod'
import { redirect } from 'next/navigation'

const PostSchema = z.object({
  title: z.string().min(3, 'Title must be at least 3 characters'),
  content: z.string().min(10, 'Content must be at least 10 characters'),
})

export async function createPost(formData: FormData) {
  const rawData = {
    title: formData.get('title'),
    content: formData.get('content'),
  }

  // Validate
  const parsed = PostSchema.safeParse(rawData)

  if (!parsed.success) {
    return {
      errors: parsed.error.flatten().fieldErrors,
    }
  }

  // Mutate
  await db.posts.create(parsed.data)

  // Revalidate and redirect
  revalidateTag('posts', 'max')
  redirect('/posts')
}

See Template: templates/server-actions-form.tsx See Reference: references/server-actions-guide.md


3. Error Handling

Pattern: Return error objects from Server Actions, handle in Client Components.

Server Action:

// app/actions.ts
'use server'

export async function deletePost(id: string) {
  try {
    await db.posts.delete({ where: { id } })
    revalidateTag('posts', 'max')
    return { success: true }
  } catch (error) {
    return {
      success: false,
      error: 'Failed to delete post. Please try again.'
    }
  }
}

Client Component:

'use client'

import { useState } from 'react'
import { deletePost } from '@/app/actions'

export function DeleteButton({ postId }: { postId: string }) {
  const [error, setError] = useState<string | null>(null)

  async function handleDelete() {
    const result = await deletePost(postId)

    if (!result.success) {
      setError(result.error)
    }
  }

  return (
    <div>
      <button onClick={handleDelete}>Delete Post</button>
      {error && <p className="error">{error}</p>}
    </div>
  )
}

4. Optimistic Updates

Pattern: Show UI update immediately, then sync with server.

'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, amount: number) => state + amount
  )

  async function handleLike() {
    // Update UI immediately
    addOptimisticLike(1)

    // Sync with server
    await likePost(postId)
  }

  return (
    <button onClick={handleLike}>
      ❤️ {optimisticLikes} likes
    </button>
  )
}

See Reference: references/server-actions-guide.md


Route Handlers

Route Handlers are server-side API endpoints in the App Router. They replace API Routes from the Pages Router.

1. Basic Route Handler

File: app/api/hello/route.ts

import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({ message: 'Hello, World!' })
}

export async function POST(request: Request) {
  const body = await request.json()

  return NextResponse.json({
    message: 'Post created',
    data: body
  })
}

Supported Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS

See Template: templates/route-handler-api.ts


2. Dynamic Routes

File: app/api/posts/[id]/route.ts

import { NextResponse } from 'next/server'

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params // ✅ Await params in Next.js 16

  const post = await db.posts.findUnique({ where: { id } })

  if (!post) {
    return NextResponse.json(
      { error: 'Post not found' },
      { status: 404 }
    )
  }

  return NextResponse.json(post)
}

export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params

  await db.posts.delete({ where: { id } })

  return NextResponse.json({ message: 'Post deleted' })
}

3. Search Params

URL: /api/posts?tag=javascript&limit=10

import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const tag = searchParams.get('tag')
  const limit = parseInt(searchParams.get('limit') || '10')

  const posts = await db.posts.findMany({
    where: { tags: { has: tag } },
    take: limit,
  })

  return NextResponse.json(posts)
}

4. Webhooks

Pattern: Handle incoming webhooks from third-party services.

// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'

export async function POST(request: Request) {
  const body = await request.text()
  const headersList = await headers() // ✅ Await headers in Next.js 16
  const signature = headersList.get('stripe-signature')

  // Verify webhook signature
  const event = stripe.webhooks.constructEvent(
    body,
    signature!,
    process.env.STRIPE_WEBHOOK_SECRET!
  )

  // Handle event
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePaymentSuccess(event.data.object)
      break
    case 'payment_intent.failed':
      await handlePaymentFailure(event.data.object)
      break
  }

  return NextResponse.json({ received: true })
}

See Template: templates/route-handler-api.ts See Reference: references/route-handlers-reference.md


Proxy vs Middleware

Next.js 16 introduces proxy.ts to replace middleware.ts.

Why the Change?

  • middleware.ts: Runs on Edge runtime (limited Node.js APIs)
  • proxy.ts: Runs on Node.js runtime (full Node.js APIs)

The new proxy.ts makes the network boundary explicit and provides more flexibility.

Migration

Before (middleware.ts):

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Check auth
  const token = request.cookies.get('token')

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/dashboard/:path*',
}

After (proxy.ts):

// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // Check auth
  const token = request.cookies.get('token')

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/dashboard/:path*',
}

See Template: templates/proxy-migration.ts See Reference: references/proxy-vs-middleware.md


Parallel Routes & Route Groups

1. Parallel Routes

Use Case: Render multiple pages in the same layout (e.g., modal + main content, dashboard panels).

Structure:

app/
├── @modal/
│   ├── login/
│   │   └── page.tsx
│   └── default.tsx  ← REQUIRED in Next.js 16
├── @feed/
│   ├── trending/
│   │   └── page.tsx
│   └── default.tsx  ← REQUIRED in Next.js 16
└── layout.tsx

Layout:

// app/layout.tsx
export default function Layout({
  children,
  modal,
  feed,
}: {
  children: React.ReactNode
  modal: React.ReactNode
  feed: React.ReactNode
}) {
  return (
    <html>
      <body>
        {modal}
        <main>
          {children}
          <aside>{feed}</aside>
        </main>
      </body>
    </html>
  )
}

Default Files (REQUIRED):

// app/@modal/default.tsx
export default function ModalDefault() {
  return null
}

// app/@feed/default.tsx
export default function FeedDefault() {
  return <div>Default Feed</div>
}

See Template: templates/parallel-routes-with-default.tsx


2. Route Groups

Use Case: Organize routes without affecting URL structure.

Structure:

app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx       → /about
│   └── contact/
│       └── page.tsx       → /contact
├── (shop)/
│   ├── products/
│   │   └── page.tsx       → /products
│   └── cart/
│       └── page.tsx       → /cart
└── layout.tsx

Different Layouts per Group:

app/
├── (marketing)/
│   ├── layout.tsx          ← Marketing layout
│   └── about/page.tsx
├── (shop)/
│   ├── layout.tsx          ← Shop layout
│   └── products/page.tsx
└── layout.tsx              ← Root layout

See Reference: references/app-router-fundamentals.md


React 19.2 Features

Next.js 16 integrates React 19.2, which includes new features from React Canary.

1. View Transitions

Use Case: Smooth animations between page transitions.

'use client'

import { useRouter } from 'next/navigation'
import { startTransition } from 'react'

export function NavigationLink({ href, children }: { href: string; children: React.ReactNode }) {
  const router = useRouter()

  function handleClick(e: React.MouseEvent) {
    e.preventDefault()

    // Wrap navigation in startTransition for View Transitions
    startTransition(() => {
      router.push(href)
    })
  }

  return <a href={href} onClick={handleClick}>{children}</a>
}

With CSS View Transitions API:

/* app/globals.css */
@view-transition {
  navigation: auto;
}

/* Animate elements with view-transition-name */
.page-title {
  view-transition-name: page-title;
}

See Template: templates/view-transitions-react-19.tsx


2. useEffectEvent() (Experimental)

Use Case: Extract non-reactive logic from useEffect.

'use client'

import { useEffect, experimental_useEffectEvent as useEffectEvent } from 'react'

export function ChatRoom({ roomId }: { roomId: string }) {
  const onConnected = useEffectEvent(() => {
    console.log('Connected to room:', roomId)
  })

  useEffect(() => {
    const connection = connectToRoom(roomId)
    onConnected() // Non-reactive callback

    return () => connection.disconnect()
  }, [roomId]) // Only re-run when roomId changes

  return <div>Chat Room {roomId}</div>
}

Why Use It: Prevents unnecessary useEffect re-runs when callback dependencies change.


3. React Compiler (Stable)

Use Case: Automatic memoization without useMemo, useCallback.

Enable in next.config.ts:

import type { NextConfig } from 'next'

const config: NextConfig = {
  experimental: {
    reactCompiler: true,
  },
}

export default config

Install Plugin:

npm install babel-plugin-react-compiler

Example (no manual memoization needed):

'use client'

export function ExpensiveList({ items }: { items: string[] }) {
  // React Compiler automatically memoizes this
  const filteredItems = items.filter(item => item.length > 3)

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  )
}

See Reference: references/react-19-integration.md


Metadata API

The Metadata API provides type-safe SEO and social sharing metadata.

1. Static Metadata

Pattern: Export metadata object from page or layout.

// app/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Welcome to my app',
  openGraph: {
    title: 'My App',
    description: 'Welcome to my app',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'My App',
    description: 'Welcome to my app',
    images: ['/twitter-image.jpg'],
  },
}

export default function Page() {
  return <h1>Home</h1>
}

2. Dynamic Metadata

Pattern: Export generateMetadata async function.

// app/posts/[id]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
  const { id } = await params
  const post = await fetch(`/api/posts/${id}`).then(r => r.json())

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const post = await fetch(`/api/posts/${id}`).then(r => r.json())

  return <article>{post.content}</article>
}

3. Sitemap

File: app/sitemap.ts

import type { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await fetch('/api/posts').then(r => r.json())

  const postUrls = posts.map((post: { id: string; updatedAt: string }) => ({
    url: `https://example.com/posts/${post.id}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }))

  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    ...postUrls,
  ]
}

See Template: templates/metadata-config.ts See Reference: references/metadata-api-guide.md


Image & Font Optimization

1. next/image

Basic Usage:

import Image from 'next/image'

export function MyImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero image"
      width={1200}
      height={600}
      priority // Load above the fold
    />
  )
}

Responsive Images:

<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  style={{ objectFit: 'cover' }}
  sizes="(max-width: 768px) 100vw, 50vw"
/>

Remote Images (configure in next.config.ts):

import type { NextConfig } from 'next'

const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
        pathname: '/images/**',
      },
    ],
  },
}

export default config

See Template: templates/image-optimization.tsx


2. next/font

Google Fonts:

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

Local Fonts:

import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/my-font.woff2',
  display: 'swap',
  variable: '--font-my-font',
})

See Template: templates/font-optimization.tsx


Performance Patterns

1. Lazy Loading

Component Lazy Loading:

import dynamic from 'next/dynamic'

const HeavyComponent = dynamic(() => import('./heavy-component'), {
  loading: () => <div>Loading...</div>,
  ssr: false, // Disable SSR for client-only components
})

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <HeavyComponent />
    </div>
  )
}

Conditional Loading:

'use client'

import { useState } from 'react'
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('./chart'), { ssr: false })

export function Dashboard() {
  const [showChart, setShowChart] = useState(false)

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {showChart && <Chart />}
    </div>
  )
}

2. Code Splitting

Route-level Splitting (automatic):

app/
├── page.tsx       → Bundles: /, shared
├── about/
│   └── page.tsx   → Bundles: /about, shared
└── products/
    └── page.tsx   → Bundles: /products, shared

Component-level Splitting (with dynamic import):

const Analytics = dynamic(() => import('./analytics'))
const Comments = dynamic(() => import('./comments'))

export default function BlogPost() {
  return (
    <article>
      <h1>Post Title</h1>
      <p>Content...</p>
      <Analytics /> {/* Separate bundle */}
      <Comments />  {/* Separate bundle */}
    </article>
  )
}

3. Turbopack (Stable in Next.js 16)

Default: Turbopack is now the default bundler in Next.js 16.

Metrics:

  • 2–5× faster production builds
  • Up to 10× faster Fast Refresh

Opt-out (if needed):

npm run build -- --webpack

Enable File System Caching (beta):

// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  experimental: {
    turbopack: {
      fileSystemCaching: true, // Beta: Persist cache between runs
    },
  },
}

export default config

See Reference: references/performance-optimization.md


TypeScript Configuration

1. Strict Mode

Enable strict mode in tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

2. Path Aliases

Configure in tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./app/*"],
      "@/components/*": ["./app/components/*"],
      "@/lib/*": ["./lib/*"],
      "@/styles/*": ["./styles/*"]
    }
  }
}

Usage:

// Instead of: import { Button } from '../../../components/button'
import { Button } from '@/components/button'

3. Type-Safe Routing

Generate types from routes:

npm run build

Usage:

import { useRouter } from 'next/navigation'

const router = useRouter()

// Type-safe routing
router.push('/posts/123') // ✅ Valid route
router.push('/invalid')   // ❌ Type error if route doesn't exist

See Reference: references/typescript-configuration.md


Common Errors & Solutions

1. Error: params is a Promise

Error:

Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }'

Cause: Next.js 16 changed params to async.

Solution: Await params:

// ❌ Before
export default function Page({ params }: { params: { id: string } }) {
  const id = params.id
}

// ✅ After
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
}

2. Error: searchParams is a Promise

Error:

Property 'query' does not exist on type 'Promise<{ query: string }>'

Cause: searchParams is now async in Next.js 16.

Solution:

// ❌ Before
export default function Page({ searchParams }: { searchParams: { query: string } }) {
  const query = searchParams.query
}

// ✅ After
export default async function Page({ searchParams }: { searchParams: Promise<{ query: string }> }) {
  const { query } = await searchParams
}

3. Error: cookies() requires await

Error:

'cookies' implicitly has return type 'any'

Cause: cookies() is now async in Next.js 16.

Solution:

// ❌ Before
import { cookies } from 'next/headers'

export function MyComponent() {
  const cookieStore = cookies()
}

// ✅ After
import { cookies } from 'next/headers'

export async function MyComponent() {
  const cookieStore = await cookies()
}

4. Error: Parallel route missing default.js

Error:

Error: Parallel route @modal/login was matched but no default.js was found

Cause: Next.js 16 requires default.js for all parallel routes.

Solution: Add default.tsx files:

// app/@modal/default.tsx
export default function ModalDefault() {
  return null
}

5. Error: revalidateTag() requires 2 arguments

Error:

Expected 2 arguments, but got 1

Cause: revalidateTag() now requires a cacheLife argument in Next.js 16.

Solution:

// ❌ Before
revalidateTag('posts')

// ✅ After
revalidateTag('posts', 'max')

6. Error: Cannot use React hooks in Server Component

Error:

You're importing a component that needs useState. It only works in a Client Component

Cause: Using React hooks in Server Component.

Solution: Add 'use client' directive:

// ✅ Add 'use client' at the top
'use client'

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

7. Error: middleware.ts is deprecated

Warning:

Warning: middleware.ts is deprecated. Use proxy.ts instead.

Solution: Migrate to proxy.ts:

// Rename: middleware.ts → proxy.ts
// Rename function: middleware → proxy

export function proxy(request: NextRequest) {
  // Same logic
}

8. Error: Turbopack build failure

Error:

Error: Failed to compile with Turbopack

Cause: Turbopack is now default in Next.js 16.

Solution: Opt out of Turbopack if incompatible:

npm run build -- --webpack

9. Error: Invalid next/image src

Error:

Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured under images in your `next.config.js`

Solution: Add remote patterns in next.config.ts:

const config: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
      },
    ],
  },
}

10. Error: Cannot import Server Component into Client Component

Error:

You're importing a Server Component into a Client Component

Solution: Pass Server Component as children:

// ❌ Wrong
'use client'
import { ServerComponent } from './server-component' // Error

export function ClientComponent() {
  return <ServerComponent />
}

// ✅ Correct
'use client'

export function ClientComponent({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

// Usage
<ClientComponent>
  <ServerComponent /> {/* Pass as children */}
</ClientComponent>

11. Error: generateStaticParams not working

Cause: generateStaticParams only works with static generation (export const dynamic = 'force-static').

Solution:

export const dynamic = 'force-static'

export async function generateStaticParams() {
  const posts = await fetch('/api/posts').then(r => r.json())
  return posts.map((post: { id: string }) => ({ id: post.id }))
}

12. Error: fetch() not caching

Cause: Next.js 16 uses opt-in caching with "use cache" directive.

Solution: Add "use cache" to component or function:

'use cache'

export async function getPosts() {
  const response = await fetch('/api/posts')
  return response.json()
}

13. Error: Route collision with Route Groups

Error:

Error: Conflicting routes: /about and /(marketing)/about

Cause: Route groups create same URL path.

Solution: Ensure route groups don't conflict:

app/
├── (marketing)/about/page.tsx  → /about
└── (shop)/about/page.tsx       → ERROR: Duplicate /about

# Fix: Use different routes
app/
├── (marketing)/about/page.tsx     → /about
└── (shop)/store-info/page.tsx     → /store-info

14. Error: Metadata not updating

Cause: Using dynamic metadata without generateMetadata().

Solution: Use generateMetadata() for dynamic pages:

export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
  const { id } = await params
  const post = await fetch(`/api/posts/${id}`).then(r => r.json())

  return {
    title: post.title,
    description: post.excerpt,
  }
}

15. Error: next/font font not loading

Cause: Font variable not applied to HTML element.

Solution: Apply font variable to <html> or <body>:

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html className={inter.variable}> {/* ✅ Apply variable */}
      <body>{children}</body>
    </html>
  )
}

16. Error: Environment variables not available in browser

Cause: Server-only env vars are not exposed to browser.

Solution: Prefix with NEXT_PUBLIC_ for client-side access:

# .env
SECRET_KEY=abc123                  # Server-only
NEXT_PUBLIC_API_URL=https://api    # Available in browser
// Server Component (both work)
const secret = process.env.SECRET_KEY
const apiUrl = process.env.NEXT_PUBLIC_API_URL

// Client Component (only public vars work)
const apiUrl = process.env.NEXT_PUBLIC_API_URL

17. Error: Server Action not found

Error:

Error: Could not find Server Action

Cause: Missing 'use server' directive.

Solution: Add 'use server':

// ❌ Before
export async function createPost(formData: FormData) {
  await db.posts.create({ ... })
}

// ✅ After
'use server'

export async function createPost(formData: FormData) {
  await db.posts.create({ ... })
}

18. Error: TypeScript path alias not working

Cause: Incorrect baseUrl or paths in tsconfig.json.

Solution: Configure correctly:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"],
      "@/components/*": ["./app/components/*"]
    }
  }
}

See Reference: references/top-errors.md


Templates Reference

The following templates are available in templates/:

App Router Fundamentals:

  • app-router-async-params.tsx - Async params, searchParams patterns (Next.js 16)
  • parallel-routes-with-default.tsx - Parallel routes with required default.js
  • route-groups-example.tsx - Route groups organization

Cache Components (Next.js 16):

  • cache-component-use-cache.tsx - Cache Components with "use cache"
  • partial-prerendering.tsx - PPR with static + dynamic parts
  • revalidate-tag-cache-life.ts - Updated revalidateTag() API
  • server-action-update-tag.ts - updateTag() for read-your-writes

Server Components:

  • server-component-data-fetching.tsx - Data fetching patterns
  • server-component-streaming.tsx - Streaming with Suspense
  • server-component-composition.tsx - Server + Client component composition

Server Actions:

  • server-actions-form.tsx - Form handling with Server Actions
  • server-actions-validation.ts - Server Action validation with Zod
  • server-actions-optimistic.tsx - Optimistic updates

Route Handlers:

  • route-handler-api.ts - Basic CRUD API
  • route-handler-webhook.ts - Webhook handling
  • route-handler-streaming.ts - Streaming responses

Proxy & Middleware:

  • proxy-migration.ts - Migrate from middleware.ts to proxy.ts
  • proxy-auth.ts - Auth with proxy.ts

React 19.2:

  • view-transitions-react-19.tsx - View Transitions API
  • use-effect-event.tsx - useEffectEvent() pattern
  • react-compiler-example.tsx - React Compiler usage

Metadata:

  • metadata-config.ts - Static and dynamic metadata
  • sitemap.ts - Sitemap generation
  • robots.ts - robots.txt generation

Optimization:

  • image-optimization.tsx - next/image patterns
  • font-optimization.tsx - next/font patterns
  • lazy-loading.tsx - Dynamic imports and lazy loading
  • code-splitting.tsx - Code splitting strategies

TypeScript:

  • typescript-config.json - Recommended TypeScript configuration
  • path-aliases.tsx - Path alias usage

Configuration:

  • next.config.ts - Full Next.js configuration
  • package.json - Recommended dependencies for Next.js 16

Additional Resources

Bundled References (in references/):

  • next-16-migration-guide.md - Complete migration from Next.js 15 to 16
  • cache-components-guide.md - Cache Components deep dive
  • proxy-vs-middleware.md - Proxy.ts vs middleware.ts comparison
  • async-route-params.md - Async params, searchParams, cookies(), headers()
  • app-router-fundamentals.md - App Router concepts and patterns
  • server-components-patterns.md - Server Components best practices
  • server-actions-guide.md - Server Actions patterns and validation
  • route-handlers-reference.md - Route Handlers API reference
  • metadata-api-guide.md - Metadata API complete guide
  • performance-optimization.md - Performance patterns and Turbopack
  • react-19-integration.md - React 19.2 features in Next.js
  • top-errors.md - 18+ common errors and solutions

Scripts:

  • scripts/check-versions.sh - Verify Next.js and dependency versions

External Documentation:


Version Compatibility

Package Minimum Version Recommended
Next.js 16.0.0 16.0.0+
React 19.2.0 19.2.0+
Node.js 20.9.0 20.9.0+
TypeScript 5.1.0 5.7.0+
Turbopack (built-in) Stable

Check Versions:

./scripts/check-versions.sh

Token Efficiency

Estimated Token Savings: 65-70%

Without Skill (manual setup from docs):

  • Read Next.js 16 migration guide: ~5k tokens
  • Read App Router docs: ~8k tokens
  • Read Server Actions docs: ~4k tokens
  • Read Metadata API docs: ~3k tokens
  • Trial-and-error fixes: ~8k tokens
  • Total: ~28k tokens

With Skill:

  • Load skill: ~8k tokens
  • Use templates: ~2k tokens
  • Total: ~10k tokens
  • Savings: ~18k tokens (~64%)

Errors Prevented: 18+ common mistakes = 100% error prevention


Maintenance

Last Verified: 2025-10-24 Next Review: 2026-01-24 (Quarterly) Maintainer: Jezweb | jeremy@jezweb.net Repository: https://github.com/jezweb/claude-skills

Update Triggers:

  • Next.js major/minor releases
  • React major releases
  • Breaking changes in APIs
  • New Turbopack features

Version Check:

cd skills/nextjs
./scripts/check-versions.sh

End of SKILL.md

Weekly Installs
10
First Seen
Jan 24, 2026
Installed on
claude-code7
gemini-cli6
antigravity6
cursor5
opencode5
trae4