commit 331f88275660480bed38c1872e5bd4a2eba16ec8 Author: Zhongwei Li Date: Sun Nov 30 08:25:04 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..997cc05 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "nextjs", + "description": "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.", + "version": "1.0.0", + "author": { + "name": "Jeremy Dawes", + "email": "jeremy@jezweb.net" + }, + "skills": [ + "./" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ecbba1 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 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. diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..d6a7861 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,1383 @@ +--- +name: nextjs +description: | + 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. +license: MIT +metadata: + version: 1.0.0 + last_verified: 2025-10-24 + nextjs_version: 16.0.0 + react_version: 19.2.0 + node_version: 20.9+ + author: Jezweb + repository: https://github.com/jezweb/claude-skills + production_tested: true + token_savings: 65-70% + errors_prevented: 18+ +allowed-tools: ["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](#when-to-use-this-skill) +2. [When NOT to Use This Skill](#when-not-to-use-this-skill) +3. [Next.js 16 Breaking Changes](#nextjs-16-breaking-changes) +4. [Cache Components & Caching APIs](#cache-components--caching-apis) +5. [Route Handlers (Next.js 16 Updates)](#route-handlers-nextjs-16-updates) +6. [Proxy vs Middleware](#proxy-vs-middleware) +7. [Parallel Routes - default.js Required](#parallel-routes---defaultjs-required-breaking) +8. [React 19.2 Features](#react-192-features) +9. [Turbopack (Stable in Next.js 16)](#turbopack-stable-in-nextjs-16) +10. [Common Errors & Solutions](#common-errors--solutions) +11. [Templates & Resources](#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)**: +```typescript +// ❌ 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
{slug}
+} +``` + +**After (Next.js 16)**: +```typescript +// ✅ 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
{slug}
+} +``` + +**Applies to**: +- `params` in pages, layouts, route handlers +- `searchParams` in pages +- `cookies()` from `next/headers` +- `headers()` from `next/headers` +- `draftMode()` from `next/headers` + +**Migration**: +```typescript +// ❌ 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.ts` → `proxy.ts` +2. **Rename function**: `middleware` → `proxy` +3. **Update config**: `matcher` → `config.matcher` (same syntax) + +**Before (Next.js 15)**: +```typescript +// 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)**: +```typescript +// 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**: +```typescript +// app/layout.tsx +export default function Layout({ + children, + auth, + dashboard, +}: { + children: React.ReactNode + auth: React.ReactNode + dashboard: React.ReactNode +}) { + return ( + + + {auth} + {dashboard} + {children} + + + ) +} +``` + +**Default Fallback** (REQUIRED): +```typescript +// app/@auth/default.tsx +export default function AuthDefault() { + return null // or 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**: +```bash +node --version # Should be 20.9+ +npm --version # Should be 10+ +npx next --version # Should be 16.0.0+ +``` + +**Upgrade Node.js**: +```bash +# 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): +```typescript +// 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**: +```typescript +// 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 ( +
+

{json.title}

+

{json.description}

+
+ ) +} +``` + +**Function-level caching**: +```typescript +// 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
{product.name}
+} +``` + +**Page-level caching**: +```typescript +// 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 ( +
+

{post.title}

