Files
2025-11-30 08:25:04 +08:00

34 KiB
Raw Permalink Blame History

name, description, license, metadata, allowed-tools
name description license metadata allowed-tools
nextjs Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components ("use cache"), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), "use cache" directives, parallel route 404s (missing default.js), or proxy.ts CORS. MIT
version last_verified nextjs_version react_version node_version author repository production_tested token_savings errors_prevented
1.0.0 2025-10-24 16.0.0 19.2.0 20.9+ Jezweb https://github.com/jezweb/claude-skills true 65-70% 18+
Read
Write
Edit
Bash
Glob
Grep

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. Route Handlers (Next.js 16 Updates)
  6. Proxy vs Middleware
  7. Parallel Routes - default.js Required
  8. React 19.2 Features
  9. Turbopack (Stable in Next.js 16)
  10. Common Errors & Solutions
  11. Templates & Resources

When to Use This Skill

Focus: Next.js 16 breaking changes and knowledge gaps (December 2024+).

Use this skill when you need:

  • Next.js 16 breaking changes (async params, proxy.ts, parallel routes default.js, removed features)
  • 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 (avoid breaking change errors)
  • Async route params (params, searchParams, cookies(), headers() now async)
  • Parallel routes with default.js (REQUIRED in Next.js 16)
  • React 19.2 features (View Transitions, useEffectEvent(), React Compiler)
  • Turbopack (stable and default in Next.js 16)
  • Image defaults changed (TTL, sizes, qualities in Next.js 16)
  • Error prevention (18+ documented Next.js 16 errors with solutions)

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, better-auth, 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)
  • better-auth: For Better Auth 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



Route Handlers (Next.js 16 Updates)

Async Params in Route Handlers (BREAKING)

IMPORTANT: params and headers() are now async in Next.js 16 route handlers.

Example:

// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'

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

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

  return NextResponse.json(post)
}

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


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 - default.js Required (BREAKING)

Breaking Change in Next.js 16: Parallel routes now require explicit default.js files.

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

Default Files (REQUIRED):

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

Why Required: Next.js 16 changed soft navigation handling. Without default.js, unmatched slots error during client-side navigation.

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


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


Turbopack (Stable in Next.js 16)

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

Performance Improvements:

  • 25× faster production builds
  • Up to 10× faster Fast Refresh

Opt-out (if needed):

npm run build -- --webpack

Enable File System Caching (experimental):

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

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

export default config

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 & Resources

Next.js 16-Specific Templates (in templates/):

  • app-router-async-params.tsx - Async params migration patterns
  • parallel-routes-with-default.tsx - Required default.js files
  • cache-component-use-cache.tsx - Cache Components with "use cache"
  • revalidate-tag-cache-life.ts - Updated revalidateTag() with cacheLife
  • server-action-update-tag.ts - updateTag() for read-your-writes
  • proxy-migration.ts - Migrate from middleware.ts to proxy.ts
  • view-transitions-react-19.tsx - React 19.2 View Transitions
  • next.config.ts - Next.js 16 configuration

Bundled References (in references/):

  • next-16-migration-guide.md - Complete Next.js 15→16 migration guide
  • cache-components-guide.md - Cache Components deep dive
  • proxy-vs-middleware.md - Proxy.ts vs middleware.ts
  • async-route-params.md - Async params breaking change details
  • react-19-integration.md - React 19.2 features in Next.js 16
  • top-errors.md - 18+ common errors with solutions

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