nextjs — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited nextjs (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Version: Next.js 16.0.0 React Version: 19.2.0 Node.js: 20.9+ Last Verified: 2025-10-24
Use this skill when you need:
"use cache" directive (NEW in Next.js 16)revalidateTag(), updateTag(), refresh() (Updated in Next.js 16)proxy.ts replaces middleware.ts in Next.js 16)params, searchParams, cookies(), headers() now async)useEffectEvent(), React Compiler)next/image with updated defaults in Next.js 16)next/font patterns)Do NOT use this skill for:
cloudflare-nextjs skill insteadclerk-auth, auth-js, or other auth-specific skillscloudflare-d1, drizzle-orm-d1, or database-specific skillstailwind-v4-shadcn skill for Tailwind + shadcn/uizustand-state-management, tanstack-query skillsreact-hook-form-zod skillRelationship with Other Skills:
IMPORTANT: Next.js 16 introduces multiple breaking changes. Read this section carefully if migrating from Next.js 15 or earlier.
Breaking Change: params, searchParams, cookies(), headers(), draftMode() are now async and must be awaited.
Before (Next.js 15):
// ❌ This no longer works in Next.js 16
export default function Page({ params, searchParams }: {
params: { slug: string }
searchParams: { query: string }
}) {
const slug = params.slug // ❌ Error: params is a Promise
const query = searchParams.query // ❌ Error: searchParams is a Promise
return <div>{slug}</div>
}After (Next.js 16):
// ✅ Correct: await params and searchParams
export default async function Page({ params, searchParams }: {
params: Promise<{ slug: string }>
searchParams: Promise<{ query: string }>
}) {
const { slug } = await params // ✅ Await the promise
const { query } = await searchParams // ✅ Await the promise
return <div>{slug}</div>
}Applies to:
params in pages, layouts, route handlerssearchParams in pagescookies() from next/headersheaders() from next/headersdraftMode() from next/headersMigration:
// ❌ 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
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:
middleware.ts → proxy.tsmiddleware → proxymatcher → config.matcher (same syntax)Before (Next.js 15):
// middleware.ts ❌ Deprecated in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/api/:path*',
}After (Next.js 16):
// proxy.ts ✅ New in Next.js 16
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: '/api/:path*',
}Note: middleware.ts still works in Next.js 16 but is deprecated. Migrate to proxy.ts for future compatibility.
See Template: templates/proxy-migration.ts See Reference: references/proxy-vs-middleware.md
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.tsxLayout:
// app/layout.tsx
export default function Layout({
children,
auth,
dashboard,
}: {
children: React.ReactNode
auth: React.ReactNode
dashboard: React.ReactNode
}) {
return (
<html>
<body>
{auth}
{dashboard}
{children}
</body>
</html>
)
}Default Fallback (REQUIRED):
// app/@auth/default.tsx
export default function AuthDefault() {
return null // or <Skeleton /> or redirect
}
// app/@dashboard/default.tsx
export default function DashboardDefault() {
return null
}Why Required: Next.js 16 changed how parallel routes handle soft navigation. Without default.js, unmatched slots will error during client-side navigation.
See Template: templates/parallel-routes-with-default.tsx
The following features are REMOVED in Next.js 16:
"use cache" directive.Migration:
npx eslint . or npx biome lint . directly.serverRuntimeConfig with process.env.VARIABLE.experimental.ppr to "use cache" directive (see Cache Components section).Next.js 16 requires:
Check Versions:
node --version # Should be 20.9+
npm --version # Should be 10+
npx next --version # Should be 16.0.0+Upgrade Node.js:
# Using nvm
nvm install 20
nvm use 20
nvm alias default 20
# Using Homebrew (macOS)
brew install node@20
# Using apt (Ubuntu/Debian)
sudo apt update
sudo apt install nodejs npmNext.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:
Override Defaults (if needed):
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
minimumCacheTTL: 60, // Revert to 60 seconds
deviceSizes: [640, 750, 828, 1080, 1200, 1920], // Add larger sizes
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Restore old sizes
formats: ['image/webp'], // Default
},
}
export default configSee Template: templates/image-optimization.tsx
NEW in Next.js 16: Cache Components introduce opt-in caching with the "use cache" directive, replacing implicit caching from Next.js 15.
What Changed:
"use cache" directiveWhy the Change: Explicit caching gives developers more control and makes caching behavior predictable.
Cache Components enable:
"use cache" DirectiveSyntax: Add "use cache" at the top of a Server Component, function, or route handler.
Component-level caching:
// app/components/expensive-component.tsx
'use cache'
export async function ExpensiveComponent() {
const data = await fetch('https://api.example.com/data')
const json = await data.json()
return (
<div>
<h1>{json.title}</h1>
<p>{json.description}</p>
</div>
)
}Function-level caching:
// lib/data.ts
'use cache'
export async function getExpensiveData(id: string) {
const response = await fetch(`https://api.example.com/items/${id}`)
return response.json()
}
// Usage in component
import { getExpensiveData } from '@/lib/data'
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getExpensiveData(id) // Cached
return <div>{product.name}</div>
}Page-level caching:
// app/blog/[slug]/page.tsx
'use cache'
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return posts.map((post: { slug: string }) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}See Template: templates/cache-component-use-cache.tsx
PPR allows caching static parts of a page while rendering dynamic parts on-demand.
Pattern:
// app/dashboard/page.tsx
// Static header (cached)
'use cache'
async function StaticHeader() {
return <header>My App</header>
}
// Dynamic user info (not cached)
async function DynamicUserInfo() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const user = await fetch(`/api/users/${userId}`).then(r => r.json())
return <div>Welcome, {user.name}</div>
}
// Page combines both
export default function Dashboard() {
return (
<div>
<StaticHeader /> {/* Cached */}
<DynamicUserInfo /> {/* Dynamic */}
</div>
)
}When to Use PPR:
See Reference: references/cache-components-guide.md
revalidateTag() - Updated APIBREAKING CHANGE: revalidateTag() now requires a second argument (cacheLife profile) for stale-while-revalidate behavior.
Before (Next.js 15):
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string) {
await fetch(`/api/posts/${id}`, { method: 'PATCH' })
revalidateTag('posts') // ❌ Only one argument in Next.js 15
}After (Next.js 16):
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string) {
await fetch(`/api/posts/${id}`, { method: 'PATCH' })
revalidateTag('posts', 'max') // ✅ Second argument required in Next.js 16
}Built-in Cache Life Profiles:
'max' - Maximum staleness (recommended for most use cases)'hours' - Stale after hours'days' - Stale after days'weeks' - Stale after weeks'default' - Default cache behaviorCustom Cache Life Profile:
revalidateTag('posts', {
stale: 3600, // Stale after 1 hour (seconds)
revalidate: 86400, // Revalidate every 24 hours (seconds)
expire: false, // Never expire (optional)
})Pattern in Server Actions:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
})
revalidateTag('posts', 'max') // ✅ Revalidate with max staleness
}See Template: templates/revalidate-tag-cache-life.ts
updateTag() - NEW API (Server Actions Only)NEW in Next.js 16: updateTag() provides read-your-writes semantics for Server Actions.
What it does:
Difference from `revalidateTag()`:
revalidateTag(): Stale-while-revalidate (shows stale data, revalidates in background)updateTag(): Immediate refresh (expires cache, fetches fresh data in same request)Use Case: Forms, user settings, or any mutation where user expects immediate feedback.
Pattern:
'use server'
import { updateTag } from 'next/cache'
export async function updateUserProfile(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
// Update database
await db.users.update({ name, email })
// Immediately refresh cache (read-your-writes)
updateTag('user-profile')
// User sees updated data immediately (no stale data)
}When to Use:
See Template: templates/server-action-update-tag.ts
refresh() - NEW API (Server Actions Only)NEW in Next.js 16: refresh() refreshes uncached data only (complements client-side router.refresh()).
When to Use:
router.refresh() on server sidePattern:
'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 datarevalidateTag(): Revalidates specific tagged data (stale-while-revalidate)updateTag(): Immediately expires and refreshes specific tagged dataSee Reference: references/cache-components-guide.md
Server Components are React components that render on the server. They enable efficient data fetching, reduce client bundle size, and improve performance.
Default Behavior: All components in the App Router are Server Components by default (unless marked with 'use client').
Example:
// app/posts/page.tsx (Server Component by default)
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
{posts.map((post: { id: string; title: string }) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}Rules:
await promises directly in component bodycookies(), headers(), draftMode() (with await)useState, useEffect, etc.)onClick, onChange, etc.)Pattern: Use async/await directly in component body.
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
// Fetch data directly in component
const product = await fetch(`https://api.example.com/products/${id}`)
.then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</div>
)
}Parallel Data Fetching:
export default async function Dashboard() {
// Fetch in parallel with Promise.all
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
])
return (
<div>
<UserInfo user={user} />
<PostsList posts={posts} />
<CommentsList comments={comments} />
</div>
)
}Sequential Data Fetching (when needed):
export default async function UserPosts({ params }: { params: Promise<{ userId: string }> }) {
const { userId } = await params
// Fetch user first
const user = await fetch(`/api/users/${userId}`).then(r => r.json())
// Then fetch user's posts (depends on user data)
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json())
return (
<div>
<h1>{user.name}'s Posts</h1>
<ul>
{posts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}See Template: templates/server-component-streaming.tsx
Pattern: Wrap slow components in <Suspense> to stream content as it loads.
import { Suspense } from 'react'
// Fast component (loads immediately)
async function Header() {
return <header>My App</header>
}
// Slow component (takes 2 seconds)
async function SlowData() {
await new Promise(resolve => setTimeout(resolve, 2000))
const data = await fetch('/api/slow-data').then(r => r.json())
return <div>{data.content}</div>
}
// Page streams content
export default function Page() {
return (
<div>
<Header /> {/* Loads immediately */}
<Suspense fallback={<div>Loading...</div>}>
<SlowData /> {/* Streams when ready */}
</Suspense>
</div>
)
}When to Use Streaming:
See Reference: references/server-components-patterns.md
When to Use Server Components (default):
When to Use Client Components ('use client'):
useState, useEffect, useContext)onClick, onChange, onSubmit)window, localStorage, navigator)Pattern: Use Server Components by default, add 'use client' only when needed.
// app/components/interactive-button.tsx
'use client' // Client Component
import { useState } from 'react'
export function InteractiveButton() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}
// app/page.tsx (Server Component)
import { InteractiveButton } from './components/interactive-button'
export default async function Page() {
const data = await fetch('/api/data').then(r => r.json())
return (
<div>
<h1>{data.title}</h1>
<InteractiveButton /> {/* Client Component inside Server Component */}
</div>
)
}Composition Rules:
See Reference: references/server-components-patterns.md
Server Actions are asynchronous functions that run on the server. They enable server-side mutations, form handling, and data revalidation.
Definition: Add 'use server' directive to create a Server Action.
File-level Server Actions:
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Mutate database
await db.posts.create({ title, content })
// Revalidate cache
revalidateTag('posts', 'max')
}Inline Server Actions:
// app/posts/new/page.tsx
export default function NewPostPage() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.posts.create({ title, content })
revalidateTag('posts', 'max')
}
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Create Post</button>
</form>
)
}See Template: templates/server-actions-form.tsx
Basic Form:
// app/components/create-post-form.tsx
import { createPost } from '@/app/actions'
export function CreatePostForm() {
return (
<form action={createPost}>
<label>
Title:
<input type="text" name="title" required />
</label>
<label>
Content:
<textarea name="content" required />
</label>
<button type="submit">Create Post</button>
</form>
)
}With Loading State (useFormStatus):
'use client'
import { useFormStatus } from 'react-dom'
import { createPost } from '@/app/actions'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
export function CreatePostForm() {
return (
<form action={createPost}>
<input type="text" name="title" required />
<textarea name="content" required />
<SubmitButton />
</form>
)
}With Validation:
// app/actions.ts
'use server'
import { z } from 'zod'
import { redirect } from 'next/navigation'
const PostSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
content: z.string().min(10, 'Content must be at least 10 characters'),
})
export async function createPost(formData: FormData) {
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
}
// Validate
const parsed = PostSchema.safeParse(rawData)
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Mutate
await db.posts.create(parsed.data)
// Revalidate and redirect
revalidateTag('posts', 'max')
redirect('/posts')
}See Template: templates/server-actions-form.tsx See Reference: references/server-actions-guide.md
Pattern: Return error objects from Server Actions, handle in Client Components.
Server Action:
// app/actions.ts
'use server'
export async function deletePost(id: string) {
try {
await db.posts.delete({ where: { id } })
revalidateTag('posts', 'max')
return { success: true }
} catch (error) {
return {
success: false,
error: 'Failed to delete post. Please try again.'
}
}
}Client Component:
'use client'
import { useState } from 'react'
import { deletePost } from '@/app/actions'
export function DeleteButton({ postId }: { postId: string }) {
const [error, setError] = useState<string | null>(null)
async function handleDelete() {
const result = await deletePost(postId)
if (!result.success) {
setError(result.error)
}
}
return (
<div>
<button onClick={handleDelete}>Delete Post</button>
{error && <p className="error">{error}</p>}
</div>
)
}Pattern: Show UI update immediately, then sync with server.
'use client'
import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
)
async function handleLike() {
// Update UI immediately
addOptimisticLike(1)
// Sync with server
await likePost(postId)
}
return (
<button onClick={handleLike}>
❤️ {optimisticLikes} likes
</button>
)
}See Reference: references/server-actions-guide.md
Route Handlers are server-side API endpoints in the App Router. They replace API Routes from the Pages Router.
File: app/api/hello/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello, World!' })
}
export async function POST(request: Request) {
const body = await request.json()
return NextResponse.json({
message: 'Post created',
data: body
})
}Supported Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
See Template: templates/route-handler-api.ts
File: app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Await params in Next.js 16
const post = await db.posts.findUnique({ where: { id } })
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
await db.posts.delete({ where: { id } })
return NextResponse.json({ message: 'Post deleted' })
}URL: /api/posts?tag=javascript&limit=10
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const tag = searchParams.get('tag')
const limit = parseInt(searchParams.get('limit') || '10')
const posts = await db.posts.findMany({
where: { tags: { has: tag } },
take: limit,
})
return NextResponse.json(posts)
}Pattern: Handle incoming webhooks from third-party services.
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
export async function POST(request: Request) {
const body = await request.text()
const headersList = await headers() // ✅ Await headers in Next.js 16
const signature = headersList.get('stripe-signature')
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
signature!,
process.env.STRIPE_WEBHOOK_SECRET!
)
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object)
break
case 'payment_intent.failed':
await handlePaymentFailure(event.data.object)
break
}
return NextResponse.json({ received: true })
}See Template: templates/route-handler-api.ts See Reference: references/route-handlers-reference.md
Next.js 16 introduces `proxy.ts` to replace middleware.ts.
The new proxy.ts makes the network boundary explicit and provides more flexibility.
Before (middleware.ts):
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check auth
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}After (proxy.ts):
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
// Check auth
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}See Template: templates/proxy-migration.ts See Reference: references/proxy-vs-middleware.md
Use Case: Render multiple pages in the same layout (e.g., modal + main content, dashboard panels).
Structure:
app/
├── @modal/
│ ├── login/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
├── @feed/
│ ├── trending/
│ │ └── page.tsx
│ └── default.tsx ← REQUIRED in Next.js 16
└── layout.tsxLayout:
// app/layout.tsx
export default function Layout({
children,
modal,
feed,
}: {
children: React.ReactNode
modal: React.ReactNode
feed: React.ReactNode
}) {
return (
<html>
<body>
{modal}
<main>
{children}
<aside>{feed}</aside>
</main>
</body>
</html>
)
}Default Files (REQUIRED):
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}
// app/@feed/default.tsx
export default function FeedDefault() {
return <div>Default Feed</div>
}See Template: templates/parallel-routes-with-default.tsx
Use Case: Organize routes without affecting URL structure.
Structure:
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about
│ └── contact/
│ └── page.tsx → /contact
├── (shop)/
│ ├── products/
│ │ └── page.tsx → /products
│ └── cart/
│ └── page.tsx → /cart
└── layout.tsxDifferent Layouts per Group:
app/
├── (marketing)/
│ ├── layout.tsx ← Marketing layout
│ └── about/page.tsx
├── (shop)/
│ ├── layout.tsx ← Shop layout
│ └── products/page.tsx
└── layout.tsx ← Root layoutSee Reference: references/app-router-fundamentals.md
Next.js 16 integrates React 19.2, which includes new features from React Canary.
Use Case: Smooth animations between page transitions.
'use client'
import { useRouter } from 'next/navigation'
import { startTransition } from 'react'
export function NavigationLink({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter()
function handleClick(e: React.MouseEvent) {
e.preventDefault()
// Wrap navigation in startTransition for View Transitions
startTransition(() => {
router.push(href)
})
}
return <a href={href} onClick={handleClick}>{children}</a>
}With CSS View Transitions API:
/* app/globals.css */
@view-transition {
navigation: auto;
}
/* Animate elements with view-transition-name */
.page-title {
view-transition-name: page-title;
}See Template: templates/view-transitions-react-19.tsx
useEffectEvent() (Experimental)Use Case: Extract non-reactive logic from useEffect.
'use client'
import { useEffect, experimental_useEffectEvent as useEffectEvent } from 'react'
export function ChatRoom({ roomId }: { roomId: string }) {
const onConnected = useEffectEvent(() => {
console.log('Connected to room:', roomId)
})
useEffect(() => {
const connection = connectToRoom(roomId)
onConnected() // Non-reactive callback
return () => connection.disconnect()
}, [roomId]) // Only re-run when roomId changes
return <div>Chat Room {roomId}</div>
}Why Use It: Prevents unnecessary useEffect re-runs when callback dependencies change.
Use Case: Automatic memoization without useMemo, useCallback.
Enable in next.config.ts:
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
reactCompiler: true,
},
}
export default configInstall Plugin:
npm install babel-plugin-react-compilerExample (no manual memoization needed):
'use client'
export function ExpensiveList({ items }: { items: string[] }) {
// React Compiler automatically memoizes this
const filteredItems = items.filter(item => item.length > 3)
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)
}See Reference: references/react-19-integration.md
The Metadata API provides type-safe SEO and social sharing metadata.
Pattern: Export metadata object from page or layout.
// app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'Welcome to my app',
openGraph: {
title: 'My App',
description: 'Welcome to my app',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'Welcome to my app',
images: ['/twitter-image.jpg'],
},
}
export default function Page() {
return <h1>Home</h1>
}Pattern: Export generateMetadata async function.
// app/posts/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return <article>{post.content}</article>
}File: app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetch('/api/posts').then(r => r.json())
const postUrls = posts.map((post: { id: string; updatedAt: string }) => ({
url: `https://example.com/posts/${post.id}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}))
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postUrls,
]
}See Template: templates/metadata-config.ts See Reference: references/metadata-api-guide.md
Basic Usage:
import Image from 'next/image'
export function MyImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load above the fold
/>
)
}Responsive Images:
<Image
src="/hero.jpg"
alt="Hero"
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, 50vw"
/>Remote Images (configure in next.config.ts):
import type { NextConfig } from 'next'
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
},
}
export default configSee Template: templates/image-optimization.tsx
Google Fonts:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}Local Fonts:
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-my-font',
})See Template: templates/font-optimization.tsx
Component Lazy Loading:
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./heavy-component'), {
loading: () => <div>Loading...</div>,
ssr: false, // Disable SSR for client-only components
})
export default function Page() {
return (
<div>
<h1>My Page</h1>
<HeavyComponent />
</div>
)
}Conditional Loading:
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./chart'), { ssr: false })
export function Dashboard() {
const [showChart, setShowChart] = useState(false)
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && <Chart />}
</div>
)
}Route-level Splitting (automatic):
app/
├── page.tsx → Bundles: /, shared
├── about/
│ └── page.tsx → Bundles: /about, shared
└── products/
└── page.tsx → Bundles: /products, sharedComponent-level Splitting (with dynamic import):
const Analytics = dynamic(() => import('./analytics'))
const Comments = dynamic(() => import('./comments'))
export default function BlogPost() {
return (
<article>
<h1>Post Title</h1>
<p>Content...</p>
<Analytics /> {/* Separate bundle */}
<Comments /> {/* Separate bundle */}
</article>
)
}Default: Turbopack is now the default bundler in Next.js 16.
Metrics:
Opt-out (if needed):
npm run build -- --webpackEnable File System Caching (beta):
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
experimental: {
turbopack: {
fileSystemCaching: true, // Beta: Persist cache between runs
},
},
}
export default configSee Reference: references/performance-optimization.md
Enable strict mode in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}Configure in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./app/*"],
"@/components/*": ["./app/components/*"],
"@/lib/*": ["./lib/*"],
"@/styles/*": ["./styles/*"]
}
}
}Usage:
// Instead of: import { Button } from '../../../components/button'
import { Button } from '@/components/button'Generate types from routes:
npm run buildUsage:
import { useRouter } from 'next/navigation'
const router = useRouter()
// Type-safe routing
router.push('/posts/123') // ✅ Valid route
router.push('/invalid') // ❌ Type error if route doesn't existSee Reference: references/typescript-configuration.md
params is a PromiseError:
Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }'Cause: Next.js 16 changed params to async.
Solution: Await params:
// ❌ Before
export default function Page({ params }: { params: { id: string } }) {
const id = params.id
}
// ✅ After
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}searchParams is a PromiseError:
Property 'query' does not exist on type 'Promise<{ query: string }>'Cause: searchParams is now async in Next.js 16.
Solution:
// ❌ Before
export default function Page({ searchParams }: { searchParams: { query: string } }) {
const query = searchParams.query
}
// ✅ After
export default async function Page({ searchParams }: { searchParams: Promise<{ query: string }> }) {
const { query } = await searchParams
}cookies() requires awaitError:
'cookies' implicitly has return type 'any'Cause: cookies() is now async in Next.js 16.
Solution:
// ❌ Before
import { cookies } from 'next/headers'
export function MyComponent() {
const cookieStore = cookies()
}
// ✅ After
import { cookies } from 'next/headers'
export async function MyComponent() {
const cookieStore = await cookies()
}default.jsError:
Error: Parallel route @modal/login was matched but no default.js was foundCause: Next.js 16 requires default.js for all parallel routes.
Solution: Add default.tsx files:
// app/@modal/default.tsx
export default function ModalDefault() {
return null
}revalidateTag() requires 2 argumentsError:
Expected 2 arguments, but got 1Cause: revalidateTag() now requires a cacheLife argument in Next.js 16.
Solution:
// ❌ Before
revalidateTag('posts')
// ✅ After
revalidateTag('posts', 'max')Error:
You're importing a component that needs useState. It only works in a Client ComponentCause: Using React hooks in Server Component.
Solution: Add 'use client' directive:
// ✅ Add 'use client' at the top
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}middleware.ts is deprecatedWarning:
Warning: middleware.ts is deprecated. Use proxy.ts instead.Solution: Migrate to proxy.ts:
// Rename: middleware.ts → proxy.ts
// Rename function: middleware → proxy
export function proxy(request: NextRequest) {
// Same logic
}Error:
Error: Failed to compile with TurbopackCause: Turbopack is now default in Next.js 16.
Solution: Opt out of Turbopack if incompatible:
npm run build -- --webpacknext/image srcError:
Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured under images in your `next.config.js`Solution: Add remote patterns in next.config.ts:
const config: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
}Error:
You're importing a Server Component into a Client ComponentSolution: Pass Server Component as children:
// ❌ Wrong
'use client'
import { ServerComponent } from './server-component' // Error
export function ClientComponent() {
return <ServerComponent />
}
// ✅ Correct
'use client'
export function ClientComponent({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
// Usage
<ClientComponent>
<ServerComponent /> {/* Pass as children */}
</ClientComponent>generateStaticParams not workingCause: generateStaticParams only works with static generation (export const dynamic = 'force-static').
Solution:
export const dynamic = 'force-static'
export async function generateStaticParams() {
const posts = await fetch('/api/posts').then(r => r.json())
return posts.map((post: { id: string }) => ({ id: post.id }))
}fetch() not cachingCause: Next.js 16 uses opt-in caching with "use cache" directive.
Solution: Add "use cache" to component or function:
'use cache'
export async function getPosts() {
const response = await fetch('/api/posts')
return response.json()
}Error:
Error: Conflicting routes: /about and /(marketing)/aboutCause: 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-infoCause: Using dynamic metadata without generateMetadata().
Solution: Use generateMetadata() for dynamic pages:
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
}
}next/font font not loadingCause: Font variable not applied to HTML element.
Solution: Apply font variable to <html> or <body>:
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html className={inter.variable}> {/* ✅ Apply variable */}
<body>{children}</body>
</html>
)
}Cause: Server-only env vars are not exposed to browser.
Solution: Prefix with NEXT_PUBLIC_ for client-side access:
# .env
SECRET_KEY=abc123 # Server-only
NEXT_PUBLIC_API_URL=https://api # Available in browser// Server Component (both work)
const secret = process.env.SECRET_KEY
const apiUrl = process.env.NEXT_PUBLIC_API_URL
// Client Component (only public vars work)
const apiUrl = process.env.NEXT_PUBLIC_API_URLError:
Error: Could not find Server ActionCause: Missing 'use server' directive.
Solution: Add 'use server':
// ❌ Before
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}
// ✅ After
'use server'
export async function createPost(formData: FormData) {
await db.posts.create({ ... })
}Cause: Incorrect baseUrl or paths in tsconfig.json.
Solution: Configure correctly:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/components/*": ["./app/components/*"]
}
}
}See Reference: references/top-errors.md
The following templates are available in templates/:
App Router Fundamentals:
app-router-async-params.tsx - Async params, searchParams patterns (Next.js 16)parallel-routes-with-default.tsx - Parallel routes with required default.jsroute-groups-example.tsx - Route groups organizationCache Components (Next.js 16):
cache-component-use-cache.tsx - Cache Components with "use cache"partial-prerendering.tsx - PPR with static + dynamic partsrevalidate-tag-cache-life.ts - Updated revalidateTag() APIserver-action-update-tag.ts - updateTag() for read-your-writesServer Components:
server-component-data-fetching.tsx - Data fetching patternsserver-component-streaming.tsx - Streaming with Suspenseserver-component-composition.tsx - Server + Client component compositionServer Actions:
server-actions-form.tsx - Form handling with Server Actionsserver-actions-validation.ts - Server Action validation with Zodserver-actions-optimistic.tsx - Optimistic updatesRoute Handlers:
route-handler-api.ts - Basic CRUD APIroute-handler-webhook.ts - Webhook handlingroute-handler-streaming.ts - Streaming responsesProxy & Middleware:
proxy-migration.ts - Migrate from middleware.ts to proxy.tsproxy-auth.ts - Auth with proxy.tsReact 19.2:
view-transitions-react-19.tsx - View Transitions APIuse-effect-event.tsx - useEffectEvent() patternreact-compiler-example.tsx - React Compiler usageMetadata:
metadata-config.ts - Static and dynamic metadatasitemap.ts - Sitemap generationrobots.ts - robots.txt generationOptimization:
image-optimization.tsx - next/image patternsfont-optimization.tsx - next/font patternslazy-loading.tsx - Dynamic imports and lazy loadingcode-splitting.tsx - Code splitting strategiesTypeScript:
typescript-config.json - Recommended TypeScript configurationpath-aliases.tsx - Path alias usageConfiguration:
next.config.ts - Full Next.js configurationpackage.json - Recommended dependencies for Next.js 16Bundled References (in references/):
next-16-migration-guide.md - Complete migration from Next.js 15 to 16cache-components-guide.md - Cache Components deep diveproxy-vs-middleware.md - Proxy.ts vs middleware.ts comparisonasync-route-params.md - Async params, searchParams, cookies(), headers()app-router-fundamentals.md - App Router concepts and patternsserver-components-patterns.md - Server Components best practicesserver-actions-guide.md - Server Actions patterns and validationroute-handlers-reference.md - Route Handlers API referencemetadata-api-guide.md - Metadata API complete guideperformance-optimization.md - Performance patterns and Turbopackreact-19-integration.md - React 19.2 features in Next.jstop-errors.md - 18+ common errors and solutionsScripts:
scripts/check-versions.sh - Verify Next.js and dependency versionsExternal Documentation:
/websites/nextjs for latest Next.js reference| Package | Minimum Version | Recommended |
|---|---|---|
| Next.js | 16.0.0 | 16.0.0+ |
| React | 19.2.0 | 19.2.0+ |
| Node.js | 20.9.0 | 20.9.0+ |
| TypeScript | 5.1.0 | 5.7.0+ |
| Turbopack | (built-in) | Stable |
Check Versions:
./scripts/check-versions.shEstimated Token Savings: 65-70%
Without Skill (manual setup from docs):
With Skill:
Errors Prevented: 18+ common mistakes = 100% error prevention
Last Verified: 2025-10-24 Next Review: 2026-01-24 (Quarterly) Maintainer: Jezweb | [email protected] Repository: https://github.com/jezweb/claude-skills
Update Triggers:
Version Check:
cd skills/nextjs
./scripts/check-versions.shEnd of SKILL.md
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.