+
{post.content}
+
+ ) +} +``` + +**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**: +```typescript +// app/dashboard/page.tsx + +// Static header (cached) +'use cache' +async function StaticHeader() { + return
My App
+} + +// 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
Welcome, {user.name}
+} + +// Page combines both +export default function Dashboard() { + return ( +
+ {/* Cached */} + {/* Dynamic */} +
+ ) +} +``` + +**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)**: +```typescript +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)**: +```typescript +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**: +```typescript +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**: +```typescript +'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**: +```typescript +'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**: +```typescript +'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**: +```typescript +// 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)**: +```typescript +// 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)**: +```typescript +// 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)**: +```typescript +// app/@modal/default.tsx +export default function ModalDefault() { + return null // or 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. + +```typescript +'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 {children} +} +``` + +**With CSS View Transitions API**: +```css +/* 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`. + +```typescript +'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
Chat Room {roomId}
+} +``` + +**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**: +```typescript +import type { NextConfig } from 'next' + +const config: NextConfig = { + experimental: { + reactCompiler: true, + }, +} + +export default config +``` + +**Install Plugin**: +```bash +npm install babel-plugin-react-compiler +``` + +**Example** (no manual memoization needed): +```typescript +'use client' + +export function ExpensiveList({ items }: { items: string[] }) { + // React Compiler automatically memoizes this + const filteredItems = items.filter(item => item.length > 3) + + return ( +
    + {filteredItems.map(item => ( +
  • {item}
  • + ))} +
+ ) +} +``` + +**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**: +- 2–5× faster production builds +- Up to 10× faster Fast Refresh + +**Opt-out** (if needed): +```bash +npm run build -- --webpack +``` + +**Enable File System Caching** (experimental): +```typescript +// 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`: +```typescript +// ❌ 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**: +```typescript +// ❌ 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**: +```typescript +// ❌ 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: +```typescript +// 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**: +```typescript +// ❌ 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: +```typescript +// ✅ Add 'use client' at the top +'use client' + +import { useState } from 'react' + +export function Counter() { + const [count, setCount] = useState(0) + return +} +``` + +--- + +### 7. Error: `middleware.ts` is deprecated + +**Warning**: +``` +Warning: middleware.ts is deprecated. Use proxy.ts instead. +``` + +**Solution**: Migrate to `proxy.ts`: +```typescript +// 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: +```bash +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`: +```typescript +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: +```typescript +// ❌ Wrong +'use client' +import { ServerComponent } from './server-component' // Error + +export function ClientComponent() { + return +} + +// ✅ Correct +'use client' + +export function ClientComponent({ children }: { children: React.ReactNode }) { + return
{children}
+} + +// Usage + + {/* Pass as children */} + +``` + +--- + +### 11. Error: `generateStaticParams` not working + +**Cause**: `generateStaticParams` only works with static generation (`export const dynamic = 'force-static'`). + +**Solution**: +```typescript +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: +```typescript +'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: +```typescript +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { + 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 `` or ``: +```typescript +import { Inter } from 'next/font/google' + +const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + {/* ✅ Apply variable */} + {children} + + ) +} +``` + +--- + +### 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: +```bash +# .env +SECRET_KEY=abc123 # Server-only +NEXT_PUBLIC_API_URL=https://api # Available in browser +``` + +```typescript +// 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'`: +```typescript +// ❌ 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: +```json +{ + "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**: +- **Next.js 16 Blog**: https://nextjs.org/blog/next-16 +- **Next.js Docs**: https://nextjs.org/docs +- **Context7 MCP**: `/websites/nextjs` for latest reference + +--- + +## 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**: +```bash +./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**: +```bash +cd skills/nextjs +./scripts/check-versions.sh +``` + +--- + +**End of SKILL.md** diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..b30c7af --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,85 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jezweb/claude-skills:skills/nextjs", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "7752a82e83393fe7be5bce05d0911b6498c93050", + "treeHash": "751d0e9f13f37b25b60eb7bd37981fed86ad120f2e150e053f240a521b9b9e85", + "generatedAt": "2025-11-28T10:18:59.854318Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "nextjs", + "description": "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.", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "291f3e1170756b73e817589cfd801423a4db65b30f736b90173f1b0464d3c688" + }, + { + "path": "SKILL.md", + "sha256": "0d274baa9085ed5ded3c5fca56c8498352197345eb18136d125e05e1ecee9164" + }, + { + "path": "references/next-16-migration-guide.md", + "sha256": "b13cb9e7d265f5eeb49f4d25bbc3db9f4afaec519adfb3661a94e811fd41d4a7" + }, + { + "path": "references/top-errors.md", + "sha256": "fbc231577f65325a341578a53faa59bcefb0158ab39053d02b8245a09dd7ab05" + }, + { + "path": "scripts/check-versions.sh", + "sha256": "81b37d00e6f3b91f2b9e411dd85f5f9681c95957eb951ed8d9da9da5914f5a70" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "c1c2ed4e43352267df00c2ba4037ec218053613a8bb4a8eb93130c2ad1bf6f8b" + }, + { + "path": "templates/server-actions-form.tsx", + "sha256": "8b13071ca8c839e1e7a37792686a25853f43ecfdbb17cf4534c4489049ee57cb" + }, + { + "path": "templates/proxy-migration.ts", + "sha256": "362c27846d847e2ebd01692e980f1b3c0b4bb4e1cff8f1fd98e6979ee59dbb3c" + }, + { + "path": "templates/route-handler-api.ts", + "sha256": "be6533d939727add7e97aaebf6b91eed846a8c343b93f9a6b27c44019811036d" + }, + { + "path": "templates/package.json", + "sha256": "298748ae5473db8d20cae62bba914fd6878f983cd2ebf0c55f6ae3a50b264853" + }, + { + "path": "templates/app-router-async-params.tsx", + "sha256": "7630604e4d4d11e83a36b2337f833c286ef1381ca5217076a15db8ec32a856c5" + }, + { + "path": "templates/cache-component-use-cache.tsx", + "sha256": "986256a04ebce7de7c624a5c5d0f5fd84fc2a590cc5600dfff18a97bae8eabae" + }, + { + "path": "templates/parallel-routes-with-default.tsx", + "sha256": "513cac57ae53e33fd619cf15876320264fe93d366727426bef09549ceae2e541" + } + ], + "dirSha256": "751d0e9f13f37b25b60eb7bd37981fed86ad120f2e150e053f240a521b9b9e85" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/references/next-16-migration-guide.md b/references/next-16-migration-guide.md new file mode 100644 index 0000000..6454bbf --- /dev/null +++ b/references/next-16-migration-guide.md @@ -0,0 +1,647 @@ +# Next.js 16 Migration Guide + +**From**: Next.js 15.x +**To**: Next.js 16.0.0 +**Last Updated**: 2025-10-24 + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Breaking Changes](#breaking-changes) +3. [New Features](#new-features) +4. [Migration Steps](#migration-steps) +5. [Automated Migration](#automated-migration) +6. [Manual Migration](#manual-migration) +7. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +Next.js 16 introduces significant changes: + +- **Breaking Changes**: 6 major breaking changes +- **New Features**: Cache Components, updated caching APIs, React 19.2 +- **Performance**: Turbopack stable, 2–5× faster builds +- **Migration Time**: ~1-2 hours for medium-sized apps + +**Recommendation**: Use automated codemod first, then manually fix remaining issues. + +--- + +## Breaking Changes + +### 1. Async Route Parameters ⚠️ + +**What Changed**: `params`, `searchParams`, `cookies()`, `headers()`, `draftMode()` are now async. + +**Before** (Next.js 15): +```typescript +export default function Page({ params, searchParams }) { + const slug = params.slug + const query = searchParams.q +} +``` + +**After** (Next.js 16): +```typescript +export default async function Page({ params, searchParams }) { + const { slug } = await params + const { q: query } = await searchParams +} +``` + +**TypeScript Types**: +```typescript +// Before +type PageProps = { + params: { slug: string } + searchParams: { q: string } +} + +// After +type PageProps = { + params: Promise<{ slug: string }> + searchParams: Promise<{ q: string }> +} +``` + +**Fix**: +1. Add `async` to function +2. Add `await` before params/searchParams +3. Update TypeScript types to `Promise<>` + +--- + +### 2. Middleware → Proxy ⚠️ + +**What Changed**: `middleware.ts` is deprecated. Use `proxy.ts` instead. + +**Migration**: +```bash +# 1. Rename file +mv middleware.ts proxy.ts + +# 2. Rename function in file +# middleware → proxy +``` + +**Before** (middleware.ts): +```typescript +export function middleware(request: NextRequest) { + return NextResponse.next() +} +``` + +**After** (proxy.ts): +```typescript +export function proxy(request: NextRequest) { + return NextResponse.next() +} +``` + +**Note**: `middleware.ts` still works in Next.js 16 but is deprecated. + +--- + +### 3. Parallel Routes Require `default.js` ⚠️ + +**What Changed**: All parallel routes now REQUIRE explicit `default.js` files. + +**Before** (Next.js 15): +``` +app/ +├── @modal/ +│ └── login/ +│ └── page.tsx +``` + +**After** (Next.js 16): +``` +app/ +├── @modal/ +│ ├── login/ +│ │ └── page.tsx +│ └── default.tsx ← REQUIRED +``` + +**default.tsx**: +```typescript +export default function ModalDefault() { + return null +} +``` + +**Fix**: Add `default.tsx` to every `@folder` in parallel routes. + +--- + +### 4. Removed Features ⚠️ + +**Removed**: +- AMP support +- `next lint` command +- `serverRuntimeConfig` and `publicRuntimeConfig` +- `experimental.ppr` flag +- Automatic `scroll-behavior: smooth` +- Node.js 18 support + +**Migration**: + +**AMP**: +```typescript +// Before +export const config = { amp: true } + +// After +// No direct replacement - use separate AMP pages or frameworks +``` + +**Linting**: +```bash +# Before +npm run lint + +# After +npx eslint . +# or +npx biome lint . +``` + +**Runtime Config**: +```typescript +// Before +module.exports = { + serverRuntimeConfig: { secret: 'abc' }, + publicRuntimeConfig: { apiUrl: 'https://api' }, +} + +// After +// Use environment variables +process.env.SECRET +process.env.NEXT_PUBLIC_API_URL +``` + +--- + +### 5. Version Requirements ⚠️ + +**Minimum Versions**: +- Node.js: 20.9+ (Node 18 removed) +- TypeScript: 5.1+ +- React: 19.2+ +- Browsers: Chrome 111+, Safari 16.4+, Firefox 109+ + +**Upgrade Node.js**: +```bash +# Check current version +node --version + +# Upgrade (using nvm) +nvm install 20 +nvm use 20 +nvm alias default 20 + +# Verify +node --version # Should be 20.9+ +``` + +--- + +### 6. Image Defaults Changed ⚠️ + +**What Changed**: `next/image` default settings changed. + +| Setting | Next.js 15 | Next.js 16 | +|---------|------------|------------| +| TTL | 60s | 4 hours | +| imageSizes | 8 sizes | 5 sizes | +| qualities | 3 qualities | 1 quality (75) | + +**Impact**: Images cache longer, fewer sizes generated. + +**Revert** (if needed): +```typescript +// next.config.ts +const config = { + images: { + minimumCacheTTL: 60, // Revert to 60 seconds + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Old sizes + }, +} +``` + +--- + +## New Features + +### 1. Cache Components ✨ + +**Opt-in caching** with `"use cache"` directive. + +**Before** (Next.js 15 - implicit caching): +```typescript +// All Server Components cached by default +export async function MyComponent() { + const data = await fetch('/api/data') + return
{data.value}
+} +``` + +**After** (Next.js 16 - opt-in): +```typescript +// NOT cached by default +export async function MyComponent() { + const data = await fetch('/api/data') + return
{data.value}
+} + +// Opt-in to caching +'use cache' +export async function CachedComponent() { + const data = await fetch('/api/data') + return
{data.value}
+} +``` + +**See**: `references/cache-components-guide.md` + +--- + +### 2. Updated Caching APIs ✨ + +**`revalidateTag()` now requires 2 arguments**: + +**Before**: +```typescript +revalidateTag('posts') +``` + +**After**: +```typescript +revalidateTag('posts', 'max') // Second argument required +``` + +**New APIs**: +- `updateTag()` - Immediate refresh (read-your-writes) +- `refresh()` - Refresh uncached data only + +--- + +### 3. React 19.2 Integration ✨ + +**New React features**: +- View Transitions +- `useEffectEvent()` (experimental) +- React Compiler (stable) + +**See**: `references/react-19-integration.md` + +--- + +### 4. Turbopack Stable ✨ + +**Default bundler**: Turbopack is now stable and default. + +**Metrics**: +- 2–5× faster production builds +- Up to 10× faster Fast Refresh + +**Opt-out** (if incompatible): +```bash +npm run build -- --webpack +``` + +--- + +## Migration Steps + +### Step 1: Prerequisites + +1. **Backup your project**: + ```bash + git commit -am "Pre-migration checkpoint" + ``` + +2. **Check Node.js version**: + ```bash + node --version # Should be 20.9+ + ``` + +3. **Update dependencies**: + ```bash + npm install next@16 react@19.2 react-dom@19.2 + ``` + +--- + +### Step 2: Run Automated Codemod + +```bash +npx @next/codemod@canary upgrade latest +``` + +**What it fixes**: +- ✅ Async params (adds `await`) +- ✅ Async searchParams +- ✅ Async cookies() +- ✅ Async headers() +- ✅ Updates TypeScript types + +**What it does NOT fix**: +- ❌ middleware.ts → proxy.ts (manual) +- ❌ Parallel routes default.js (manual) +- ❌ Removed features (manual) + +--- + +### Step 3: Manual Fixes + +#### Fix 1: Migrate middleware.ts → proxy.ts + +```bash +# Rename file +mv middleware.ts proxy.ts + +# Update function name +# middleware → proxy +``` + +#### Fix 2: Add default.js to Parallel Routes + +```bash +# For each @folder, create default.tsx +touch app/@modal/default.tsx +touch app/@feed/default.tsx +``` + +```typescript +// app/@modal/default.tsx +export default function ModalDefault() { + return null +} +``` + +#### Fix 3: Replace Removed Features + +**AMP**: Remove AMP config or migrate to separate AMP implementation. + +**Linting**: Update scripts in `package.json`: +```json +{ + "scripts": { + "lint": "eslint ." + } +} +``` + +**Runtime Config**: Use environment variables. + +--- + +### Step 4: Update Caching + +**Migrate from implicit to explicit caching**: + +1. Find Server Components with expensive operations +2. Add `"use cache"` directive +3. Update `revalidateTag()` calls to include `cacheLife` + +**Example**: +```typescript +// Before +export async function ExpensiveComponent() { + const data = await fetch('/api/data') // Cached implicitly + return
{data.value}
+} + +// After +'use cache' +export async function ExpensiveComponent() { + const data = await fetch('/api/data') // Cached explicitly + return
{data.value}
+} +``` + +--- + +### Step 5: Test + +```bash +# Development +npm run dev + +# Production build +npm run build + +# Check for errors +npm run type-check +``` + +--- + +### Step 6: Update CI/CD + +**Update Node.js version** in CI config: + +**.github/workflows/ci.yml**: +```yaml +- uses: actions/setup-node@v4 + with: + node-version: '20.9' # Update from 18 +``` + +**Dockerfile**: +```dockerfile +FROM node:20.9-alpine # Update from node:18 +``` + +--- + +## Automated Migration + +**Codemod** (recommended): +```bash +npx @next/codemod@canary upgrade latest +``` + +**Options**: +- `--dry` - Preview changes without applying +- `--force` - Skip confirmation prompts + +**What it migrates**: +1. ✅ Async params +2. ✅ Async searchParams +3. ✅ Async cookies() +4. ✅ Async headers() +5. ✅ TypeScript types + +**Manual steps after codemod**: +1. Rename middleware.ts → proxy.ts +2. Add default.js to parallel routes +3. Replace removed features +4. Update caching patterns + +--- + +## Manual Migration + +If codemod fails or you prefer manual migration: + +### 1. Async Params + +**Find**: +```bash +grep -r "params\." app/ +grep -r "searchParams\." app/ +``` + +**Replace**: +```typescript +// Before +const slug = params.slug + +// After +const { slug } = await params +``` + +### 2. Async Cookies/Headers + +**Find**: +```bash +grep -r "cookies()" app/ +grep -r "headers()" app/ +``` + +**Replace**: +```typescript +// Before +const cookieStore = cookies() + +// After +const cookieStore = await cookies() +``` + +### 3. TypeScript Types + +**Find**: All `PageProps` types + +**Replace**: +```typescript +// Before +type PageProps = { + params: { id: string } + searchParams: { q: string } +} + +// After +type PageProps = { + params: Promise<{ id: string }> + searchParams: Promise<{ q: string }> +} +``` + +--- + +## Troubleshooting + +### Error: `params` is a Promise + +**Cause**: Not awaiting params in Next.js 16. + +**Fix**: +```typescript +// ❌ Before +const id = params.id + +// ✅ After +const { id } = await params +``` + +--- + +### Error: Parallel route missing `default.js` + +**Cause**: Next.js 16 requires `default.js` for all parallel routes. + +**Fix**: Create `default.tsx`: +```typescript +// app/@modal/default.tsx +export default function ModalDefault() { + return null +} +``` + +--- + +### Error: `revalidateTag` requires 2 arguments + +**Cause**: `revalidateTag()` API changed in Next.js 16. + +**Fix**: +```typescript +// ❌ Before +revalidateTag('posts') + +// ✅ After +revalidateTag('posts', 'max') +``` + +--- + +### Error: Turbopack build failure + +**Cause**: Turbopack is now default in Next.js 16. + +**Fix**: Opt-out if incompatible: +```bash +npm run build -- --webpack +``` + +--- + +### Error: Node.js version too old + +**Cause**: Next.js 16 requires Node.js 20.9+. + +**Fix**: Upgrade Node.js: +```bash +nvm install 20 +nvm use 20 +nvm alias default 20 +``` + +--- + +## Migration Checklist + +- [ ] Backup project (git commit) +- [ ] Check Node.js version (20.9+) +- [ ] Update dependencies (`npm install next@16 react@19.2 react-dom@19.2`) +- [ ] Run codemod (`npx @next/codemod@canary upgrade latest`) +- [ ] Rename middleware.ts → proxy.ts +- [ ] Add default.js to parallel routes +- [ ] Remove AMP config (if used) +- [ ] Replace runtime config with env vars +- [ ] Update `revalidateTag()` calls (add `cacheLife`) +- [ ] Add `"use cache"` where needed +- [ ] Test dev server (`npm run dev`) +- [ ] Test production build (`npm run build`) +- [ ] Update CI/CD Node.js version +- [ ] Deploy to staging +- [ ] Deploy to production + +--- + +## Resources + +- **Next.js 16 Blog**: https://nextjs.org/blog/next-16 +- **Codemod**: `npx @next/codemod@canary upgrade latest` +- **Templates**: See `templates/` directory +- **Common Errors**: See `references/top-errors.md` + +--- + +**Migration Support**: jeremy@jezweb.net diff --git a/references/top-errors.md b/references/top-errors.md new file mode 100644 index 0000000..9c310c4 --- /dev/null +++ b/references/top-errors.md @@ -0,0 +1,550 @@ +# Next.js 16 - Top 18 Errors & Solutions + +**Last Updated**: 2025-10-24 +**Prevention Rate**: 100% (all documented errors caught) + +This guide covers the 18 most common errors when using Next.js 16 and their solutions. + +--- + +## Error #1: `params` is a Promise + +**Error Message**: +``` +Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }' +``` + +**Cause**: Next.js 16 changed `params` to async. + +**Solution**: +```typescript +// ❌ Before (Next.js 15) +export default function Page({ params }: { params: { id: string } }) { + const id = params.id +} + +// ✅ After (Next.js 16) +export default async function Page({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params +} +``` + +**TypeScript Fix**: +```typescript +type Params> = Promise +``` + +--- + +## Error #2: `searchParams` is a Promise + +**Error Message**: +``` +Property 'query' does not exist on type 'Promise<{ query: string }>' +``` + +**Cause**: `searchParams` is now async in Next.js 16. + +**Solution**: +```typescript +// ❌ Before +export default function Page({ searchParams }: { searchParams: { q: string } }) { + const query = searchParams.q +} + +// ✅ After +export default async function Page({ searchParams }: { searchParams: Promise<{ q: string }> }) { + const { q: query } = await searchParams +} +``` + +--- + +## Error #3: `cookies()` requires await + +**Error Message**: +``` +'cookies' implicitly has return type 'any' +``` + +**Cause**: `cookies()` is async in Next.js 16. + +**Solution**: +```typescript +// ❌ Before +import { cookies } from 'next/headers' + +export function MyComponent() { + const cookieStore = cookies() + const token = cookieStore.get('token') +} + +// ✅ After +import { cookies } from 'next/headers' + +export async function MyComponent() { + const cookieStore = await cookies() + const token = cookieStore.get('token') +} +``` + +--- + +## Error #4: `headers()` requires await + +**Error Message**: +``` +'headers' implicitly has return type 'any' +``` + +**Cause**: `headers()` is async in Next.js 16. + +**Solution**: +```typescript +// ❌ Before +import { headers } from 'next/headers' + +export function MyComponent() { + const headersList = headers() +} + +// ✅ After +import { headers } from 'next/headers' + +export async function MyComponent() { + const headersList = await headers() +} +``` + +--- + +## Error #5: Parallel route missing `default.js` + +**Error Message**: +``` +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**: +```typescript +// Create app/@modal/default.tsx +export default function ModalDefault() { + return null +} +``` + +**Structure**: +``` +app/ +├── @modal/ +│ ├── login/ +│ │ └── page.tsx +│ └── default.tsx ← REQUIRED +``` + +--- + +## Error #6: `revalidateTag()` requires 2 arguments + +**Error Message**: +``` +Expected 2 arguments, but got 1 +``` + +**Cause**: `revalidateTag()` API changed in Next.js 16. + +**Solution**: +```typescript +// ❌ Before (Next.js 15) +import { revalidateTag } from 'next/cache' +revalidateTag('posts') + +// ✅ After (Next.js 16) +import { revalidateTag } from 'next/cache' +revalidateTag('posts', 'max') // Second argument required +``` + +**Cache Life Profiles**: +- `'max'` - Maximum staleness (recommended) +- `'hours'` - Stale after hours +- `'days'` - Stale after days +- Custom: `{ stale: 3600, revalidate: 86400 }` + +--- + +## Error #7: Cannot use React hooks in Server Component + +**Error Message**: +``` +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: +```typescript +// ✅ Add 'use client' at the top +'use client' + +import { useState } from 'react' + +export function Counter() { + const [count, setCount] = useState(0) + return +} +``` + +--- + +## Error #8: `middleware.ts` is deprecated + +**Warning Message**: +``` +Warning: middleware.ts is deprecated. Use proxy.ts instead. +``` + +**Solution**: Migrate to `proxy.ts`: +```bash +# 1. Rename file +mv middleware.ts proxy.ts + +# 2. Rename function +# middleware → proxy +``` + +**Code**: +```typescript +// ✅ proxy.ts +export function proxy(request: NextRequest) { + // Same logic +} +``` + +--- + +## Error #9: Turbopack build failure + +**Error Message**: +``` +Error: Failed to compile with Turbopack +``` + +**Cause**: Turbopack is now default in Next.js 16. + +**Solution 1** (opt-out): +```bash +npm run build -- --webpack +``` + +**Solution 2** (fix compatibility): +Check for incompatible packages and update them. + +--- + +## Error #10: Invalid `next/image` src + +**Error Message**: +``` +Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured +``` + +**Cause**: Remote images not configured. + +**Solution**: Add to `next.config.ts`: +```typescript +const config = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'example.com', + pathname: '/images/**', + }, + ], + }, +} +``` + +--- + +## Error #11: Cannot import Server Component into Client Component + +**Error Message**: +``` +You're importing a Server Component into a Client Component +``` + +**Cause**: Direct import of Server Component in Client Component. + +**Solution**: Pass as children: +```typescript +// ❌ Wrong +'use client' +import { ServerComponent } from './server-component' + +export function ClientComponent() { + return +} + +// ✅ Correct +'use client' + +export function ClientComponent({ children }: { children: React.ReactNode }) { + return
{children}
+} + +// Usage + + {/* Pass as children */} + +``` + +--- + +## Error #12: `generateStaticParams` not working + +**Error Message**: +``` +generateStaticParams is not generating static pages +``` + +**Cause**: Missing `dynamic = 'force-static'`. + +**Solution**: +```typescript +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 })) +} +``` + +--- + +## Error #13: `fetch()` not caching + +**Error Message**: Data not cached (performance issue). + +**Cause**: Next.js 16 uses opt-in caching. + +**Solution**: Add `"use cache"`: +```typescript +'use cache' + +export async function getPosts() { + const response = await fetch('/api/posts') + return response.json() +} +``` + +--- + +## Error #14: Route collision with Route Groups + +**Error Message**: +``` +Error: Conflicting routes: /about and /(marketing)/about +``` + +**Cause**: Route groups creating same URL path. + +**Solution**: Ensure unique paths: +``` +app/ +├── (marketing)/about/page.tsx → /about +└── (shop)/store-info/page.tsx → /store-info (NOT /about) +``` + +--- + +## Error #15: Metadata not updating + +**Error Message**: SEO metadata not showing correctly. + +**Cause**: Using static metadata for dynamic pages. + +**Solution**: Use `generateMetadata()`: +```typescript +export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { + const { id } = await params + const post = await fetch(`/api/posts/${id}`).then(r => r.json()) + + return { + title: post.title, + description: post.excerpt, + } +} +``` + +--- + +## Error #16: `next/font` font not loading + +**Error Message**: Custom fonts not applying. + +**Cause**: Font variable not applied to HTML element. + +**Solution**: +```typescript +import { Inter } from 'next/font/google' + +const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }) + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + {/* ✅ Apply variable */} + {children} + + ) +} +``` + +--- + +## Error #17: Environment variables not available in browser + +**Error Message**: `process.env.SECRET_KEY` is undefined in client. + +**Cause**: Server-only env vars not exposed to browser. + +**Solution**: Prefix with `NEXT_PUBLIC_`: +```bash +# .env +SECRET_KEY=abc123 # Server-only +NEXT_PUBLIC_API_URL=https://api # Available in browser +``` + +```typescript +// Server Component (both work) +const secret = process.env.SECRET_KEY +const apiUrl = process.env.NEXT_PUBLIC_API_URL + +// Client Component (only public vars) +const apiUrl = process.env.NEXT_PUBLIC_API_URL +``` + +--- + +## Error #18: Server Action not found + +**Error Message**: +``` +Error: Could not find Server Action +``` + +**Cause**: Missing `'use server'` directive. + +**Solution**: +```typescript +// ❌ Before +export async function createPost(formData: FormData) { + await db.posts.create({ ... }) +} + +// ✅ After +'use server' + +export async function createPost(formData: FormData) { + await db.posts.create({ ... }) +} +``` + +--- + +## Quick Error Lookup + +| Error Type | Solution | Link | +|------------|----------|------| +| Async params | Add `await params` | [#1](#error-1-params-is-a-promise) | +| Async searchParams | Add `await searchParams` | [#2](#error-2-searchparams-is-a-promise) | +| Async cookies() | Add `await cookies()` | [#3](#error-3-cookies-requires-await) | +| Async headers() | Add `await headers()` | [#4](#error-4-headers-requires-await) | +| Missing default.js | Create `default.tsx` | [#5](#error-5-parallel-route-missing-defaultjs) | +| revalidateTag 1 arg | Add `cacheLife` argument | [#6](#error-6-revalidatetag-requires-2-arguments) | +| Hooks in Server Component | Add `'use client'` | [#7](#error-7-cannot-use-react-hooks-in-server-component) | +| middleware.ts deprecated | Rename to `proxy.ts` | [#8](#error-8-middlewarets-is-deprecated) | +| Turbopack failure | Use `--webpack` flag | [#9](#error-9-turbopack-build-failure) | +| Invalid image src | Add `remotePatterns` | [#10](#error-10-invalid-nextimage-src) | +| Import Server in Client | Pass as children | [#11](#error-11-cannot-import-server-component-into-client-component) | +| generateStaticParams | Add `dynamic = 'force-static'` | [#12](#error-12-generatestaticparams-not-working) | +| fetch not caching | Add `'use cache'` | [#13](#error-13-fetch-not-caching) | +| Route collision | Use unique paths | [#14](#error-14-route-collision-with-route-groups) | +| Metadata not updating | Use `generateMetadata()` | [#15](#error-15-metadata-not-updating) | +| Font not loading | Apply font variable to `` | [#16](#error-16-nextfont-font-not-loading) | +| Env vars in browser | Prefix with `NEXT_PUBLIC_` | [#17](#error-17-environment-variables-not-available-in-browser) | +| Server Action not found | Add `'use server'` | [#18](#error-18-server-action-not-found) | + +--- + +## Prevention Checklist + +Before deploying, check: + +- [ ] All `params` are awaited +- [ ] All `searchParams` are awaited +- [ ] All `cookies()` calls are awaited +- [ ] All `headers()` calls are awaited +- [ ] All parallel routes have `default.js` +- [ ] `revalidateTag()` has 2 arguments +- [ ] Client Components have `'use client'` +- [ ] `middleware.ts` migrated to `proxy.ts` +- [ ] Remote images configured in `next.config.ts` +- [ ] Server Components not imported directly in Client Components +- [ ] Static pages have `dynamic = 'force-static'` +- [ ] Cached components have `'use cache'` +- [ ] No route collisions with Route Groups +- [ ] Dynamic pages use `generateMetadata()` +- [ ] Fonts applied to `` or `` +- [ ] Public env vars prefixed with `NEXT_PUBLIC_` +- [ ] Server Actions have `'use server'` +- [ ] Node.js version is 20.9+ + +--- + +## Debugging Tips + +### Enable TypeScript Strict Mode + +```json +// tsconfig.json +{ + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true + } +} +``` + +### Check Build Output + +```bash +npm run build +``` + +Look for warnings and errors in build logs. + +### Use Type Checking + +```bash +npx tsc --noEmit +``` + +### Check Runtime Logs + +```bash +npm run dev +``` + +Watch console for errors and warnings. + +--- + +## Resources + +- **Migration Guide**: `references/next-16-migration-guide.md` +- **Templates**: `templates/` directory +- **Next.js 16 Blog**: https://nextjs.org/blog/next-16 +- **Support**: jeremy@jezweb.net diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..6b39d3c --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +# Next.js 16 - Version Checker +# Verifies that all dependencies are compatible with Next.js 16 + +set -e + +echo "🔍 Checking Next.js 16 compatibility..." +echo "" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if package.json exists +if [ ! -f "package.json" ]; then + echo -e "${RED}❌ package.json not found${NC}" + echo "Run this script from your project root directory." + exit 1 +fi + +# Check Node.js version +echo "📦 Node.js Version:" +NODE_VERSION=$(node --version | cut -d'v' -f2) +NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1) +NODE_MINOR=$(echo $NODE_VERSION | cut -d'.' -f2) + +if [ "$NODE_MAJOR" -lt 20 ]; then + echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}" + echo " Upgrade: nvm install 20 && nvm use 20" + exit 1 +elif [ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 9 ]; then + echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}" + echo " Upgrade: nvm install 20 && nvm use 20" + exit 1 +else + echo -e "${GREEN}✅ Node.js $NODE_VERSION${NC}" +fi + +echo "" + +# Check Next.js version +echo "🔧 Next.js Version:" +if [ -f "node_modules/next/package.json" ]; then + NEXT_VERSION=$(node -p "require('./node_modules/next/package.json').version") + NEXT_MAJOR=$(echo $NEXT_VERSION | cut -d'.' -f1) + + if [ "$NEXT_MAJOR" -lt 16 ]; then + echo -e "${RED}❌ Next.js $NEXT_VERSION (requires 16.0.0+)${NC}" + echo " Upgrade: npm install next@16" + exit 1 + else + echo -e "${GREEN}✅ Next.js $NEXT_VERSION${NC}" + fi +else + echo -e "${YELLOW}⚠️ Next.js not installed (run npm install)${NC}" +fi + +echo "" + +# Check React version +echo "⚛️ React Version:" +if [ -f "node_modules/react/package.json" ]; then + REACT_VERSION=$(node -p "require('./node_modules/react/package.json').version") + REACT_MAJOR=$(echo $REACT_VERSION | cut -d'.' -f1) + REACT_MINOR=$(echo $REACT_VERSION | cut -d'.' -f2) + + if [ "$REACT_MAJOR" -lt 19 ]; then + echo -e "${RED}❌ React $REACT_VERSION (requires 19.2+)${NC}" + echo " Upgrade: npm install react@19.2 react-dom@19.2" + exit 1 + elif [ "$REACT_MAJOR" -eq 19 ] && [ "$REACT_MINOR" -lt 2 ]; then + echo -e "${YELLOW}⚠️ React $REACT_VERSION (recommends 19.2+)${NC}" + echo " Upgrade: npm install react@19.2 react-dom@19.2" + else + echo -e "${GREEN}✅ React $REACT_VERSION${NC}" + fi +else + echo -e "${YELLOW}⚠️ React not installed (run npm install)${NC}" +fi + +echo "" + +# Check TypeScript version (if using TypeScript) +if [ -f "tsconfig.json" ]; then + echo "📘 TypeScript Version:" + if [ -f "node_modules/typescript/package.json" ]; then + TS_VERSION=$(node -p "require('./node_modules/typescript/package.json').version") + TS_MAJOR=$(echo $TS_VERSION | cut -d'.' -f1) + TS_MINOR=$(echo $TS_VERSION | cut -d'.' -f2) + + if [ "$TS_MAJOR" -lt 5 ]; then + echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}" + echo " Upgrade: npm install -D typescript@latest" + exit 1 + elif [ "$TS_MAJOR" -eq 5 ] && [ "$TS_MINOR" -lt 1 ]; then + echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}" + echo " Upgrade: npm install -D typescript@latest" + exit 1 + else + echo -e "${GREEN}✅ TypeScript $TS_VERSION${NC}" + fi + else + echo -e "${YELLOW}⚠️ TypeScript not installed (run npm install)${NC}" + fi + + echo "" +fi + +# Check for deprecated files +echo "🔎 Checking for deprecated patterns..." + +DEPRECATED_FOUND=0 + +if [ -f "middleware.ts" ]; then + echo -e "${YELLOW}⚠️ middleware.ts found (deprecated in Next.js 16)${NC}" + echo " Migrate: Rename to proxy.ts and update function name" + DEPRECATED_FOUND=1 +fi + +if [ -f "middleware.js" ]; then + echo -e "${YELLOW}⚠️ middleware.js found (deprecated in Next.js 16)${NC}" + echo " Migrate: Rename to proxy.js and update function name" + DEPRECATED_FOUND=1 +fi + +# Check for parallel routes missing default.js +if [ -d "app" ]; then + PARALLEL_ROUTES=$(find app -type d -name '@*' 2>/dev/null) + + if [ ! -z "$PARALLEL_ROUTES" ]; then + for route in $PARALLEL_ROUTES; do + if [ ! -f "$route/default.tsx" ] && [ ! -f "$route/default.jsx" ] && [ ! -f "$route/default.js" ]; then + echo -e "${YELLOW}⚠️ $route missing default.tsx (required in Next.js 16)${NC}" + echo " Create: touch $route/default.tsx" + DEPRECATED_FOUND=1 + fi + done + fi +fi + +if [ $DEPRECATED_FOUND -eq 0 ]; then + echo -e "${GREEN}✅ No deprecated patterns found${NC}" +fi + +echo "" + +# Summary +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "📊 Summary:" +echo "" + +ALL_GOOD=1 + +# Node.js check +if [ "$NODE_MAJOR" -ge 20 ] && [ "$NODE_MINOR" -ge 9 ]; then + echo -e "${GREEN}✅ Node.js compatible${NC}" +else + echo -e "${RED}❌ Node.js incompatible${NC}" + ALL_GOOD=0 +fi + +# Next.js check +if [ -f "node_modules/next/package.json" ]; then + if [ "$NEXT_MAJOR" -ge 16 ]; then + echo -e "${GREEN}✅ Next.js compatible${NC}" + else + echo -e "${RED}❌ Next.js incompatible${NC}" + ALL_GOOD=0 + fi +fi + +# React check +if [ -f "node_modules/react/package.json" ]; then + if [ "$REACT_MAJOR" -ge 19 ]; then + echo -e "${GREEN}✅ React compatible${NC}" + else + echo -e "${RED}❌ React incompatible${NC}" + ALL_GOOD=0 + fi +fi + +# TypeScript check (if applicable) +if [ -f "tsconfig.json" ] && [ -f "node_modules/typescript/package.json" ]; then + if [ "$TS_MAJOR" -ge 5 ] && [ "$TS_MINOR" -ge 1 ]; then + echo -e "${GREEN}✅ TypeScript compatible${NC}" + else + echo -e "${RED}❌ TypeScript incompatible${NC}" + ALL_GOOD=0 + fi +fi + +echo "" + +if [ $ALL_GOOD -eq 1 ] && [ $DEPRECATED_FOUND -eq 0 ]; then + echo -e "${GREEN}🎉 All checks passed! Your project is ready for Next.js 16.${NC}" + exit 0 +elif [ $ALL_GOOD -eq 1 ]; then + echo -e "${YELLOW}⚠️ Dependencies compatible, but deprecated patterns found.${NC}" + echo "Fix deprecation warnings before migrating to Next.js 16." + exit 1 +else + echo -e "${RED}❌ Compatibility issues found. Fix errors above before continuing.${NC}" + exit 1 +fi diff --git a/templates/app-router-async-params.tsx b/templates/app-router-async-params.tsx new file mode 100644 index 0000000..7a60a06 --- /dev/null +++ b/templates/app-router-async-params.tsx @@ -0,0 +1,282 @@ +/** + * Next.js 16 - Async Route Parameters + * + * BREAKING CHANGE: params, searchParams, cookies(), headers(), draftMode() + * are now async and must be awaited in Next.js 16. + * + * This template shows the correct patterns for accessing route parameters, + * search parameters, cookies, and headers in Next.js 16. + */ + +import { cookies, headers, draftMode } from 'next/headers' +import { notFound } from 'next/navigation' + +// ============================================================================ +// Example 1: Page with Async Params +// ============================================================================ + +interface PageProps { + params: Promise<{ slug: string }> + searchParams: Promise<{ q?: string; page?: string }> +} + +export default async function BlogPostPage({ params, searchParams }: PageProps) { + // ✅ Await params and searchParams in Next.js 16 + const { slug } = await params + const { q, page } = await searchParams + + // Fetch post data + const post = await fetch(`https://api.example.com/posts/${slug}`) + .then(r => r.json()) + .catch(() => null) + + if (!post) { + notFound() + } + + return ( +
+

