Ai nextjs
Next.js App Router best practices — Server Components, data fetching, caching, routing, middleware, metadata, error handling, streaming, Server Actions, and performance optimization for Next.js 14-16+.
git clone https://github.com/wpank/ai
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/frontend/nextjs" ~/.claude/skills/wpank-ai-nextjs && rm -rf "$T"
skills/frontend/nextjs/SKILL.mdNext.js App Router
Apply these patterns when building, reviewing, or debugging Next.js App Router applications.
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install nextjs
WHEN
- Building Next.js applications with App Router
- Migrating from Pages Router to App Router
- Implementing Server Components and streaming
- Setting up parallel and intercepting routes
- Optimizing data fetching and caching
- Building full-stack features with Server Actions
- Debugging hydration errors or RSC boundary issues
Rendering Modes
| Mode | Where | When to Use |
|---|---|---|
| Server Components | Server only | Data fetching, secrets, heavy computation |
| Client Components | Browser | Interactivity, hooks, browser APIs |
| Static (SSG) | Build time | Content that rarely changes |
| Dynamic (SSR) | Request time | Personalized or real-time data |
| Streaming | Progressive | Large pages, slow data sources |
Server vs Client Decision Tree
Does it need...? ├── useState, useEffect, event handlers, browser APIs │ └── Client Component ('use client') ├── Direct data fetching, no interactivity │ └── Server Component (default) └── Both? └── Split: Server parent fetches data → Client child handles UI
File Conventions
See file-conventions.md for complete reference.
app/ ├── layout.tsx # Shared UI wrapper (persists across navigations) ├── page.tsx # Route UI ├── loading.tsx # Suspense fallback (automatic) ├── error.tsx # Error boundary (must be 'use client') ├── not-found.tsx # 404 UI ├── route.ts # API endpoint (cannot coexist with page.tsx) ├── template.tsx # Like layout but re-mounts on navigation ├── default.tsx # Parallel route fallback └── opengraph-image.tsx # OG image generation
Route segments:
[slug] dynamic, [...slug] catch-all, [[...slug]] optional catch-all, (group) route group, @slot parallel route, _folder private (excluded from routing).
Data Fetching Patterns
Choose the right pattern for each use case. See data-patterns.md for full decision tree.
| Pattern | Use Case | Caching |
|---|---|---|
| Server Component fetch | Internal reads (preferred) | Full Next.js caching |
| Server Action | Mutations, form submissions | POST only, no cache |
| Route Handler | External APIs, webhooks, public REST | GET can be cached |
| Client fetch → API | Client-side reads (last resort) | HTTP cache headers |
Server Component Data Fetching (Preferred)
// app/products/page.tsx — Server Component by default export default async function ProductsPage() { const products = await db.product.findMany() // Direct DB access, no API layer return <ProductGrid products={products} /> }
Avoiding Data Waterfalls
// BAD: Sequential — each awaits before the next starts const user = await getUser() const posts = await getPosts() // GOOD: Parallel fetching const [user, posts] = await Promise.all([getUser(), getPosts()]) // GOOD: Streaming with Suspense — each section loads independently <Suspense fallback={<UserSkeleton />}><UserSection /></Suspense> <Suspense fallback={<PostsSkeleton />}><PostsSection /></Suspense>
Server Actions (Mutations)
// app/actions.ts 'use server' import { revalidateTag } from 'next/cache' export async function addToCart(productId: string) { const cookieStore = await cookies() const sessionId = cookieStore.get('session')?.value if (!sessionId) redirect('/login') await db.cart.upsert({ where: { sessionId_productId: { sessionId, productId } }, update: { quantity: { increment: 1 } }, create: { sessionId, productId, quantity: 1 }, }) revalidateTag('cart') return { success: true } }
Caching Strategy
| Method | Syntax | Use Case |
|---|---|---|
| No cache | | Always-fresh data |
| Static | | Rarely changes |
| ISR | | Time-based refresh |
| Tag-based | | On-demand invalidation |
Invalidate from Server Actions:
'use server' import { revalidateTag, revalidatePath } from 'next/cache' export async function updateProduct(id: string, data: ProductData) { await db.product.update({ where: { id }, data }) revalidateTag('products') // Invalidate by tag revalidatePath('/products') // Invalidate by path }
RSC Boundaries
Props crossing Server → Client boundary must be JSON-serializable. See rsc-boundaries.md.
| Prop Type | Valid? | Fix |
|---|---|---|
, , | Yes | — |
| Plain object / array | Yes | — |
Server Action () | Yes | — |
Function | No | Define inside client component |
object | No | Use |
, , class instance | No | Convert to plain object/array |
Critical rule: Client Components cannot be
async. Fetch data in a Server Component parent and pass it down.
Async APIs (Next.js 15+)
params, searchParams, cookies(), and headers() are all async. See async-patterns.md.
// Pages and layouts — always await params type Props = { params: Promise<{ slug: string }> } export default async function Page({ params }: Props) { const { slug } = await params } // Server functions const cookieStore = await cookies() const headersList = await headers() // Non-async components — use React.use() import { use } from 'react' export default function Page({ params }: Props) { const { slug } = use(params) }
Routing Patterns
Route Organization
| Pattern | Syntax | Purpose |
|---|---|---|
| Route groups | | Organize without affecting URL |
| Parallel routes | | Multiple independent sections in one layout |
| Intercepting routes | | Modal overlays on soft navigation |
| Private folders | | Exclude from routing |
Parallel Routes & Modals
See parallel-routes.md for complete modal pattern.
Key rules:
- Every
folder must have a@slot
(returnsdefault.tsx
) or you get 404 on refreshnull - Close modals with
, neverrouter.back()
orrouter.push()<Link> - Intercepting route matchers:
same level,(.)
one level up,(..)
from root(...)
Metadata & SEO
See metadata.md for OG images, sitemaps, and file conventions.
// Static metadata (layout or page) export const metadata: Metadata = { title: { default: 'My App', template: '%s | My App' }, description: 'Built with Next.js', } // Dynamic metadata export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params const post = await getPost(slug) return { title: post.title, description: post.description, openGraph: { images: [{ url: post.image, width: 1200, height: 630 }] }, } }
Metadata is Server Components only. If a page has
'use client', extract metadata to a parent layout.
Error Handling
See error-handling.md for full patterns including auth errors.
// app/blog/error.tsx — must be 'use client' 'use client' export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ) }
Critical gotcha:
redirect(), notFound(), forbidden(), and unauthorized() throw special errors. Never catch them in try/catch:
// BAD: redirect throw is caught — navigation fails! try { await db.post.create({ data }) redirect(`/posts/${post.id}`) } catch (error) { return { error: 'Failed' } // Catches the redirect too! } // GOOD: Call redirect outside try-catch let post try { post = await db.post.create({ data }) } catch (error) { return { error: 'Failed' } } redirect(`/posts/${post.id}`)
Streaming with Suspense
export default async function ProductPage({ params }: Props) { const { id } = await params const product = await getProduct(id) // Blocking — loads first return ( <div> <ProductHeader product={product} /> <Suspense fallback={<ReviewsSkeleton />}> <Reviews productId={id} /> {/* Streams in independently */} </Suspense> <Suspense fallback={<RecommendationsSkeleton />}> <Recommendations productId={id} /> {/* Streams in independently */} </Suspense> </div> ) }
Hooks That Require Suspense Boundaries
| Hook | Suspense Required |
|---|---|
| Always (or entire page becomes CSR) |
| In dynamic routes |
| No |
| No |
Performance
- Always use
overnext/image
— see image-optimization.md<img> - Always use
overnext/link
— client-side navigation with prefetching<a> - Always use
— see font-optimization.mdnext/font - Always use
— see scripts.mdnext/script - Set
on above-the-fold images (LCP)priority - Add
when usingsizes
— without it, the largest image variant downloadsfill - Dynamic imports for heavy client components:
const Chart = dynamic(() => import('./Chart')) - Use
to pre-render dynamic routes at build timegenerateStaticParams
Route Handlers
See route-handlers.md for API endpoint patterns.
Bundling
See bundling.md for fixing third-party package issues, server-incompatible packages, and ESM/CommonJS problems.
Hydration Errors
See hydration-errors.md for all causes and fixes.
| Cause | Fix |
|---|---|
Browser APIs (, ) | Client component with mount check |
| Render on client with |
for IDs | Use hook |
| Fix invalid HTML nesting |
| Third-party scripts modifying DOM | Use with |
Self-Hosting
See self-hosting.md for Docker, PM2, cache handlers, and deployment checklist.
Key points:
- Use
for Docker — creates minimal production bundleoutput: 'standalone' - Copy
andpublic/
separately (not included in standalone).next/static/ - Set
for containersHOSTNAME="0.0.0.0" - Multi-instance ISR requires a custom cache handler (Redis/S3) — filesystem cache breaks
- Set health check endpoint at
/api/health
NEVER Do
| Never | Why | Instead |
|---|---|---|
Add by default | Bloats client bundle, loses Server Component benefits | Server Components are default — add only for interactivity |
Make client components | Not supported — will crash | Fetch in Server Component parent, pass data as props |
Pass //functions to client | Not serializable across RSC boundary | Serialize to string/plain object, or use Server Actions |
| Fetch from own API in Server Components | Unnecessary round-trip — you're already on the server | Access DB/service directly |
Wrap / in try-catch | They throw special errors that get swallowed | Call outside try-catch or use |
Skip or Suspense fallbacks | Users see blank page during data loading | Always provide loading states |
Use without Suspense | Entire page silently falls back to CSR | Wrap in boundary |
Use to close modals | Breaks history, modal can flash/persist | Use |
Use for OG images | Built into Next.js already | Import from |
Omit in parallel route slots | Hard navigation (refresh) returns 404 | Add returning |
| Use Edge runtime unless required | Limited APIs, most npm packages break | Default Node.js runtime covers 95% of cases |
Skip prop on images | Downloads largest image variant always | Add or appropriate breakpoints |
| Import fonts in multiple components | Creates duplicate instances | Import once in layout, use CSS variable |
Use for Google Fonts | No optimization, blocks rendering | Use |
Reference Files
| File | Topic |
|---|---|
| rsc-boundaries.md | Server/Client boundary rules, serialization |
| data-patterns.md | Fetching decision tree, waterfall avoidance |
| error-handling.md | Error boundaries, redirect gotcha, auth errors |
| async-patterns.md | Next.js 15+ async params/cookies/headers |
| metadata.md | SEO, OG images, sitemaps, file conventions |
| parallel-routes.md | Modal pattern, intercepting routes, gotchas |
| hydration-errors.md | Causes, debugging, fixes |
| self-hosting.md | Docker, PM2, cache handlers, deployment |
| file-conventions.md | Project structure, special files, middleware |
| bundling.md | Third-party packages, SSR issues, Turbopack |
| image-optimization.md | next/image best practices |
| font-optimization.md | next/font best practices |
| scripts.md | next/script, third-party loading |
| route-handlers.md | API endpoints, request/response helpers |