
Nextjs Development
- 9 installs
- 7 repo stars
- Updated August 2, 2026
- practicalswan/agent-skills
nextjs-development is a Claude Code skill for ai & agent building.
About
nextjs-development is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- nextjs-development
- AI & Agent Building
- AI-coding skill
Nextjs Development by the numbers
- 9 all-time installs (skills.sh)
- Ranked #12,133 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/practicalswan/agent-skills --skill nextjs-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 7 |
| Last updated | August 2, 2026 |
| Repository | practicalswan/agent-skills ↗ |
How do I helps with ai & agent building tasks.?
Helps with ai & agent building tasks.
Who is it for?
Best when you're working on ai & agent building and need structured help with nextjs development.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks., or when nextjs-development is a claude code skill for ai & agent building.
What you get
Structured output aligned to nextjs-development: nextjs-development, AI & Agent Building.
Files
Next.js Development
Optimized for Next.js 16+, React 19+, TypeScript 5.5+, Turbopack, and App Router-first architectures.
Comprehensive reference for Next.js (latest: 16.2.4) with the App Router, TypeScript, and modern patterns. Covers project structure, Server/Client Components, data fetching, caching with the use cache directive, Server Actions, MCP devtools integration, and performance optimization.
- Leverage native parallel subagent dispatch and 200k+ context windows where available.
Component Review Rubric Reference
Apply the shared Component Review Rubric before approving Next.js components, then run the Next.js-specific checks below.
Anti-Patterns
- Mixing server and client responsibilities: Bundle size, caching, and auth decisions become harder to reason about.
- Using legacy synchronous request APIs: Modern Next.js expects async request surfaces such as
params,headers(), andcookies(). - Skipping route-level loading and error states: Streaming apps feel broken when only the happy path is implemented.
Verification Protocol
Before claiming "skill applied successfully":
1. Pass/fail: The Nextjs Development guidance is tied to a concrete route, component, screen, or design artifact. 2. Pass/fail: Component states cover loading, empty, error, success, and responsive breakpoints where applicable. 3. Pass/fail: Accessibility, visual hierarchy, and interaction behavior are reviewed against the shared component rubric. 4. Pressure-test scenario: Review the component on a narrow mobile viewport, keyboard-only path, and slow-loading state. 5. Success metric: Zero generic UI approval; every approval cites rendered behavior or source evidence.
Before and After Example
// Before
export default function ProductPage({ params }: { params: { id: string } }) {
const [product, setProduct] = useState<Product | null>(null);
useEffect(() => {
fetch(`/api/products/${params.id}`).then((r) => r.json()).then(setProduct);
}, [params.id]);
return product ? <ProductView product={product} /> : <Spinner />;
}
// After
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params;
const product = await getProduct(id);
return <ProductView product={product} />;
}Moves data fetching into the server component and follows the async request API used in current Next.js releases.
Activation Conditions
Use symptom -> action triggers: when one matches, apply this skill and verify with the protocol below.
App Router & Routing
- Creating or modifying
page.tsx,layout.tsx,loading.tsx,error.tsx,not-found.tsx - Working with dynamic routes
[slug], catch-all[...slug], optional catch-all[[...slug]] - Parallel routes
@slot, intercepting routes, route groups(group) - New v15/v16 file conventions:
forbidden.tsx,proxy.ts,template.tsx,unauthorized.tsx
Server & Client Components
- Deciding when to use
"use client"or"use server"directives - Component boundary questions, RSC + RCC composition patterns
- Passing Server Components as children/props to Client Components
taintAPI for data security
Data Fetching & Caching
- Using
use cachedirective (replacescache: 'force-cache') cacheTag(),cacheLife(),revalidateTag(),revalidatePath()- Async Request APIs:
await cookies(),await headers(),await params,await searchParams after()for post-response work,connection()for dynamic rendering
Server Actions & Forms
"use server"in functions or module scope<Form>component with client-side navigation- Form validation, optimistic updates, error handling
after()for side-effects after action completes
Performance & Turbopack
next devwith Turbopack (default in v15+, stable)- Image optimization with
next/image - Font subsetting with
next/font - Lazy loading, bundle optimization,
serverComponentsHmrCache
Next.js MCP Dev Tools
- Querying live errors, logs, routes from the running dev server
- Using
next-devtools-mcpwith coding agents (requires Next.js 16+) - Upgrading to Next.js 16 with codemods
- Enabling Cache Components feature
---
Part 1: Project Setup & Config
Creating a New Project
npx create-next-app@latest my-app --typescript --tailwind --eslint --appTypeScript Config (next.config.ts)
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true, // stable in v16
reactStrictMode: true,
serverExternalPackages: ['sharp'], // renamed from serverComponentsExternalPackages in v15
experimental: {
turbopackFileSystemCache: true, // persist Turbopack cache across restarts
serverComponentsHmrCache: true, // cache fetch responses during HMR
},
cacheLife: { // custom cache profiles
frequent: { stale: 60, revalidate: 60, expire: 3600 },
daily: { stale: 3600, revalidate: 3600, expire: 86400 },
},
}
export default nextConfigProject Structure
my-app/
├── app/
│ ├── layout.tsx # Root layout (required)
│ ├── page.tsx # Home page
│ ├── loading.tsx # Streaming skeleton
│ ├── error.tsx # Error boundary
│ ├── not-found.tsx # 404 page
│ ├── forbidden.tsx # 403 page (v16)
│ ├── unauthorized.tsx # 401 page (v16)
│ ├── (marketing)/ # Route group (no URL segment)
│ │ └── about/page.tsx
│ ├── blog/
│ │ └── [slug]/page.tsx # Dynamic route
│ └── api/
│ └── route.ts # Route Handler
├── components/ # Shared RSC/RCC components
├── lib/ # Server utilities
├── public/ # Static assets
├── next.config.ts # TypeScript config (v15+)
├── .mcp.json # MCP server config (v16)
└── instrumentation.ts # Server lifecycle hooks (stable v15)---
Part 2: App Router Routing
File Conventions
| File | Purpose |
|---|---|
page.tsx | UI for the route segment, makes it publicly accessible |
layout.tsx | Shared UI that persists across navigations |
template.tsx | Like layout, but remounts on navigation |
loading.tsx | Suspense skeleton; shown while page loads |
error.tsx | Isolate errors; "use client" required |
not-found.tsx | Rendered by notFound() or 404 |
forbidden.tsx | Rendered by forbidden() (v16) |
unauthorized.tsx | Rendered by unauthorized() (v16) |
route.ts | API endpoint (cannot coexist with page.tsx at same level) |
proxy.ts | Lightweight HTTP proxy (v16) |
middleware.ts | Runs before request completes (project root) |
instrumentation.ts | Server lifecycle, OpenTelemetry (stable v15) |
instrumentation-client.ts | Client-side performance monitoring (v16) |
Dynamic Routes
// app/blog/[slug]/page.tsx
// IMPORTANT: params is now async in Next.js 15+
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params // must await in v15+
return <h1>{slug}</h1>
}
// Generate static paths
export async function generateStaticParams() {
const posts = await fetchPosts()
return posts.map((post) => ({ slug: post.slug }))
}Route Groups & Parallel Routes
app/
├── (auth)/ # Route group: no URL impact
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
├── @modal/ # Parallel route (slot)
│ └── photo/[id]/page.tsx
├── layout.tsx # Receives { children, modal } props
└── page.tsxIntercepting Routes
app/
├── photos/[id]/page.tsx # Full page: /photos/123
└── @modal/
└── (.)photos/[id]/ # Intercept same-level route
└── page.tsx # Renders as modal without full navigationsearchParams (async in v15+)
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q: string; page: string }>
}) {
const { q, page } = await searchParams // must await in v15+
return <Results query={q} page={Number(page)} />
}---
Part 3: Server & Client Components
Decision Tree
Does the component need:
- onClick, onChange, event handlers? → "use client"
- useState, useEffect, useReducer? → "use client"
- Browser-only APIs (window, localStorage)? → "use client"
- useRouter, useParams, useSearchParams? → "use client"
Otherwise:
- Direct DB/API access without extra fetch? → Server Component (default)
- Large dependencies (marked-js, date-fns)? → Server Component (not in JS bundle)
- Access cookies(), headers(), auth tokens? → Server ComponentComponent Composition Pattern
// ✅ Pass Server Components as children to Client Components
// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper'
import { ServerData } from '@/components/ServerData'
export default function Page() {
return (
<ClientWrapper>
<ServerData /> {/* Server Component as child — no "use client" boundary issue */}
</ClientWrapper>
)
}
// components/ClientWrapper.tsx
"use client"
import { useState } from 'react'
export function ClientWrapper({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return <div onClick={() => setOpen(!open)}>{children}</div>
}Directives Reference
| Directive | Where | Effect |
|---|---|---|
"use client" | Top of file | All exports are Client Components |
"use server" | Top of file or function | Marks Server Actions; top-of-file applies to all exports |
"use cache" | Top of file or function | Marks a component/function as a Cache Component |
"use cache: private" | Top of file or function | Cache Component, private (user-specific) data |
"use cache: remote" | Top of file or function | Cache Component, persisted remotely |
Data Security with taint
// next.config.ts
const nextConfig: NextConfig = {
experimental: { taint: true }
}
// lib/user.ts (Server)
import { experimental_taintUniqueValue } from 'react'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
// Prevent accidental serialization of sensitive fields
experimental_taintUniqueValue(
'Do not pass user.passwordHash to Client',
user,
user.passwordHash
)
return user
}---
Part 4: Data Fetching & Caching
The use cache Directive (Next.js 15+)
use cache replaces the old cache: 'force-cache' approach and works at the file, component, or function level.
// Cache an entire async function
async function getProducts() {
'use cache'
const data = await fetch('https://api.example.com/products')
return data.json()
}
// Cache a Server Component
async function ProductList() {
'use cache'
cacheLife('daily') // use named profile from next.config.ts
cacheTag('products') // tag for targeted revalidation
const products = await getProducts()
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}cacheLife Profiles
// Built-in profiles
cacheLife('seconds') // stale: 0, revalidate: 1, expire: 60
cacheLife('minutes') // stale: 60, revalidate: 60, expire: 3600
cacheLife('hours') // stale: 3600, revalidate: 3600, expire: 86400
cacheLife('days') // stale: 86400, revalidate: 86400, expire: 604800
cacheLife('weeks') // stale: 604800, revalidate: 604800, expire: 2592000
cacheLife('max') // stale: 2592000, revalidate: 2592000, expire: Infinity
// Custom profile (defined in next.config.ts)
cacheLife('frequent') // stale: 60, revalidate: 60, expire: 3600Targeted Revalidation with cacheTag
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data: Object.fromEntries(data) })
revalidateTag('products') // invalidates all cached items with this tag
revalidateTag(`product-${id}`) // fine-grained invalidation
}
// app/products/[id]/page.tsx
async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
'use cache'
const { id } = await params
cacheTag('products', `product-${id}`)
const product = await db.products.findUnique({ where: { id } })
return <Product data={product} />
}fetch Cache Behavior (v15+ defaults changed)
// GET route handlers are NO LONGER cached by default in v15+
// Explicitly opt-in to caching:
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600, tags: ['products'] }
})
// Force dynamic (never cache):
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// ISR — revalidate every N seconds:
export const revalidate = 3600 // segment-level optionAsync Request APIs (v15 Breaking Change)
import { cookies, headers } from 'next/headers'
// BEFORE (v14): synchronous
const cookieStore = cookies()
// AFTER (v15+): must await
const cookieStore = await cookies()
const headersList = await headers()
// params and searchParams also async in page/layout props
const { slug } = await params
const { q } = await searchParamsafter() — Post-Response Side Effects
import { after } from 'next/server'
export async function GET(request: Request) {
const data = await fetchData()
// Fires AFTER response is sent to client
after(async () => {
await logAnalyticsEvent('data-fetched', { timestamp: Date.now() })
})
return Response.json(data)
}connection() — Force Dynamic Rendering
import { connection } from 'next/server'
export default async function Page() {
// Signals this component requires a live request (opts out of static rendering)
await connection()
const realTimeData = await fetchLiveData()
return <Dashboard data={realTimeData} />
}---
Part 5: Server Actions & Forms
Server Actions
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
})
export async function createPost(formData: FormData) {
const parsed = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
const post = await db.posts.create({ data: parsed.data })
revalidatePath('/blog')
redirect(`/blog/${post.id}`)
}<Form> Component (Next.js 15+)
import Form from 'next/form'
export default function SearchForm() {
// <Form> replaces <form> for client-side navigation + prefetching
return (
<Form action="/search">
<input name="q" placeholder="Search..." />
<button type="submit">Search</button>
</Form>
)
}Optimistic Updates with useOptimistic
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from '@/app/actions'
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, delta: number) => state + delta
)
const [isPending, startTransition] = useTransition()
return (
<button
onClick={() => startTransition(async () => {
addOptimisticLike(1)
await toggleLike(postId)
})}
disabled={isPending}
>
{optimisticLikes} Likes
</button>
)
}---
Part 6: Next.js MCP Dev Tools
The next-devtools-mcp package enables coding agents to connect to the live Next.js development server. Requires Next.js 16+.
Setup
// .mcp.json (project root)
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}Next.js 16+ includes a built-in MCP endpoint at /_next/mcp in the development server. next-devtools-mcp automatically discovers and connects to running instances — even across multiple ports.
Available MCP Tools
| Tool | What It Does |
|---|---|
get_errors | Retrieve current build, runtime, and type errors from the dev server |
get_logs | Get the path to the dev log file (browser console + server output) |
get_page_metadata | Get metadata about specific pages: routes, components, rendering type |
get_project_metadata | Retrieve project structure, next.config, and dev server URL |
get_server_action_by_id | Look up Server Actions by ID to find source file and function name |
nextjs_docs | Query comprehensive Next.js documentation and best practices |
nextjs_runtime | Interact with the running Next.js instance |
upgrade_nextjs_16 | Automated upgrade guide to Next.js 16 with codemods |
enable_cache_components | Setup and configuration guide for Cache Components |
Usage Patterns
# Ask the agent about runtime state
"What errors are currently in my application?"
→ Agent calls get_errors → analyzes build/type/runtime errors → suggests fixes
# Debug a specific route
"Why is /dashboard rendering statically instead of dynamically?"
→ Agent calls get_page_metadata with route=/dashboard → shows rendering config
# Navigate the codebase
"What Server Actions exist in this app?"
→ Agent calls get_project_metadata → then get_server_action_by_id for each action
# Upgrade workflow
"Help me upgrade to Next.js 16"
→ Agent calls upgrade_nextjs_16 → runs codemods → handles breaking changes
# Enable new features
"Set up Cache Components for this project"
→ Agent calls enable_cache_components → configures next.config.ts + shows patterns---
Part 7: Performance & Turbopack
Turbopack (Default in v15+)
# Turbopack is now the default — no flag needed
npm run dev # Uses Turbopack automatically
# Opt back to webpack if needed
npm run dev -- --webpack
# Enable Turbopack filesystem cache (persist across restarts)
# next.config.ts
experimental: { turbopackFileSystemCache: true }Benchmark vs webpack: 76.7% faster cold starts, 96.3% faster HMR.
Image Optimization
import Image from 'next/image'
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // LCP image: preloads synchronously
sizes="(max-width: 768px) 100vw, 1200px"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
)
}Font Optimization
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter', // CSS variable for Tailwind
display: 'swap',
})
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
)
}React Compiler
// next.config.ts — stable in v16
const nextConfig: NextConfig = {
reactCompiler: true, // eliminates manual useMemo/useCallback
}Bundle Optimization
// next.config.ts
const nextConfig: NextConfig = {
// Avoid importing entire icon libraries
// next/font handles subsetting automatically
bundlePagesRouterDependencies: true, // renamed from bundlePagesExternals in v15
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react'],
},
}---
Part 8: Metadata & SEO
Static Metadata
// app/layout.tsx or app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
description: 'App description for SEO',
openGraph: {
type: 'website',
url: 'https://example.com',
images: [{ url: '/og-image.jpg', width: 1200, height: 630 }],
},
robots: { index: true, follow: true },
metadataBase: new URL('https://example.com'),
}Dynamic Metadata
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params // async in v15+
const post = await getPost(slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [{ url: post.cover, width: 1200, height: 630 }],
},
}
}---
Part 9: Route Handlers & Middleware
Route Handlers (Uncached by Default in v15+)
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { after } from 'next/server'
// GET is NO LONGER cached by default in v15+
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')
const products = await db.products.findMany({
where: category ? { category } : undefined,
})
// Side effect after response
after(() => logRequest(request.url))
return NextResponse.json(products)
}
// Opt-in to caching for a route segment
export const revalidate = 3600 // revalidate every hour
export const dynamic = 'force-static' // always staticMiddleware
// middleware.ts (project root)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}---
Part 10: Instrumentation
Server-Side Lifecycle (instrumentation.ts)
// instrumentation.ts (stable in v15)
export async function register() {
// Runs once when the server starts (Node.js + Edge)
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Initialize OpenTelemetry, Sentry, etc.
const { initTracing } = await import('./lib/tracing')
await initTracing()
}
}
export async function onRequestError(
error: Error,
request: { path: string; method: string },
context: { routeType: string }
) {
// Centralized error reporting
await reportError(error, { path: request.path })
}Client-Side Instrumentation (instrumentation-client.ts)
// instrumentation-client.ts (v16)
export function onRouteChange({ path }: { path: string }) {
// Track route changes for analytics
analytics.track('page_view', { path })
}
export function onCaughtError(error: Error) {
// Capture client-side errors
Sentry.captureException(error)
}---
Part 11: Auth Interrupts (v16)
// middleware.ts or Server Component
import { forbidden, unauthorized } from 'next/navigation'
export default async function AdminPage() {
const session = await getSession()
if (!session) {
unauthorized() // renders unauthorized.tsx
}
if (!session.user.isAdmin) {
forbidden() // renders forbidden.tsx
}
return <AdminDashboard />
}---
Part 12: Upgrading to v15/v16
Automated Codemods
# Upgrade to v15 (handles async Request APIs automatically)
npx @next/codemod@latest upgrade
# Or upgrade to v16 specifically
npx @next/codemod@latest upgrade next@16
# Available codemods
npx @next/codemod@latest next-async-request-api .
npx @next/codemod@latest next-og-import .Key v15 Breaking Changes
| Change | Before (v14) | After (v15+) |
|---|---|---|
cookies() | sync | await cookies() |
headers() | sync | await headers() |
params | sync | await params |
searchParams | sync | await searchParams |
| GET Route Handlers | cached by default | not cached by default |
| Client Router Cache | cached segments | not cached by default |
serverComponentsExternalPackages | old name | serverExternalPackages |
bundlePagesExternals | old name | bundlePagesRouterDependencies |
---
Modern Component and Testing Examples
Server Components
export default async function DashboardPage() {
const metrics = await getDashboardMetrics();
return <Dashboard metrics={metrics} />;
}Error Boundaries
// app/dashboard/error.tsx
'use client';
export default function Error({ reset }: { reset: () => void }) {
return <button onClick={reset}>Retry dashboard</button>;
}Accessibility Testing Tools
import AxeBuilder from '@axe-core/playwright';
test('dashboard has no critical accessibility issues', async ({ page }) => {
await page.goto('/dashboard');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});Common Pitfalls
- Mixing server and client responsibilities: It bloats bundles and makes caching or auth decisions harder to reason about.
- Using old synchronous request APIs: Current Next.js releases expect async
params,searchParams,cookies(), andheaders(). - Skipping error and loading states: Streaming routes feel broken when only the happy path is modeled.
<!-- PORTABILITY:START -->
Cross-Client Portability
This skill is written to stay usable across GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- GitHub Copilot: keep the folder in a Copilot-visible skill or plugin path, or wrap the workflow as project instructions if the host does not support portable skill folders directly.
- Claude Code: keep the folder in a local skills directory or a compatible plugin or marketplace source.
- Codex: install or sync the folder into
$CODEX_HOME/skills/<skill-name>and restart Codex after major changes. - Gemini CLI: this repository generates a project command named
/skills:nextjs-developmentfrom this skill. Rebuild commands withpython scripts/export-gemini-skill.py nextjs-developmentand then run/commands reloadinside Gemini CLI.
<!-- PORTABILITY:END -->
<!-- MCP:START -->
MCP Availability And Fallback
Preferred MCP Server: Next.js MCP
- Fallback prompt: "Use the Next.js Development skill without MCP. Rely on the local
SKILL.md, bundled references or scripts, and manual verification. Show the exact commands, evidence, and final checks you used before concluding." - Use
next dev,next build,next lint, browser console output, and local server logs when live MCP diagnostics are unavailable. - Verify routing, rendering mode, and data-fetching behavior with the bundled examples and a running dev server.
<!-- MCP:END -->
Related Skills
- react-development: Use it when the workflow also needs React component architecture and client or server boundaries.
- javascript-development: Use it when the workflow also needs modern JavaScript and TypeScript application code.
- web-testing: Use it when the workflow also needs browser and end-to-end testing evidence.
- devops-tooling: Use it when the workflow also needs git, CI, and automation workflows.
Changelog
[2026-04-25] - Version 1.2 Verification Protocol Refresh
Added
- Added a
Verification Protocolsection with skill-specific pass/fail checks, one pressure-test scenario, and a measurable success metric. - Added guidance to leverage native parallel subagent dispatch and 200k+ context windows where available.
- Added or referenced the shared Component Review Rubric for frontend component review.
Changed
- Updated
SKILL.mdfrontmatter toversion: "1.2"andlast_updated: 2026-04-25. - Reframed activation guidance toward symptom -> action triggers and standardized two-stage review wording where applicable.
[2026-04-24] - Version 1.1 Refresh
Changed
- Updated the SKILL frontmatter version to
1.1for the 2026-04-24 catalog refresh.
All notable changes to the nextjs-development skill will be documented in this file.
[2026-04-24] - Skill Refresh
Changed
- Standardized the SKILL frontmatter with version metadata, last-updated date, tags, and a concise catalog description.
- Reformatted the portability and MCP guidance with a preferred server line, a copy-paste fallback prompt, and consistent bullet lists.
- Added a catalog-standard Anti-Patterns section and refreshed the Related Skills links at the end of the skill.
- Added current-version targeting, a before-and-after example, Common Pitfalls, and modern examples for Server Components, Error Boundaries, and accessibility testing tools.
[2026-04-24] - Current Version Refresh
Changed
- Updated the active Next.js version guidance from 16.1.6 to 16.2.4 after checking the current npm package version.
- Removed the redundant standalone Skill Paths section; the generated portability section remains the authoritative cross-client path guidance.
Tested
- Verified the latest published package version with
npm view next version.
[2026-04-04] - Gemini Path Clarification
Changed
- Expanded the explicit global path example so it documents both the Codex global skill path and the current Gemini Antigravity global skill path.
[2026-04-04] - Cross-Client Portability Refresh
Changed
- Added a standard portability note covering GitHub Copilot, Claude Code, Codex, and Gemini CLI.
- Documented the preferred MCP server surface for this skill and a local no-MCP fallback workflow.
Tested
- Validated
SKILL.mdfrontmatter, portability sections, and Gemini export readiness withpython scripts/validate-skills.py.
[2026-03-10] — Initial Release
Added
- Full Next.js 15/16 (v16.1.6) skill covering App Router, Server/Client Components, and routing
use cachedirective patterns withcacheTag(),cacheLife(), and named profiles- Async Request APIs section (v15 breaking change):
await cookies(),await headers(),await params,await searchParams - Server Actions with
"use server",<Form>component, optimistic updates after()andconnection()utility functions for post-response side effects and dynamic rendering- Next.js MCP dev tools section (
next-devtools-mcp) with.mcp.jsonsetup and tool reference table - Turbopack defaults,
turbopackFileSystemCache,serverComponentsHmrCache - React Compiler (
reactCompiler: true) stable config - Auth interrupts:
forbidden(),unauthorized()withforbidden.tsx,unauthorized.tsxfile conventions instrumentation.ts(stable) andinstrumentation-client.ts(v16) usage patterns- Middleware template with
matcherconfig - Metadata API: static and dynamic
generateMetadata - v15 upgrade breaking changes table and codemod commands
references/app-router-reference.md: complete file conventions and routing patterns quick referencereferences/nextjs-mcp-server.md: detailed MCP devtools setup and troubleshootingexamples/data-fetching-patterns.md:use cache, ISR,fetch, CSR patterns with TypeScriptexamples/server-client-components.md: RSC/RCC composition patterns and decision guidescripts/page-generator.ps1: PowerShell scaffold for App Router page, loading, error files
Data Fetching Patterns
Concrete TypeScript examples for every data fetching approach in Next.js 15/16.
---
1. use cache Directive (Preferred in v15+)
Cache a Server Component
// app/products/page.tsx
import { cacheTag, cacheLife } from 'next/cache'
async function ProductList() {
'use cache'
cacheLife('hours') // stale: 1h, revalidate: 1h, expire: 24h
cacheTag('products') // tag for targeted invalidation
const products = await db.products.findMany({
where: { active: true },
orderBy: { createdAt: 'desc' },
})
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
)
}
export default function Page() {
return <ProductList />
}Cache a Data-Fetching Function
// lib/products.ts
import { cacheTag, cacheLife } from 'next/cache'
export async function getProduct(id: string) {
'use cache'
cacheLife('days')
cacheTag('products', `product-${id}`)
return db.products.findUnique({ where: { id } })
}
// app/products/[id]/page.tsx
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await getProduct(id) // cached per id
if (!product) notFound()
return <ProductDetail product={product} />
}Revalidate on Mutation
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, formData: FormData) {
await db.products.update({
where: { id },
data: {
name: formData.get('name') as string,
price: Number(formData.get('price')),
},
})
revalidateTag(`product-${id}`) // invalidate specific product cache
revalidateTag('products') // invalidate product list cache
}---
2. ISR with revalidate (Still Valid)
// app/blog/page.tsx
// Revalidate the entire segment every 60 seconds (ISR)
export const revalidate = 60
export default async function BlogPage() {
const posts = await fetch('https://cms.example.com/posts').then(r => r.json())
return <PostList posts={posts} />
}// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((p) => ({ slug: p.slug }))
}
// Set per-page revalidation
export const revalidate = 3600 // 1 hour
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <Article post={post} />
}---
3. fetch with Cache Control
// Server Component: fine-grained fetch caching
export default async function Dashboard() {
// Cached and tagged (ISR-style)
const stats = await fetch('https://api.example.com/stats', {
next: { revalidate: 300, tags: ['stats'] },
}).then(r => r.json())
// Never cached (always fresh)
const alerts = await fetch('https://api.example.com/alerts', {
cache: 'no-store',
}).then(r => r.json())
return <DashboardView stats={stats} alerts={alerts} />
}Note: fetch caching is deduplicated per request in Next.js. Multiple components calling the same URL within one render get the same cached result (request memoization).---
4. Parallel Data Fetching
// ✅ Fetch in parallel — don't await sequentially
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// Start all fetches simultaneously
const [product, reviews, related] = await Promise.all([
getProduct(id),
getReviews(id),
getRelatedProducts(id),
])
return (
<>
<ProductDetail product={product} />
<ReviewList reviews={reviews} />
<RelatedProducts products={related} />
</>
)
}---
5. Streaming with Suspense
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { ReviewSkeleton } from '@/components/skeletons'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const product = await getProduct(id) // blocks render until resolved
return (
<>
<ProductDetail product={product} />
{/* Reviews load independently without blocking the product */}
<Suspense fallback={<ReviewSkeleton />}>
<ReviewList productId={id} />
</Suspense>
</>
)
}
// ReviewList is a separate async Server Component
async function ReviewList({ productId }: { productId: string }) {
const reviews = await getReviews(productId) // streamed in
return <ul>{reviews.map(r => <li key={r.id}>{r.body}</li>)}</ul>
}---
6. Client-Side Fetching (SWR / React Query)
For data that requires interactivity, user-specific state, or real-time updates:
// components/UserDashboard.tsx
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(r => r.json())
export function UserDashboard({ userId }: { userId: string }) {
const { data, error, isLoading, mutate } = useSWR(
`/api/users/${userId}/dashboard`,
fetcher,
{ refreshInterval: 30000 } // poll every 30s
)
if (isLoading) return <Skeleton />
if (error) return <ErrorMessage />
return (
<div>
<h2>Welcome, {data.name}</h2>
<button onClick={() => mutate()}>Refresh</button>
</div>
)
}---
7. Route Handler (API Endpoint)
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { after } from 'next/server'
// GET is NOT cached by default in v15+
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const category = searchParams.get('category') ?? undefined
const products = await db.products.findMany({
where: category ? { category } : undefined,
take: 50,
})
after(() => {
// Log analytics after response is sent
logApiCall({ endpoint: '/api/products', category })
})
return NextResponse.json(products)
}
// To opt-in to caching for this route handler:
// export const revalidate = 3600
// export const dynamic = 'force-static'
export async function POST(request: NextRequest) {
const body = await request.json()
// Validate input at API boundary
const { name, price, category } = body
if (!name || !price) {
return NextResponse.json({ error: 'name and price required' }, { status: 400 })
}
const product = await db.products.create({ data: { name, price, category } })
return NextResponse.json(product, { status: 201 })
}---
8. after() for Post-Response Work
// Server Action with post-response analytics
'use server'
import { after } from 'next/server'
export async function purchaseProduct(productId: string) {
const order = await db.orders.create({ data: { productId } })
// Fires after the action response is sent; won't delay the user
after(async () => {
await sendOrderConfirmationEmail(order.id)
await updateInventory(productId)
await logPurchaseEvent(order.id)
})
return { orderId: order.id }
}---
Pattern Comparison
| Pattern | When to Use | Cached? | Revalidated? |
|---|---|---|---|
use cache directive | Preferred for RSC data | Yes | Via revalidateTag |
export const revalidate | Segment-level ISR | Yes | On interval |
fetch with next.revalidate | Fine-grained ISR | Yes | On interval |
fetch with no-store | Always-fresh data | No | N/A |
| SWR / React Query | Client-side, interactive | Client cache | On mutation/interval |
Route Handler + force-static | Static API outputs | Yes | On interval |
Server & Client Component Patterns
Practical TypeScript patterns for composing Server Components (RSC) and Client Components (RCC) in Next.js App Router.
---
Decision Guide
Does the component need any of these?
✓ onClick, onChange, onSubmit or any event handler
✓ useState, useReducer, useEffect, useLayoutEffect
✓ useRouter, useParams, useSearchParams, usePathname
✓ window, document, localStorage, navigator
✓ Third-party libraries that use browser APIs
✓ Real-time subscriptions (WebSocket, SSE)
→ Add "use client" directive
Otherwise (default — no directive needed):
✓ Fetch data directly from DB or internal API
✓ Access cookies(), headers(), auth tokens
✓ Use large server-only dependencies (sharp, pdf-lib)
✓ Keep sensitive logic/credentials out of the JS bundle
✓ Top-level await in component body
→ Server Component (RSC)---
1. Basic Server Component
// app/products/page.tsx — Server Component (default, no directive)
import { db } from '@/lib/db'
export default async function ProductsPage() {
// Direct DB access — no API call needed, zero client-side JS
const products = await db.products.findMany({ where: { active: true } })
return (
<main>
<h1>Products</h1>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — ${p.price}
</li>
))}
</ul>
</main>
)
}---
2. Basic Client Component
// components/Counter.tsx
'use client'
import { useState } from 'react'
export function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [count, setCount] = useState(initialCount)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<button onClick={() => setCount(c => c - 1)}>Decrement</button>
</div>
)
}---
3. Composition: Server Data → Client Component
The most common pattern: fetch data in a Server Component, pass it as props to a Client Component.
// app/dashboard/page.tsx (Server Component)
import { getUser, getStats } from '@/lib/data'
import { StatsChart } from '@/components/StatsChart'
export default async function DashboardPage() {
const [user, stats] = await Promise.all([getUser(), getStats()])
return (
<div>
<h1>Welcome, {user.name}</h1>
{/* StatsChart is "use client" — receives plain data as props */}
<StatsChart data={stats} />
</div>
)
}
// components/StatsChart.tsx (Client Component)
'use client'
import { LineChart } from 'recharts'
export function StatsChart({ data }: { data: StatsData[] }) {
// data is already fetched; this component only handles rendering
return <LineChart data={data} width={600} height={300} />
}---
4. Pass Server Component as Child to Client Component
// ✅ This works — children passes through the "use client" boundary
// app/page.tsx (Server Component)
import { Modal } from '@/components/Modal'
import { UserProfile } from '@/components/UserProfile'
export default async function Page() {
const user = await getUser()
return (
<Modal>
{/* UserProfile is a Server Component — allowed as children prop */}
<UserProfile user={user} />
</Modal>
)
}
// components/Modal.tsx (Client Component)
'use client'
import { useState } from 'react'
export function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>Open</button>
{open && <div className="modal">{children}</div>}
</div>
)
}---
5. Server Component as Leaf Inside Client Component Tree
// ❌ WRONG: Importing a Server Component inside a Client Component
// components/ClientParent.tsx
'use client'
import { ServerChild } from './ServerChild' // ERROR: can't import RSC in RCC
// ✅ CORRECT: Pass Server Component via props/children
// app/page.tsx (Server Component — the composition boundary)
import { ClientParent } from '@/components/ClientParent'
import { ServerChild } from '@/components/ServerChild'
export default function Page() {
return (
<ClientParent>
<ServerChild /> {/* injected as children, not imported */}
</ClientParent>
)
}---
6. Context Providers (Must Be Client Components)
// components/providers/ThemeProvider.tsx
'use client'
import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext<{ dark: boolean; toggle: () => void } | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [dark, setDark] = useState(false)
return (
<ThemeContext.Provider value={{ dark, toggle: () => setDark(d => !d) }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
return ctx
}
// app/layout.tsx (Server Component)
import { ThemeProvider } from '@/components/providers/ThemeProvider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeProvider>
{children} {/* Server Components can be children of Client providers */}
</ThemeProvider>
</body>
</html>
)
}---
7. Server Actions from Client Components
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'
const schema = z.object({ message: z.string().min(1) })
export async function submitComment(
prevState: { error?: string } | null,
formData: FormData
) {
const result = schema.safeParse({ message: formData.get('message') })
if (!result.success) {
return { error: result.error.flatten().fieldErrors.message?.[0] }
}
await db.comments.create({ data: result.data })
revalidatePath('/comments')
return null
}
// components/CommentForm.tsx
'use client'
import { useActionState } from 'react'
import { submitComment } from '@/app/actions'
export function CommentForm() {
const [state, action, isPending] = useActionState(submitComment, null)
return (
<form action={action}>
<textarea name="message" required />
{state?.error && <p className="error">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Posting…' : 'Post Comment'}
</button>
</form>
)
}---
8. useLinkStatus Hook (v16)
// components/NavLink.tsx — shows pending state during navigation
'use client'
import Link from 'next/link'
import { useLinkStatus } from 'next/link'
function PendingIndicator() {
const { pending } = useLinkStatus()
return pending ? <Spinner /> : null
}
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<Link href={href}>
{children}
<PendingIndicator />
</Link>
)
}---
9. Server-Only and Client-Only Modules
// lib/server-auth.ts
import 'server-only' // throws if imported in Client Component
export async function getSession() {
const cookieStore = await cookies()
// ... decode JWT
}
// lib/analytics.ts
import 'client-only' // throws if imported in Server Component
export function trackEvent(name: string) {
window.gtag('event', name)
}---
Key Rules Summary
| Scenario | Solution |
|---|---|
| Need interactivity (click, state) | Add "use client" |
| Need server data in a Client Component | Fetch in RSC, pass as props |
| Need Server Component inside Client Component | Pass as children prop |
| Need context state (theme, auth session) | Wrap with a Client Provider in layout |
| Want to protect server-only code | Use import 'server-only' |
| Server Action from Client form | useActionState with action prop on <form> |
| Navigation pending state (v16) | useLinkStatus() inside <Link> scope |
MIT License
Copyright (c) 2026 Sithu Win San
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
App Router Quick Reference
Complete file conventions and routing patterns for Next.js App Router (v15/v16).
Official docs: https://nextjs.org/docs/app/api-reference/file-conventions
---
Table of Contents
1. Special Files 2. Dynamic Routes 3. Route Groups 4. Parallel Routes 5. Intercepting Routes 6. Route Segment Config 7. Metadata Files
---
Special Files
Core Layout & Page Files
| File | Extension | Description |
|---|---|---|
layout | .js .jsx .tsx | Root or segment layout; wraps children; persists across navigations |
page | .js .jsx .tsx | Unique UI for a route; makes it publicly accessible |
template | .js .jsx .tsx | Like layout but re-mounts on navigation; resets state |
loading | .js .jsx .tsx | Suspense wrapper shown while page/layout loads |
error | .js .jsx .tsx | Error boundary for the segment; must be a Client Component |
global-error | .js .jsx .tsx | Catches errors in root layout; wraps entire app |
not-found | .js .jsx .tsx | Rendered by notFound() or unmatched routes |
forbidden | .js .jsx .tsx | Rendered by forbidden() — HTTP 403 (v16) |
unauthorized | .js .jsx .tsx | Rendered by unauthorized() — HTTP 401 (v16) |
default | .js .jsx .tsx | Fallback for parallel routes when slot has no active match |
API & Infra Files
| File | Extension | Description |
|---|---|---|
route | .js .ts | API endpoint; exports HTTP verbs (GET, POST, etc.) |
proxy | .js .ts | Lightweight HTTP proxy for the segment (v16) |
middleware | .js .ts | Runs before request completes; place at project root |
instrumentation | .js .ts | Server lifecycle hooks, OpenTelemetry init (stable v15) |
instrumentation-client | .js .ts | Client-side performance + error monitoring (v16) |
---
Dynamic Routes
Segment Types
| Syntax | Example | Matches |
|---|---|---|
[slug] | app/blog/[slug]/page.tsx | /blog/hello-world |
[...slug] | app/docs/[...slug]/page.tsx | /docs/a, /docs/a/b, /docs/a/b/c |
[[...slug]] | app/shop/[[...slug]]/page.tsx | /shop, /shop/a, /shop/a/b |
Async Params Pattern (v15+ Required)
// app/blog/[slug]/page.tsx
interface Props {
params: Promise<{ slug: string }>
searchParams: Promise<{ page?: string; sort?: string }>
}
export default async function BlogPost({ params, searchParams }: Props) {
// Both params and searchParams are now async
const { slug } = await params
const { page = '1', sort = 'date' } = await searchParams
const post = await getPost(slug)
return <Article post={post} page={Number(page)} sort={sort} />
}
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}---
Route Groups
Use (groupName) to organize routes without affecting the URL structure.
app/
├── (marketing)/
│ ├── layout.tsx # Layout shared only for marketing pages
│ ├── page.tsx # → /
│ ├── about/page.tsx # → /about
│ └── pricing/page.tsx # → /pricing
├── (app)/
│ ├── layout.tsx # Layout shared only for app pages (requires auth)
│ ├── dashboard/page.tsx # → /dashboard
│ └── settings/page.tsx # → /settings
└── layout.tsx # Root layoutUse cases:
- Different layouts for different sections without URL nesting
- Opt segments in/out of a shared layout
- Split large apps into logical sections
---
Parallel Routes
Render multiple pages simultaneously in the same layout using slots (@folderName).
app/
├── layout.tsx # Receives { children, modal } props
├── page.tsx # → / (main content)
└── @modal/ # Parallel slot
├── default.tsx # Fallback when no modal is active
└── photo/
└── [id]/
└── page.tsx # → renders as modal alongside main page// app/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode // @modal slot
}) {
return (
<>
{children}
{modal}
</>
)
}Use cases:
- Modals with shareable URLs (soft navigation)
- Split views (e.g., sidebar + main content)
- Tab navigation that preserves page state
---
Intercepting Routes
Intercept a route in a different context (e.g., show a modal instead of full page navigation).
| Convention | Intercepts |
|---|---|
(.)folder | Same level |
(..)folder | One level up |
(..)(..)folder | Two levels up |
(...)folder | From root app/ |
app/
├── photos/
│ └── [id]/
│ └── page.tsx # Full page: /photos/123
├── @modal/
│ ├── default.tsx # null (no modal by default)
│ └── (.)photos/ # Intercept same-level /photos
│ └── [id]/
│ └── page.tsx # Modal: shown when navigating from feed
└── layout.tsx # Renders both children + @modalPattern: On soft navigation from within the app → modal. On hard navigation (new tab, direct URL) → full page.
---
Route Segment Config
Export these from page.tsx, layout.tsx, or route.ts to control rendering behavior.
// Rendering mode
export const dynamic = 'auto' // default: auto-detect
export const dynamic = 'force-dynamic' // always SSR
export const dynamic = 'error' // error if dynamic
export const dynamic = 'force-static' // always static
// Revalidation (ISR)
export const revalidate = false // cache forever
export const revalidate = 0 // no cache (same as force-dynamic)
export const revalidate = 3600 // revalidate every hour
// Runtime
export const runtime = 'nodejs' // default
export const runtime = 'edge' // Edge Runtime
// Fetch cache
export const fetchCache = 'auto' // default
export const fetchCache = 'force-no-store'
export const fetchCache = 'force-cache'
// Generate static paths at build
export async function generateStaticParams() { ... }---
Metadata Files
These files are auto-detected in any route segment and handle SEO/social metadata.
| File | Content Type | Description |
|---|---|---|
favicon.ico | Image | Browser favicon |
icon.png / icon.svg | Image | App icon |
apple-icon.png | Image | iOS home screen icon |
opengraph-image.png | Image | OG image for social sharing |
twitter-image.png | Image | Twitter card image |
opengraph-image.tsx | Dynamic | Auto-generated OG image with ImageResponse |
sitemap.xml / sitemap.ts | XML | Crawlable sitemap |
robots.txt / robots.ts | Text | Crawl directives |
manifest.json | JSON | PWA web app manifest |
Dynamic OG Image Example
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function OGImage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return new ImageResponse(
<div style={{ display: 'flex', background: '#fff', width: '100%', height: '100%' }}>
<h1 style={{ fontSize: 60 }}>{post.title}</h1>
</div>,
{ ...size }
)
}Next.js MCP Server Reference
Setup, capabilities, and usage patterns for next-devtools-mcp with Next.js 16+.
Official guide: https://nextjs.org/docs/app/guides/mcp
Package: https://www.npmjs.com/package/next-devtools-mcp
Repository: https://github.com/vercel/next-devtools-mcp
---
Requirements
- Next.js 16.0.0 or above
- A running development server (
npm run dev) - A coding agent that supports MCP (e.g. GitHub Copilot, Claude)
---
Setup
1. Add .mcp.json to Project Root
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}2. Start the Dev Server
npm run dev
# or
pnpm devnext-devtools-mcp automatically discovers all running Next.js 16+ instances on your machine and connects to them via the built-in /_next/mcp endpoint.
3. Agent Auto-Discovery
Once your agent loads the .mcp.json config, it connects automatically. No additional setup needed.
---
How It Works
Next.js 16+ includes a built-in MCP endpoint at /_next/mcp inside the development server. This endpoint exposes runtime state, errors, and project metadata over the Model Context Protocol.
next-devtools-mcp acts as a proxy that:
- Discovers all running Next.js instances (scanning local ports)
- Forwards tool calls to the appropriate dev server
- Returns structured results to the agent
This means agents work seamlessly with multi-app setups (e.g., monorepos with multiple ports).
---
Available Tools
get_errors
Retrieve current errors from the running development server.
Returns:
- Build errors (TypeScript, ESLint, module resolution)
- Runtime errors (unhandled exceptions)
- Type errors from the TypeScript language server
Use when: App shows an error overlay, build fails, or you need to check the current error state.
---
get_logs
Get the path to the development log file.
Returns: File path containing:
- Browser console logs (errors, warnings, info)
- Server-side console output
- HMR events
Use when: Debugging rendering behavior, tracing data flow, finding silent failures.
---
get_page_metadata
Get detailed metadata about a specific page in the application.
Parameters: route (string) — e.g. /dashboard, /blog/[slug]
Returns:
- Rendering type (static, dynamic, ISR)
- Component tree (Server Components, Client Components)
- Route segment config (
dynamic,revalidate,runtime) - Applied layouts and templates
Use when: Diagnosing unexpected static/dynamic rendering, understanding component boundaries.
---
get_project_metadata
Retrieve high-level project information.
Returns:
- Next.js version and
next.configoptions - Dev server URL and running port
- Directory structure overview
- All registered routes and their types
Use when: Starting work in an unfamiliar project, checking config before suggesting patterns.
---
get_server_action_by_id
Look up a Server Action by its Next.js-generated ID.
Parameters: id (string) — the opaque action ID from client-side code
Returns:
- Source file path
- Function name
- Module location
Use when: Tracing Server Action calls in client components, debugging action routing.
---
nextjs_docs (Knowledge Base Query)
Query the comprehensive Next.js documentation and best practices knowledge base.
Use when:
- Answering "when should I use X?" questions about Next.js APIs
- Validating patterns against official guidance
- Getting context on specific APIs before generating code
---
nextjs_runtime
Interact directly with the running Next.js instance.
Use when:
- Live state queries during development
- Checking current configuration and middleware
- Understanding the active routing state
---
upgrade_nextjs_16
Automated guide and tooling for upgrading a project to Next.js 16.
What it does: 1. Analyzes current version and config 2. Runs applicable @next/codemod transforms 3. Identifies manual changes needed (breaking changes) 4. Guides through next.config renames and API changes
Use when: User asks to upgrade to v16, migrating from v14/v15.
---
enable_cache_components
Setup and configuration assistance for the Cache Components feature (v16).
What it does:
- Enables
cacheComponentsinnext.config.ts - Explains
"use cache",cacheTag(),cacheLife()setup - Configures
cacheHandlersif needed - Shows conversion patterns from old ISR/
fetchapproaches
Use when: User wants to adopt the use cache directive feature.
---
Troubleshooting
MCP Server Not Connecting
1. Ensure Next.js version is 16.0.0 or above: node -e "require('next/package.json').version" 2. Verify .mcp.json is at the project root (same level as package.json) 3. Check that the dev server is actively running: npm run dev 4. Restart the dev server if it was already running when you added .mcp.json 5. Verify your coding agent has loaded the MCP config (check agent settings)
Multiple Projects / Monorepo
next-devtools-mcp discovers all running Next.js instances automatically. Place the .mcp.json at each app's root if needed, or at the monorepo root to configure once.
Older Next.js (v13/v14)
The built-in /_next/mcp endpoint does not exist below v16. You cannot use next-devtools-mcp with older versions. Use the upgrade tool to migrate first.
---
Example Prompts
# Debugging
"What errors are currently in my app?"
"Why is /products/[id] rendering statically?"
"Show me all Server Actions in the project"
# Understanding structure
"What does the component tree look like for /checkout?"
"Which routes are dynamic vs static?"
"What's in my next.config.ts?"
# Upgrading & migration
"Help me upgrade this app from Next.js 14 to 16"
"Convert my getServerSideProps pages to App Router"
"Enable Cache Components in this project"
# Best practices
"When should I use 'use cache' vs ISR revalidate?"
"Should this component be a Server or Client Component?"
"How do I add OpenTelemetry to this Next.js 16 app?"# Next.js App Router Page Generator
# Scaffolds page, loading, and error files for a given route segment.
# Usage: .\page-generator.ps1 -Route "blog/[slug]"
# .\page-generator.ps1 -Route "dashboard/settings" -AppDir "src/app"
param(
[Parameter(Mandatory = $true)]
[string]$Route,
[string]$AppDir = "app"
)
# Resolve the target directory relative to current working directory
$targetDir = Join-Path (Get-Location) $AppDir $Route
# Create the directory if it doesn't exist
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
Write-Host "Created directory: $targetDir" -ForegroundColor Green
}
# Derive a PascalCase component name from the route (strip dynamic segments for naming)
$componentBase = ($Route -split '/')[-1]
$componentBase = $componentBase -replace '[\[\]\.]+', ''
$componentName = (Get-Culture).TextInfo.ToTitleCase($componentBase) -replace '\s', ''
if (-not $componentName) { $componentName = "Page" }
# --- page.tsx ---
$pageFile = Join-Path $targetDir "page.tsx"
if (-not (Test-Path $pageFile)) {
# Detect if the route has dynamic segments
$hasDynamicSegment = $Route -match '\[.+?\]'
if ($hasDynamicSegment) {
# Extract param names from brackets
$paramMatches = [regex]::Matches($Route, '\[(?:\.{3})?(\w+)\]')
$paramNames = $paramMatches | ForEach-Object { $_.Groups[1].Value }
$paramsType = ($paramNames | ForEach-Object { " $($_): string" }) -join "`n"
$paramsDestructure = ($paramNames | ForEach-Object { $_ }) -join ", "
$pageContent = @"
interface Props {
params: Promise<{
$paramsType
}>
}
export default async function ${componentName}Page({ params }: Props) {
const { $paramsDestructure } = await params
return (
<main>
<h1>${componentName}</h1>
</main>
)
}
"@
} else {
$pageContent = @"
export default async function ${componentName}Page() {
return (
<main>
<h1>${componentName}</h1>
</main>
)
}
"@
}
Set-Content -Path $pageFile -Value $pageContent -Encoding UTF8
Write-Host "Created: $pageFile" -ForegroundColor Cyan
} else {
Write-Host "Skipped (exists): $pageFile" -ForegroundColor Yellow
}
# --- loading.tsx ---
$loadingFile = Join-Path $targetDir "loading.tsx"
if (-not (Test-Path $loadingFile)) {
$loadingContent = @"
export default function ${componentName}Loading() {
return (
<div role="status" aria-label="Loading...">
<span>Loading…</span>
</div>
)
}
"@
Set-Content -Path $loadingFile -Value $loadingContent -Encoding UTF8
Write-Host "Created: $loadingFile" -ForegroundColor Cyan
} else {
Write-Host "Skipped (exists): $loadingFile" -ForegroundColor Yellow
}
# --- error.tsx ---
$errorFile = Join-Path $targetDir "error.tsx"
if (-not (Test-Path $errorFile)) {
$errorContent = @"
'use client'
import { useEffect } from 'react'
interface Props {
error: Error & { digest?: string }
reset: () => void
}
export default function ${componentName}Error({ error, reset }: Props) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div role="alert">
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
"@
Set-Content -Path $errorFile -Value $errorContent -Encoding UTF8
Write-Host "Created: $errorFile" -ForegroundColor Cyan
} else {
Write-Host "Skipped (exists): $errorFile" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Done. Route segment scaffolded at: $targetDir" -ForegroundColor Green
Related skills
FAQ
What does nextjs-development do?
nextjs-development is a Claude Code skill for ai & agent building.
When should I use nextjs-development?
When you need to helps with ai & agent building tasks., or when nextjs-development is a claude code skill for ai & agent building.
What are the main capabilities?
nextjs-development; AI & Agent Building; AI-coding skill.