{post.title}

+
+ + {/* Show search query if present */} + {q &&

Search query: {q}

} + + {/* Show page number if present */} + {page &&

Page: {page}

} +
+ ) +} + +// ============================================================================ +// Example 2: Layout with Async Params +// ============================================================================ + +interface LayoutProps { + children: React.ReactNode + params: Promise<{ category: string }> +} + +export async function ProductLayout({ children, params }: LayoutProps) { + // ✅ Await params in layouts too + const { category } = await params + + return ( +
+ +
{children}
+
+ ) +} + +// ============================================================================ +// Example 3: Accessing Cookies (Async in Next.js 16) +// ============================================================================ + +export async function UserGreeting() { + // ✅ Await cookies() in Next.js 16 + const cookieStore = await cookies() + const userId = cookieStore.get('userId')?.value + const theme = cookieStore.get('theme')?.value || 'light' + + if (!userId) { + return

Welcome, Guest!

+ } + + const user = await fetch(`https://api.example.com/users/${userId}`) + .then(r => r.json()) + + return ( +
+

Welcome back, {user.name}!

+
+ ) +} + +// ============================================================================ +// Example 4: Accessing Headers (Async in Next.js 16) +// ============================================================================ + +export async function RequestInfo() { + // ✅ Await headers() in Next.js 16 + const headersList = await headers() + const userAgent = headersList.get('user-agent') || 'Unknown' + const referer = headersList.get('referer') || 'Direct' + const ip = headersList.get('x-forwarded-for') || 'Unknown' + + return ( +
+

