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+.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "nextjs" with this command: npx skills add wpank/nextjs-guidelines

Next.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

ModeWhereWhen to Use
Server ComponentsServer onlyData fetching, secrets, heavy computation
Client ComponentsBrowserInteractivity, hooks, browser APIs
Static (SSG)Build timeContent that rarely changes
Dynamic (SSR)Request timePersonalized or real-time data
StreamingProgressiveLarge 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.

PatternUse CaseCaching
Server Component fetchInternal reads (preferred)Full Next.js caching
Server ActionMutations, form submissionsPOST only, no cache
Route HandlerExternal APIs, webhooks, public RESTGET can be cached
Client fetch → APIClient-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

MethodSyntaxUse Case
No cachefetch(url, { cache: 'no-store' })Always-fresh data
Staticfetch(url, { cache: 'force-cache' })Rarely changes
ISRfetch(url, { next: { revalidate: 60 } })Time-based refresh
Tag-basedfetch(url, { next: { tags: ['products'] } })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 TypeValid?Fix
string, number, booleanYes
Plain object / arrayYes
Server Action ('use server')Yes
Function () => {}NoDefine inside client component
Date objectNoUse .toISOString()
Map, Set, class instanceNoConvert 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

PatternSyntaxPurpose
Route groups(marketing)/Organize without affecting URL
Parallel routes@analytics/Multiple independent sections in one layout
Intercepting routes(.)photos/[id]Modal overlays on soft navigation
Private folders_components/Exclude from routing

Parallel Routes & Modals

See parallel-routes.md for complete modal pattern.

Key rules:

  • Every @slot folder must have a default.tsx (returns null) or you get 404 on refresh
  • Close modals with router.back(), never router.push() or <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

HookSuspense Required
useSearchParams()Always (or entire page becomes CSR)
usePathname()In dynamic routes
useParams()No
useRouter()No

Performance

  • Always use next/image over <img> — see image-optimization.md
  • Always use next/link over <a> — client-side navigation with prefetching
  • Always use next/font — see font-optimization.md
  • Always use next/script — see scripts.md
  • Set priority on above-the-fold images (LCP)
  • Add sizes when using fill — without it, the largest image variant downloads
  • Dynamic imports for heavy client components: const Chart = dynamic(() => import('./Chart'))
  • Use generateStaticParams to pre-render dynamic routes at build time

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.

CauseFix
Browser APIs (window, localStorage)Client component with useEffect mount check
new Date().toLocaleString()Render on client with useEffect
Math.random() for IDsUse useId() hook
<p><div>...</div></p>Fix invalid HTML nesting
Third-party scripts modifying DOMUse next/script with afterInteractive

Self-Hosting

See self-hosting.md for Docker, PM2, cache handlers, and deployment checklist.

Key points:

  • Use output: 'standalone' for Docker — creates minimal production bundle
  • Copy public/ and .next/static/ separately (not included in standalone)
  • Set HOSTNAME="0.0.0.0" for containers
  • Multi-instance ISR requires a custom cache handler (Redis/S3) — filesystem cache breaks
  • Set health check endpoint at /api/health

NEVER Do

NeverWhyInstead
Add 'use client' by defaultBloats client bundle, loses Server Component benefitsServer Components are default — add 'use client' only for interactivity
Make client components asyncNot supported — will crashFetch in Server Component parent, pass data as props
Pass Date/Map/functions to clientNot serializable across RSC boundarySerialize to string/plain object, or use Server Actions
Fetch from own API in Server ComponentsUnnecessary round-trip — you're already on the serverAccess DB/service directly
Wrap redirect()/notFound() in try-catchThey throw special errors that get swallowedCall outside try-catch or use unstable_rethrow()
Skip loading.tsx or Suspense fallbacksUsers see blank page during data loadingAlways provide loading states
Use useSearchParams without SuspenseEntire page silently falls back to CSRWrap in <Suspense> boundary
Use router.push() to close modalsBreaks history, modal can flash/persistUse router.back()
Use @vercel/og for OG imagesBuilt into Next.js alreadyImport from next/og
Omit default.tsx in parallel route slotsHard navigation (refresh) returns 404Add default.tsx returning null
Use Edge runtime unless requiredLimited APIs, most npm packages breakDefault Node.js runtime covers 95% of cases
Skip sizes prop on fill imagesDownloads largest image variant alwaysAdd sizes="100vw" or appropriate breakpoints
Import fonts in multiple componentsCreates duplicate instancesImport once in layout, use CSS variable
Use <link> for Google FontsNo optimization, blocks renderingUse next/font

Reference Files

FileTopic
rsc-boundaries.mdServer/Client boundary rules, serialization
data-patterns.mdFetching decision tree, waterfall avoidance
error-handling.mdError boundaries, redirect gotcha, auth errors
async-patterns.mdNext.js 15+ async params/cookies/headers
metadata.mdSEO, OG images, sitemaps, file conventions
parallel-routes.mdModal pattern, intercepting routes, gotchas
hydration-errors.mdCauses, debugging, fixes
self-hosting.mdDocker, PM2, cache handlers, deployment
file-conventions.mdProject structure, special files, middleware
bundling.mdThird-party packages, SSR issues, Turbopack
image-optimization.mdnext/image best practices
font-optimization.mdnext/font best practices
scripts.mdnext/script, third-party loading
route-handlers.mdAPI endpoints, request/response helpers

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

qwencloud-model-selector

[QwenCloud] Recommend the best Qwen model and parameters. TRIGGER when: choosing between Qwen models, comparing Qwen model pricing, understanding Qwen model...

Registry SourceRecently Updated
General

deployment-manager

You are a deployment manager with expertise in release orchestration, deployment strategies, and production reliability. Use when: release orchestration and...

Registry SourceRecently Updated
General

Hk Stock Morning Report

Generate HK stock market morning report (股市晨報) for bank trading desks. Triggers: "生成晨报", "股市晨报", "今日股市", "港股晨報" 報告結構(5部分): 1. 市場回顧(恒指/科指/國指 + 強弱勢股) 2. 南下資金(總...

Registry SourceRecently Updated
General

Story Long Scan

长篇网文扫榜。分析起点、番茄、晋江等平台排行榜数据,提炼市场趋势与热门题材。 触发方式:/story-long-scan、/长篇扫榜、「长篇什么火」「起点排行」

Registry SourceRecently Updated