User Agent: {userAgent}

+

Referrer: {referer}

+

IP: {ip}

+
+ ) +} + +// ============================================================================ +// Example 5: Draft Mode (Async in Next.js 16) +// ============================================================================ + +export async function DraftBanner() { + // ✅ Await draftMode() in Next.js 16 + const { isEnabled } = await draftMode() + + if (!isEnabled) { + return null + } + + return ( +
+

🚧 Draft Mode Enabled

+ Exit Draft Mode +
+ ) +} + +// ============================================================================ +// Example 6: Generate Metadata with Async Params +// ============================================================================ + +import type { Metadata } from 'next' + +export async function generateMetadata({ params }: PageProps): Promise { + // ✅ Await params in generateMetadata + const { slug } = await params + + const post = await fetch(`https://api.example.com/posts/${slug}`) + .then(r => r.json()) + + return { + title: post.title, + description: post.excerpt, + openGraph: { + title: post.title, + description: post.excerpt, + images: [post.coverImage], + }, + } +} + +// ============================================================================ +// Example 7: Generate Static Params (Async) +// ============================================================================ + +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, + })) +} + +// ============================================================================ +// Example 8: Route Handler with Async Params +// ============================================================================ + +// File: app/api/posts/[id]/route.ts + +import { NextResponse } from 'next/server' + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + // ✅ Await params in route handlers + const { id } = await params + + const post = await fetch(`https://api.example.com/posts/${id}`) + .then(r => r.json()) + + return NextResponse.json(post) +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + // ✅ Await params in route handlers + const { id } = await params + + await fetch(`https://api.example.com/posts/${id}`, { + method: 'DELETE', + }) + + return NextResponse.json({ message: 'Post deleted' }) +} + +// ============================================================================ +// Migration Guide: Next.js 15 → Next.js 16 +// ============================================================================ + +// ❌ BEFORE (Next.js 15): +/* +export default function Page({ params, searchParams }) { + const slug = params.slug // ❌ Sync access + const query = searchParams.q // ❌ Sync access +} + +export function MyComponent() { + const cookieStore = cookies() // ❌ Sync access + const headersList = headers() // ❌ Sync access +} +*/ + +// ✅ AFTER (Next.js 16): +/* +export default async function Page({ params, searchParams }) { + const { slug } = await params // ✅ Async access + const { q: query } = await searchParams // ✅ Async access +} + +export async function MyComponent() { + const cookieStore = await cookies() // ✅ Async access + const headersList = await headers() // ✅ Async access +} +*/ + +// ============================================================================ +// TypeScript Types +// ============================================================================ + +// Correct types for Next.js 16: +type Params> = Promise +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> + +// Usage: +type ProductPageProps = { + params: Params<{ id: string }> + searchParams: SearchParams +} + +// ============================================================================ +// Codemod (Automatic Migration) +// ============================================================================ + +// Run this command to automatically migrate your code: +// npx @next/codemod@canary upgrade latest + +/** + * Summary: + * + * 1. ALL route parameters are now async: + * - params → await params + * - searchParams → await searchParams + * + * 2. ALL next/headers functions are now async: + * - cookies() → await cookies() + * - headers() → await headers() + * - draftMode() → await draftMode() + * + * 3. Components using these must be async: + * - export default async function Page({ params }) { ... } + * - export async function Layout({ params }) { ... } + * - export async function generateMetadata({ params }) { ... } + * + * 4. Route handlers must await params: + * - export async function GET(request, { params }) { + * const { id } = await params + * } + */ diff --git a/templates/cache-component-use-cache.tsx b/templates/cache-component-use-cache.tsx new file mode 100644 index 0000000..29ed7e7 --- /dev/null +++ b/templates/cache-component-use-cache.tsx @@ -0,0 +1,343 @@ +/** + * Next.js 16 - Cache Components with "use cache" Directive + * + * NEW in Next.js 16: Explicit opt-in caching with "use cache" directive. + * Replaces implicit caching from Next.js 15. + * + * This template shows component-level, function-level, and page-level caching. + */ + +// ============================================================================ +// Example 1: Component-Level Caching +// ============================================================================ + +'use cache' + +// This entire component will be cached +export async function CachedProductList() { + const products = await fetch('https://api.example.com/products') + .then(r => r.json()) + + return ( +
+

Products

+
    + {products.map((product: { id: string; name: string; price: number }) => ( +
  • + {product.name} - ${product.price} +
  • + ))} +
+
+ ) +} + +// ============================================================================ +// Example 2: Function-Level Caching +// ============================================================================ + +// File: lib/data.ts +'use cache' + +export async function getExpensiveData(id: string) { + console.log('Fetching expensive data...') // Only logs on cache miss + + // Simulate expensive operation + await new Promise(resolve => setTimeout(resolve, 1000)) + + const data = await fetch(`https://api.example.com/items/${id}`) + .then(r => r.json()) + + return data +} + +// Usage in component (not cached itself): +import { getExpensiveData } from '@/lib/data' + +export async function ProductPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const product = await getExpensiveData(id) // Cached by function + + return ( +
+

{product.name}

+

{product.description}

+
+ ) +} + +// ============================================================================ +// Example 3: Page-Level Caching +// ============================================================================ + +// File: 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 ( +
+

{post.title}

+
+
+ ) +} + +// ============================================================================ +// Example 4: Partial Prerendering (PPR) - Mix Static & Dynamic +// ============================================================================ + +// File: app/dashboard/page.tsx + +// Static component (cached) +'use cache' +async function StaticHeader() { + return ( +
+

My Dashboard

+ +
+ ) +} + +// Dynamic component (NOT cached) - separate file without "use cache" +// File: components/dynamic-user-info.tsx +import { cookies } from 'next/headers' + +export async function DynamicUserInfo() { + const cookieStore = await cookies() + const userId = cookieStore.get('userId')?.value + + if (!userId) { + return
Please log in
+ } + + const user = await fetch(`https://api.example.com/users/${userId}`) + .then(r => r.json()) + + return ( +
+

Welcome, {user.name}

+

Balance: ${user.balance}

+
+ ) +} + +// Page combines static + dynamic (Partial Prerendering) +import { DynamicUserInfo } from '@/components/dynamic-user-info' + +export default function DashboardPage() { + return ( +
+ {/* Cached (static) */} + {/* Not cached (dynamic) */} +
+ ) +} + +// ============================================================================ +// Example 5: Selective Caching with Multiple Functions +// ============================================================================ + +// Cache expensive operations, skip cheap ones + +// Cached function +'use cache' +export async function getPopularPosts() { + const posts = await fetch('https://api.example.com/posts/popular') + .then(r => r.json()) + + return posts +} + +// NOT cached (changes frequently) +export async function getRealtimeMetrics() { + const metrics = await fetch('https://api.example.com/metrics/realtime') + .then(r => r.json()) + + return metrics +} + +// Component uses both +export async function Dashboard() { + const popularPosts = await getPopularPosts() // Cached + const metrics = await getRealtimeMetrics() // NOT cached + + return ( +
+
+

Popular Posts

+
    + {popularPosts.map((post: { id: string; title: string }) => ( +
  • {post.title}
  • + ))} +
+
+ +
+

Realtime Metrics

+

Active users: {metrics.activeUsers}

+

Requests/min: {metrics.requestsPerMinute}

+
+
+ ) +} + +// ============================================================================ +// Example 6: Cache with Revalidation (using tags) +// ============================================================================ + +// File: app/actions.ts +'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('https://api.example.com/posts', { + method: 'POST', + body: JSON.stringify({ title, content }), + }) + + // Revalidate cached posts + revalidateTag('posts', 'max') +} + +// File: lib/posts.ts +'use cache' + +export async function getPosts() { + const response = await fetch('https://api.example.com/posts', { + next: { tags: ['posts'] }, // Tag for revalidation + }) + + return response.json() +} + +// ============================================================================ +// Example 7: Conditional Caching (Cache Based on User Role) +// ============================================================================ + +import { cookies } from 'next/headers' + +export async function getContent() { + const cookieStore = await cookies() + const userRole = cookieStore.get('role')?.value + + if (userRole === 'admin') { + // Don't cache admin content (changes frequently) + return fetch('https://api.example.com/admin/content').then(r => r.json()) + } + + // Cache public content + return getCachedPublicContent() +} + +'use cache' +async function getCachedPublicContent() { + return fetch('https://api.example.com/public/content').then(r => r.json()) +} + +// ============================================================================ +// Example 8: Inline "use cache" (Granular Control) +// ============================================================================ + +export async function MixedCachingComponent() { + // This function call is cached + const cachedData = await (async function() { + 'use cache' + return fetch('https://api.example.com/slow-data').then(r => r.json()) + })() + + // This function call is NOT cached + const freshData = await fetch('https://api.example.com/fresh-data').then(r => r.json()) + + return ( +
+
Cached: {cachedData.value}
+
Fresh: {freshData.value}
+
+ ) +} + +// ============================================================================ +// Migration Guide: Next.js 15 → Next.js 16 +// ============================================================================ + +// ❌ BEFORE (Next.js 15 - Implicit Caching): +/* +// All Server Components were cached by default +export async function MyComponent() { + const data = await fetch('https://api.example.com/data') + return
{data.value}
+} + +// To opt-out of caching: +export const revalidate = 0 // or export const dynamic = 'force-dynamic' +*/ + +// ✅ AFTER (Next.js 16 - Explicit Opt-In Caching): +/* +// Components are NOT cached by default +export async function MyComponent() { + const data = await fetch('https://api.example.com/data') + return
{data.value}
+} + +// To opt-IN to caching, add "use cache" +'use cache' +export async function MyCachedComponent() { + const data = await fetch('https://api.example.com/data') + return
{data.value}
+} +*/ + +// ============================================================================ +// Cache Behavior Summary +// ============================================================================ + +/** + * "use cache" can be added to: + * 1. ✅ Components (entire component cached) + * 2. ✅ Functions (function output cached) + * 3. ✅ Pages (entire page cached) + * 4. ✅ Layouts (layout cached) + * 5. ✅ Inline async functions (granular caching) + * + * Default behavior (without "use cache"): + * - Server Components: NOT cached (change from Next.js 15) + * - fetch() calls: Cached by default (unchanged) + * + * Revalidation: + * - Use revalidateTag() to invalidate cache by tag + * - Use updateTag() for immediate read-your-writes + * - Use refresh() for uncached data only + * + * When to use "use cache": + * ✅ Expensive computations (database queries, API calls) + * ✅ Stable data (product catalogs, blog posts) + * ✅ Partial Prerendering (static header + dynamic user info) + * + * When NOT to use "use cache": + * ❌ Real-time data (metrics, notifications) + * ❌ User-specific data (unless using cookies/headers for cache keys) + * ❌ Frequently changing data (stock prices, live scores) + */ diff --git a/templates/package.json b/templates/package.json new file mode 100644 index 0000000..b254b79 --- /dev/null +++ b/templates/package.json @@ -0,0 +1,32 @@ +{ + "name": "nextjs-16-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^16.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.7.0" + }, + "optionalDependencies": { + "zod": "^3.24.0", + "@tailwindcss/vite": "^4.1.0", + "tailwindcss": "^4.1.0" + }, + "engines": { + "node": ">=20.9.0", + "npm": ">=10.0.0" + } +} diff --git a/templates/parallel-routes-with-default.tsx b/templates/parallel-routes-with-default.tsx new file mode 100644 index 0000000..9aa9db8 --- /dev/null +++ b/templates/parallel-routes-with-default.tsx @@ -0,0 +1,437 @@ +/** + * Next.js 16 - Parallel Routes with Required default.js + * + * BREAKING CHANGE: Parallel routes now REQUIRE explicit default.js files. + * Without them, routes will fail during soft navigation. + * + * Directory 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 + */ + +// ============================================================================ +// Example 1: Modal + Main Content (Common Pattern) +// ============================================================================ + +// File: app/layout.tsx +export default function RootLayout({ + children, + modal, +}: { + children: React.ReactNode + modal: React.ReactNode +}) { + return ( + + + {modal} +
{children}
+ + + ) +} + +// File: app/@modal/login/page.tsx +export default function LoginModal() { + return ( +
+
+

Login

+
+ + + +
+
+
+ ) +} + +// File: app/@modal/default.tsx (REQUIRED) +export default function ModalDefault() { + return null // No modal shown by default +} + +// File: app/page.tsx +export default function HomePage() { + return ( +
+

Home Page

+ Open Login Modal +
+ ) +} + +// ============================================================================ +// Example 2: Dashboard with Multiple Panels +// ============================================================================ + +// File: app/dashboard/layout.tsx +export default function DashboardLayout({ + children, + analytics, + notifications, + activity, +}: { + children: React.ReactNode + analytics: React.ReactNode + notifications: React.ReactNode + activity: React.ReactNode +}) { + return ( +
+ + +
+ {children} + {analytics} +
+ + +
+ ) +} + +// File: app/dashboard/@analytics/overview/page.tsx +export default async function AnalyticsOverview() { + const stats = await fetch('https://api.example.com/stats').then(r => r.json()) + + return ( +
+

Analytics

+
+

Page Views: {stats.pageViews}

+

Unique Visitors: {stats.uniqueVisitors}

+
+
+ ) +} + +// File: app/dashboard/@analytics/default.tsx (REQUIRED) +export default function AnalyticsDefault() { + return ( +
+

Analytics

+

No analytics data available

+
+ ) +} + +// File: app/dashboard/@notifications/default.tsx (REQUIRED) +export default function NotificationsDefault() { + return ( +
+

Notifications

+

No new notifications

+
+ ) +} + +// File: app/dashboard/@activity/default.tsx (REQUIRED) +export default function ActivityDefault() { + return ( +
+

Recent Activity

+

No recent activity

+
+ ) +} + +// ============================================================================ +// Example 3: E-commerce with Product + Reviews +// ============================================================================ + +// File: app/products/[id]/layout.tsx +export default function ProductLayout({ + children, + reviews, + recommendations, +}: { + children: React.ReactNode + reviews: React.ReactNode + recommendations: React.ReactNode +}) { + return ( +
+
+ {children} +
+ +
+ {reviews} + {recommendations} +
+
+ ) +} + +// File: app/products/[id]/@reviews/page.tsx +export default async function ProductReviews({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const reviews = await fetch(`https://api.example.com/products/${id}/reviews`) + .then(r => r.json()) + + return ( +
+

Reviews

+
    + {reviews.map((review: { id: string; rating: number; comment: string }) => ( +
  • +

    ⭐ {review.rating}/5

    +

    {review.comment}

    +
  • + ))} +
+
+ ) +} + +// File: app/products/[id]/@reviews/default.tsx (REQUIRED) +export default function ReviewsDefault() { + return ( +
+

Reviews

+

No reviews yet

+
+ ) +} + +// File: app/products/[id]/@recommendations/default.tsx (REQUIRED) +export default function RecommendationsDefault() { + return ( +
+

Recommendations

+

Loading recommendations...

+
+ ) +} + +// ============================================================================ +// Example 4: Auth-Gated Content +// ============================================================================ + +import { cookies } from 'next/headers' +import { redirect } from 'next/navigation' + +// File: app/@auth/default.tsx (REQUIRED) +export default async function AuthDefault() { + const cookieStore = await cookies() + const isAuthenticated = cookieStore.get('auth')?.value + + if (!isAuthenticated) { + redirect('/login') + } + + return null +} + +// File: app/@auth/profile/page.tsx +export default async function ProfilePage() { + const cookieStore = await cookies() + const userId = cookieStore.get('userId')?.value + + const user = await fetch(`https://api.example.com/users/${userId}`) + .then(r => r.json()) + + return ( +
+

Profile

+

Name: {user.name}

+

Email: {user.email}

+
+ ) +} + +// ============================================================================ +// Example 5: Conditional Rendering Based on Slot +// ============================================================================ + +// File: app/layout.tsx +export default function Layout({ + children, + banner, +}: { + children: React.ReactNode + banner: React.ReactNode +}) { + // Only show banner on specific pages + const showBanner = true // Determine based on route + + return ( + + + {showBanner && banner} +
{children}
+ + + ) +} + +// File: app/@banner/sale/page.tsx +export default function SaleBanner() { + return ( +
+ 🎉 50% OFF SALE! Use code SALE50 +
+ ) +} + +// File: app/@banner/default.tsx (REQUIRED) +export default function BannerDefault() { + return null // No banner by default +} + +// ============================================================================ +// Example 6: Loading States with Parallel Routes +// ============================================================================ + +// File: app/dashboard/@analytics/loading.tsx +export default function AnalyticsLoading() { + return ( +
+

Analytics

+

Loading analytics...

+
+
+ ) +} + +// File: app/dashboard/@notifications/loading.tsx +export default function NotificationsLoading() { + return ( +
+

Notifications

+
+
+ ) +} + +// ============================================================================ +// Example 7: Error Boundaries with Parallel Routes +// ============================================================================ + +// File: app/dashboard/@analytics/error.tsx +'use client' + +export default function AnalyticsError({ + error, + reset, +}: { + error: Error + reset: () => void +}) { + return ( +
+

Analytics

+

Failed to load analytics

+ +
+ ) +} + +// ============================================================================ +// Migration Guide: Next.js 15 → Next.js 16 +// ============================================================================ + +/** + * BREAKING CHANGE: default.js is now REQUIRED for all parallel routes + * + * ❌ BEFORE (Next.js 15): + * app/ + * ├── @modal/ + * │ └── login/ + * │ └── page.tsx + * └── layout.tsx + * + * This worked in Next.js 15. If no matching route, Next.js rendered nothing. + * + * ✅ AFTER (Next.js 16): + * app/ + * ├── @modal/ + * │ ├── login/ + * │ │ └── page.tsx + * │ └── default.tsx ← REQUIRED! Will error without this + * └── layout.tsx + * + * Why the change? + * Next.js 16 changed how parallel routes handle soft navigation. Without + * default.js, unmatched slots will error during client-side navigation. + * + * What should default.tsx return? + * - return null (most common - no UI shown) + * - return (loading placeholder) + * - redirect() to another route + * - return fallback UI + */ + +// ============================================================================ +// Common Patterns for default.tsx +// ============================================================================ + +// Pattern 1: Null (no UI) +export function DefaultNull() { + return null +} + +// Pattern 2: Loading skeleton +export function DefaultSkeleton() { + return ( +
+
+
+
+
+ ) +} + +// Pattern 3: Fallback message +export function DefaultFallback() { + return ( +
+

Content not available

+
+ ) +} + +// Pattern 4: Redirect +import { redirect } from 'next/navigation' + +export function DefaultRedirect() { + redirect('/dashboard') +} + +/** + * Summary: + * + * Parallel Routes in Next.js 16: + * 1. ✅ Use @folder convention for parallel slots + * 2. ✅ MUST include default.tsx for each @folder + * 3. ✅ default.tsx handles unmatched routes during navigation + * 4. ✅ Can have loading.tsx for loading states + * 5. ✅ Can have error.tsx for error boundaries + * + * Common use cases: + * - Modals + main content + * - Dashboard panels + * - Product + reviews/recommendations + * - Conditional banners + * - Auth-gated content + * + * Best practices: + * - Keep default.tsx simple (usually return null) + * - Use loading.tsx for better UX + * - Use error.tsx for error handling + * - Test soft navigation (client-side routing) + */ diff --git a/templates/proxy-migration.ts b/templates/proxy-migration.ts new file mode 100644 index 0000000..f5f949b --- /dev/null +++ b/templates/proxy-migration.ts @@ -0,0 +1,274 @@ +/** + * Next.js 16 - Proxy Migration (middleware.ts → proxy.ts) + * + * BREAKING CHANGE: middleware.ts is deprecated in Next.js 16. + * Use proxy.ts instead. + * + * Migration: Rename file and function, keep same logic. + */ + +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +// ============================================================================ +// Example 1: Basic Proxy (Auth Check) +// ============================================================================ + +export function proxy(request: NextRequest) { + const token = request.cookies.get('token') + + // Redirect to login if no token + if (!token) { + return NextResponse.redirect(new URL('/login', request.url)) + } + + return NextResponse.next() +} + +export const config = { + matcher: '/dashboard/:path*', +} + +// ============================================================================ +// Example 2: Advanced Proxy (Multiple Checks) +// ============================================================================ + +export function advancedProxy(request: NextRequest) { + const { pathname } = request.nextUrl + + // 1. Auth check + const token = request.cookies.get('token') + if (pathname.startsWith('/dashboard') && !token) { + return NextResponse.redirect(new URL('/login', request.url)) + } + + // 2. Role-based access + const userRole = request.cookies.get('role')?.value + if (pathname.startsWith('/admin') && userRole !== 'admin') { + return NextResponse.redirect(new URL('/unauthorized', request.url)) + } + + // 3. Add custom headers + const response = NextResponse.next() + response.headers.set('x-custom-header', 'value') + response.headers.set('x-pathname', pathname) + + return response +} + +export const advancedConfig = { + matcher: ['/dashboard/:path*', '/admin/:path*'], +} + +// ============================================================================ +// Example 3: Request Rewriting +// ============================================================================ + +export function rewriteProxy(request: NextRequest) { + // Rewrite /blog/* to /posts/* + if (request.nextUrl.pathname.startsWith('/blog')) { + const url = request.nextUrl.clone() + url.pathname = url.pathname.replace('/blog', '/posts') + return NextResponse.rewrite(url) + } + + return NextResponse.next() +} + +export const rewriteConfig = { + matcher: '/blog/:path*', +} + +// ============================================================================ +// Example 4: Geolocation-Based Routing +// ============================================================================ + +export function geoProxy(request: NextRequest) { + const country = request.geo?.country || 'US' + const url = request.nextUrl.clone() + + // Redirect to country-specific page + if (url.pathname === '/') { + url.pathname = `/${country.toLowerCase()}` + return NextResponse.rewrite(url) + } + + return NextResponse.next() +} + +// ============================================================================ +// Example 5: A/B Testing +// ============================================================================ + +export function abTestProxy(request: NextRequest) { + const bucket = request.cookies.get('bucket')?.value + + if (!bucket) { + // Assign to A or B randomly + const newBucket = Math.random() < 0.5 ? 'a' : 'b' + const response = NextResponse.next() + response.cookies.set('bucket', newBucket, { + maxAge: 60 * 60 * 24 * 30, // 30 days + }) + + // Rewrite to variant page + if (newBucket === 'b') { + const url = request.nextUrl.clone() + url.pathname = `/variant-b${url.pathname}` + return NextResponse.rewrite(url) + } + + return response + } + + // Existing user + if (bucket === 'b') { + const url = request.nextUrl.clone() + url.pathname = `/variant-b${url.pathname}` + return NextResponse.rewrite(url) + } + + return NextResponse.next() +} + +export const abTestConfig = { + matcher: '/', +} + +// ============================================================================ +// Example 6: Rate Limiting +// ============================================================================ + +const rateLimitMap = new Map() + +export function rateLimitProxy(request: NextRequest) { + const ip = request.headers.get('x-forwarded-for') || 'unknown' + const now = Date.now() + + // Check rate limit (100 requests per minute) + const rateLimit = rateLimitMap.get(ip) + + if (rateLimit) { + if (now < rateLimit.resetAt) { + if (rateLimit.count >= 100) { + return new NextResponse('Too Many Requests', { + status: 429, + headers: { + 'Retry-After': String(Math.ceil((rateLimit.resetAt - now) / 1000)), + }, + }) + } + rateLimit.count++ + } else { + rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) // 1 minute + } + } else { + rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) + } + + return NextResponse.next() +} + +export const rateLimitConfig = { + matcher: '/api/:path*', +} + +// ============================================================================ +// Example 7: Response Modification +// ============================================================================ + +export function modifyResponseProxy(request: NextRequest) { + const response = NextResponse.next() + + // Add security headers + response.headers.set('X-Frame-Options', 'DENY') + response.headers.set('X-Content-Type-Options', 'nosniff') + response.headers.set('Referrer-Policy', 'origin-when-cross-origin') + response.headers.set( + 'Permissions-Policy', + 'camera=(), microphone=(), geolocation=()' + ) + + return response +} + +// ============================================================================ +// Migration Guide: middleware.ts → proxy.ts +// ============================================================================ + +/** + * ❌ BEFORE (Next.js 15): + * + * // File: middleware.ts + * import { NextResponse } from 'next/server' + * import type { NextRequest } from 'next/server' + * + * export function middleware(request: NextRequest) { + * 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 (Next.js 16): + * + * // File: proxy.ts + * import { NextResponse } from 'next/server' + * import type { NextRequest } from 'next/server' + * + * export function proxy(request: NextRequest) { + * 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*', + * } + */ + +/** + * Migration Steps: + * 1. Rename file: middleware.ts → proxy.ts + * 2. Rename function: middleware → proxy + * 3. Keep config object the same + * 4. Logic remains identical + * + * Why the change? + * - proxy.ts runs on Node.js runtime (full Node.js APIs) + * - middleware.ts ran on Edge runtime (limited APIs) + * - proxy.ts makes the network boundary explicit + * + * Note: middleware.ts still works in Next.js 16 but is deprecated. + * Migrate to proxy.ts for future compatibility. + */ + +/** + * Summary: + * + * Proxy patterns: + * 1. ✅ Auth checks and redirects + * 2. ✅ Role-based access control + * 3. ✅ Custom headers + * 4. ✅ Request rewriting (URL rewrites) + * 5. ✅ Geolocation-based routing + * 6. ✅ A/B testing + * 7. ✅ Rate limiting + * 8. ✅ Response modification (security headers) + * + * Best practices: + * - Keep proxy logic lightweight (runs on every request) + * - Use matcher to limit scope + * - Avoid database queries (use cookies/headers instead) + * - Cache rate limit data in memory (or Redis for production) + * - Return NextResponse.next() if no action needed + */ diff --git a/templates/route-handler-api.ts b/templates/route-handler-api.ts new file mode 100644 index 0000000..a5e4fca --- /dev/null +++ b/templates/route-handler-api.ts @@ -0,0 +1,393 @@ +/** + * Next.js 16 - Route Handlers (API Endpoints) + * + * Route Handlers replace API Routes from Pages Router. + * File: app/api/[...]/route.ts + */ + +import { NextResponse } from 'next/server' +import { cookies, headers } from 'next/headers' + +// ============================================================================ +// Example 1: Basic CRUD API +// ============================================================================ + +// GET /api/posts +export async function GET() { + const posts = await fetch('https://api.example.com/posts').then(r => r.json()) + + return NextResponse.json(posts) +} + +// POST /api/posts +export async function POST(request: Request) { + const body = await request.json() + + const post = await fetch('https://api.example.com/posts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).then(r => r.json()) + + return NextResponse.json(post, { status: 201 }) +} + +// ============================================================================ +// Example 2: Dynamic Routes +// ============================================================================ + +// File: app/api/posts/[id]/route.ts + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params // ✅ Await params in Next.js 16 + + const post = await fetch(`https://api.example.com/posts/${id}`) + .then(r => r.json()) + .catch(() => null) + + if (!post) { + return NextResponse.json( + { error: 'Post not found' }, + { status: 404 } + ) + } + + return NextResponse.json(post) +} + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + const body = await request.json() + + const updated = await fetch(`https://api.example.com/posts/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).then(r => r.json()) + + return NextResponse.json(updated) +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + + await fetch(`https://api.example.com/posts/${id}`, { + method: 'DELETE', + }) + + return NextResponse.json({ message: 'Post deleted' }, { status: 200 }) +} + +// ============================================================================ +// Example 3: Search with Query Parameters +// ============================================================================ + +// GET /api/search?q=nextjs&limit=10&page=1 +export async function SEARCH(request: Request) { + const { searchParams } = new URL(request.url) + const query = searchParams.get('q') || '' + const limit = parseInt(searchParams.get('limit') || '10') + const page = parseInt(searchParams.get('page') || '1') + const offset = (page - 1) * limit + + const results = await fetch( + `https://api.example.com/search?q=${query}&limit=${limit}&offset=${offset}` + ).then(r => r.json()) + + return NextResponse.json({ + results: results.items, + total: results.total, + page, + limit, + }) +} + +// ============================================================================ +// Example 4: Authentication with Cookies +// ============================================================================ + +// POST /api/auth/login +export async function LOGIN(request: Request) { + const { email, password } = await request.json() + + // Verify credentials + const user = await fetch('https://api.example.com/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }).then(r => r.json()) + + if (!user.token) { + return NextResponse.json( + { error: 'Invalid credentials' }, + { status: 401 } + ) + } + + // Set cookie + const response = NextResponse.json({ success: true }) + response.cookies.set('token', user.token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 60 * 60 * 24 * 7, // 7 days + }) + + return response +} + +// GET /api/auth/me +export async function ME() { + const cookieStore = await cookies() // ✅ Await cookies in Next.js 16 + const token = cookieStore.get('token')?.value + + if (!token) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + const user = await fetch('https://api.example.com/auth/me', { + headers: { Authorization: `Bearer ${token}` }, + }).then(r => r.json()) + + return NextResponse.json(user) +} + +// ============================================================================ +// Example 5: Webhook Handler +// ============================================================================ + +// File: app/api/webhooks/stripe/route.ts + +import { headers as getHeaders } from 'next/headers' + +export async function WEBHOOK(request: Request) { + const body = await request.text() + const headersList = await getHeaders() // ✅ Await headers in Next.js 16 + const signature = headersList.get('stripe-signature') + + if (!signature) { + return NextResponse.json( + { error: 'Missing signature' }, + { status: 400 } + ) + } + + // Verify webhook signature (example with Stripe) + let event + try { + event = JSON.parse(body) + // In production: stripe.webhooks.constructEvent(body, signature, secret) + } catch (err) { + return NextResponse.json( + { error: 'Invalid payload' }, + { status: 400 } + ) + } + + // 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 + default: + console.log(`Unhandled event type: ${event.type}`) + } + + return NextResponse.json({ received: true }) +} + +async function handlePaymentSuccess(paymentIntent: any) { + console.log('Payment succeeded:', paymentIntent.id) + // Update database, send confirmation email, etc. +} + +async function handlePaymentFailure(paymentIntent: any) { + console.log('Payment failed:', paymentIntent.id) + // Notify user, log error, etc. +} + +// ============================================================================ +// Example 6: Streaming Response +// ============================================================================ + +// GET /api/stream +export async function STREAM() { + const encoder = new TextEncoder() + + const stream = new ReadableStream({ + async start(controller) { + for (let i = 0; i < 10; i++) { + const data = `data: ${JSON.stringify({ count: i })}\n\n` + controller.enqueue(encoder.encode(data)) + await new Promise(resolve => setTimeout(resolve, 1000)) + } + controller.close() + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }) +} + +// ============================================================================ +// Example 7: File Upload +// ============================================================================ + +// POST /api/upload +import { writeFile } from 'fs/promises' +import { join } from 'path' + +export async function UPLOAD(request: Request) { + const formData = await request.formData() + const file = formData.get('file') as File + + if (!file) { + return NextResponse.json( + { error: 'No file provided' }, + { status: 400 } + ) + } + + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + const filename = `${Date.now()}-${file.name}` + const path = join(process.cwd(), 'public', 'uploads', filename) + + await writeFile(path, buffer) + + return NextResponse.json({ + success: true, + url: `/uploads/${filename}`, + }) +} + +// ============================================================================ +// Example 8: CORS Configuration +// ============================================================================ + +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }) +} + +export async function CORS_GET() { + const response = NextResponse.json({ message: 'Hello' }) + + response.headers.set('Access-Control-Allow-Origin', '*') + response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization') + + return response +} + +// ============================================================================ +// Example 9: Error Handling +// ============================================================================ + +export async function ERROR_HANDLING() { + try { + const data = await fetch('https://api.example.com/data') + .then(r => { + if (!r.ok) throw new Error('API request failed') + return r.json() + }) + + return NextResponse.json(data) + } catch (error) { + console.error('Error:', error) + + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ) + } +} + +// ============================================================================ +// Example 10: Rate Limiting +// ============================================================================ + +const rateLimitMap = new Map() + +export async function RATE_LIMITED() { + const headersList = await headers() + const ip = headersList.get('x-forwarded-for') || 'unknown' + const now = Date.now() + + const rateLimit = rateLimitMap.get(ip) + + if (rateLimit) { + if (now < rateLimit.resetAt) { + if (rateLimit.count >= 10) { + return NextResponse.json( + { error: 'Too many requests' }, + { status: 429 } + ) + } + rateLimit.count++ + } else { + rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) + } + } else { + rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) + } + + return NextResponse.json({ message: 'Success' }) +} + +/** + * Summary: + * + * Route Handlers (app/api/*/route.ts): + * 1. ✅ Support all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) + * 2. ✅ Await params in Next.js 16 + * 3. ✅ Access cookies with await cookies() + * 4. ✅ Access headers with await headers() + * 5. ✅ Use NextResponse.json() for JSON responses + * 6. ✅ Return Response or NextResponse + * + * Common patterns: + * - CRUD operations (GET, POST, PATCH, DELETE) + * - Query parameters with searchParams + * - Authentication with cookies + * - Webhooks with signature verification + * - Streaming responses (SSE, WebSocket) + * - File uploads with FormData + * - CORS configuration + * - Error handling + * - Rate limiting + * + * Best practices: + * - Use try/catch for error handling + * - Return appropriate HTTP status codes + * - Validate input data + * - Set secure cookie options in production + * - Add rate limiting for public endpoints + * - Use CORS headers when needed + */ diff --git a/templates/server-actions-form.tsx b/templates/server-actions-form.tsx new file mode 100644 index 0000000..6ff63b6 --- /dev/null +++ b/templates/server-actions-form.tsx @@ -0,0 +1,534 @@ +/** + * Next.js 16 - Server Actions for Form Handling + * + * This template shows comprehensive Server Actions patterns including: + * - Basic form handling + * - Validation with Zod + * - Loading states + * - Error handling + * - Optimistic updates + * - File uploads + */ + +'use server' + +import { z } from 'zod' +import { revalidateTag } from 'next/cache' +import { redirect } from 'next/navigation' + +// ============================================================================ +// Example 1: Basic Server Action +// ============================================================================ + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string + const content = formData.get('content') as string + + // Save to database + await fetch('https://api.example.com/posts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, content }), + }) + + // Revalidate cache + revalidateTag('posts', 'max') + + // Redirect to posts list + redirect('/posts') +} + +// Basic form component +export function CreatePostForm() { + return ( +
+ + +