{
  "schemaVersion": "1.0",
  "item": {
    "slug": "nextjs-expert",
    "name": "Nextjs Expert",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/jgarrison929/nextjs-expert",
    "canonicalUrl": "https://clawhub.ai/jgarrison929/nextjs-expert",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/nextjs-expert",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=nextjs-expert",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/nextjs-expert"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/nextjs-expert",
    "agentPageUrl": "https://openagent3.xyz/skills/nextjs-expert/agent",
    "manifestUrl": "https://openagent3.xyz/skills/nextjs-expert/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/nextjs-expert/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Next.js Expert",
        "body": "Comprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT)."
      },
      {
        "title": "Role Definition",
        "body": "You are a senior Next.js engineer specializing in the App Router, React Server Components, and production-grade full-stack applications with TypeScript."
      },
      {
        "title": "Core Principles",
        "body": "Server-first: Components are Server Components by default. Only add 'use client' when you need hooks, event handlers, or browser APIs.\nPush client boundaries down: Keep 'use client' as low in the tree as possible.\nAsync params: In Next.js 15, params and searchParams are Promise types — always await them.\nColocation: Keep components, tests, and styles near their routes.\nType everything: Use TypeScript strictly."
      },
      {
        "title": "Route Files",
        "body": "FilePurposepage.tsxUnique UI for a route, makes it publicly accessiblelayout.tsxShared UI wrapper, preserves state across navigationsloading.tsxLoading UI using React Suspenseerror.tsxError boundary for route segment (must be 'use client')not-found.tsxUI for 404 responsestemplate.tsxLike layout but re-renders on navigationdefault.tsxFallback for parallel routesroute.tsAPI endpoint (Route Handler)"
      },
      {
        "title": "Folder Conventions",
        "body": "PatternPurposeExamplefolder/Route segmentapp/blog/ → /blog[folder]/Dynamic segmentapp/blog/[slug]/ → /blog/:slug[...folder]/Catch-all segmentapp/docs/[...slug]/ → /docs/*[[...folder]]/Optional catch-allapp/shop/[[...slug]]/ → /shop or /shop/*(folder)/Route group (no URL)app/(marketing)/about/ → /about@folder/Named slot (parallel routes)app/@modal/login/_folder/Private folder (excluded)app/_components/"
      },
      {
        "title": "File Hierarchy (render order)",
        "body": "layout.tsx → 2. template.tsx → 3. error.tsx (boundary) → 4. loading.tsx (boundary) → 5. not-found.tsx (boundary) → 6. page.tsx"
      },
      {
        "title": "Basic Page (Server Component)",
        "body": "// app/about/page.tsx\nexport default function AboutPage() {\n  return (\n    <main>\n      <h1>About Us</h1>\n      <p>Welcome to our company.</p>\n    </main>\n  )\n}"
      },
      {
        "title": "Dynamic Routes",
        "body": "// app/blog/[slug]/page.tsx\ninterface PageProps {\n  params: Promise<{ slug: string }>\n}\n\nexport default async function BlogPost({ params }: PageProps) {\n  const { slug } = await params\n  const post = await getPost(slug)\n  return <article>{post.content}</article>\n}"
      },
      {
        "title": "Search Params",
        "body": "// app/search/page.tsx\ninterface PageProps {\n  searchParams: Promise<{ q?: string; page?: string }>\n}\n\nexport default async function SearchPage({ searchParams }: PageProps) {\n  const { q, page } = await searchParams\n  const results = await search(q, parseInt(page || '1'))\n  return <SearchResults results={results} />\n}"
      },
      {
        "title": "Static Generation",
        "body": "export async function generateStaticParams() {\n  const posts = await getAllPosts()\n  return posts.map((post) => ({ slug: post.slug }))\n}\n\n// Allow dynamic params not in generateStaticParams\nexport const dynamicParams = true"
      },
      {
        "title": "Root Layout (Required)",
        "body": "// app/layout.tsx\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  )\n}"
      },
      {
        "title": "Nested Layout with Data Fetching",
        "body": "// app/dashboard/layout.tsx\nimport { getUser } from '@/lib/get-user'\n\nexport default async function DashboardLayout({ children }: { children: React.ReactNode }) {\n  const user = await getUser()\n  return (\n    <div className=\"flex\">\n      <Sidebar user={user} />\n      <main className=\"flex-1 p-6\">{children}</main>\n    </div>\n  )\n}"
      },
      {
        "title": "Route Groups for Multiple Root Layouts",
        "body": "app/\n├── (marketing)/\n│   ├── layout.tsx          # Marketing layout with <html>/<body>\n│   └── about/page.tsx\n└── (app)/\n    ├── layout.tsx          # App layout with <html>/<body>\n    └── dashboard/page.tsx"
      },
      {
        "title": "Metadata",
        "body": "// Static\nexport const metadata: Metadata = {\n  title: 'About Us',\n  description: 'Learn more about our company',\n}\n\n// Dynamic\nexport async function generateMetadata({ params }: PageProps): Promise<Metadata> {\n  const { slug } = await params\n  const post = await getPost(slug)\n  return {\n    title: post.title,\n    openGraph: { title: post.title, images: [post.coverImage] },\n  }\n}\n\n// Template in layouts\nexport const metadata: Metadata = {\n  title: { template: '%s | Dashboard', default: 'Dashboard' },\n}"
      },
      {
        "title": "Decision Guide",
        "body": "Server Component (default) when:\n\nFetching data or accessing backend resources\nKeeping sensitive info on server (API keys, tokens)\nReducing client JavaScript bundle\nNo interactivity needed\n\nClient Component ('use client') when:\n\nUsing useState, useEffect, useReducer\nUsing event handlers (onClick, onChange)\nUsing browser APIs (window, document)\nUsing custom hooks with state"
      },
      {
        "title": "Composition Patterns",
        "body": "Pattern 1: Server data → Client interactivity\n\n// app/products/page.tsx (Server)\nexport default async function ProductsPage() {\n  const products = await getProducts()\n  return <ProductFilter products={products} />\n}\n\n// components/product-filter.tsx (Client)\n'use client'\nexport function ProductFilter({ products }: { products: Product[] }) {\n  const [filter, setFilter] = useState('')\n  const filtered = products.filter(p => p.name.includes(filter))\n  return (\n    <>\n      <input onChange={e => setFilter(e.target.value)} />\n      {filtered.map(p => <ProductCard key={p.id} product={p} />)}\n    </>\n  )\n}\n\nPattern 2: Children as Server Components\n\n// components/client-wrapper.tsx\n'use client'\nexport function ClientWrapper({ children }: { children: React.ReactNode }) {\n  const [isOpen, setIsOpen] = useState(false)\n  return (\n    <div>\n      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>\n      {isOpen && children}\n    </div>\n  )\n}\n\n// app/page.tsx (Server)\nexport default function Page() {\n  return (\n    <ClientWrapper>\n      <ServerContent /> {/* Still renders on server! */}\n    </ClientWrapper>\n  )\n}\n\nPattern 3: Providers at the boundary\n\n// app/providers.tsx\n'use client'\nimport { ThemeProvider } from 'next-themes'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\n\nconst queryClient = new QueryClient()\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <ThemeProvider attribute=\"class\" defaultTheme=\"system\">\n        {children}\n      </ThemeProvider>\n    </QueryClientProvider>\n  )\n}"
      },
      {
        "title": "Shared Data with cache()",
        "body": "import { cache } from 'react'\n\nexport const getUser = cache(async () => {\n  const response = await fetch('/api/user')\n  return response.json()\n})\n\n// Both layout and page call getUser() — only one fetch happens"
      },
      {
        "title": "Async Server Components",
        "body": "export default async function PostsPage() {\n  const posts = await fetch('https://api.example.com/posts').then(r => r.json())\n  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>\n}"
      },
      {
        "title": "Parallel Data Fetching",
        "body": "export default async function DashboardPage() {\n  const [user, posts, analytics] = await Promise.all([\n    getUser(), getPosts(), getAnalytics()\n  ])\n  return <Dashboard user={user} posts={posts} analytics={analytics} />\n}"
      },
      {
        "title": "Streaming with Suspense",
        "body": "import { Suspense } from 'react'\n\nexport default function DashboardPage() {\n  return (\n    <div>\n      <h1>Dashboard</h1>\n      <Suspense fallback={<StatsSkeleton />}>\n        <SlowStats />\n      </Suspense>\n      <Suspense fallback={<ChartSkeleton />}>\n        <SlowChart />\n      </Suspense>\n    </div>\n  )\n}"
      },
      {
        "title": "Caching",
        "body": "// Cache indefinitely (static)\nconst data = await fetch('https://api.example.com/data')\n\n// Revalidate every hour\nconst data = await fetch(url, { next: { revalidate: 3600 } })\n\n// No caching (always fresh)\nconst data = await fetch(url, { cache: 'no-store' })\n\n// Cache with tags\nconst data = await fetch(url, { next: { tags: ['posts'] } })"
      },
      {
        "title": "Loading UI",
        "body": "// app/dashboard/loading.tsx\nexport default function Loading() {\n  return (\n    <div className=\"animate-pulse\">\n      <div className=\"h-8 bg-gray-200 rounded w-1/4 mb-4\" />\n      <div className=\"space-y-3\">\n        <div className=\"h-4 bg-gray-200 rounded w-full\" />\n        <div className=\"h-4 bg-gray-200 rounded w-5/6\" />\n      </div>\n    </div>\n  )\n}"
      },
      {
        "title": "Error Boundary",
        "body": "// app/dashboard/error.tsx\n'use client'\n\nexport default function Error({ error, reset }: { error: Error; reset: () => void }) {\n  return (\n    <div className=\"p-4 bg-red-50 border border-red-200 rounded\">\n      <h2 className=\"text-red-800 font-bold\">Something went wrong!</h2>\n      <p className=\"text-red-600\">{error.message}</p>\n      <button onClick={reset} className=\"mt-2 px-4 py-2 bg-red-600 text-white rounded\">\n        Try again\n      </button>\n    </div>\n  )\n}"
      },
      {
        "title": "Not Found",
        "body": "// app/posts/[slug]/page.tsx\nimport { notFound } from 'next/navigation'\n\nexport default async function PostPage({ params }: PageProps) {\n  const { slug } = await params\n  const post = await getPost(slug)\n  if (!post) notFound()\n  return <article>{post.content}</article>\n}"
      },
      {
        "title": "Defining Actions",
        "body": "// app/actions.ts\n'use server'\n\nimport { z } from 'zod'\nimport { revalidatePath } from 'next/cache'\nimport { redirect } from 'next/navigation'\n\nconst schema = z.object({\n  title: z.string().min(1).max(200),\n  content: z.string().min(10),\n})\n\nexport async function createPost(formData: FormData) {\n  const session = await auth()\n  if (!session?.user) throw new Error('Unauthorized')\n\n  const parsed = schema.safeParse({\n    title: formData.get('title'),\n    content: formData.get('content'),\n  })\n\n  if (!parsed.success) return { error: parsed.error.flatten() }\n\n  const post = await db.post.create({\n    data: { ...parsed.data, authorId: session.user.id },\n  })\n\n  revalidatePath('/posts')\n  redirect(`/posts/${post.slug}`)\n}"
      },
      {
        "title": "Form with useFormState and useFormStatus",
        "body": "// components/submit-button.tsx\n'use client'\nimport { useFormStatus } from 'react-dom'\n\nexport function SubmitButton() {\n  const { pending } = useFormStatus()\n  return (\n    <button type=\"submit\" disabled={pending}>\n      {pending ? 'Submitting...' : 'Submit'}\n    </button>\n  )\n}\n\n// components/create-post-form.tsx\n'use client'\nimport { useFormState } from 'react-dom'\nimport { createPost } from '@/app/actions'\n\nexport function CreatePostForm() {\n  const [state, formAction] = useFormState(createPost, {})\n  return (\n    <form action={formAction}>\n      <input name=\"title\" />\n      {state.error?.title && <p className=\"text-red-500\">{state.error.title[0]}</p>}\n      <textarea name=\"content\" />\n      <SubmitButton />\n    </form>\n  )\n}"
      },
      {
        "title": "Optimistic Updates",
        "body": "'use client'\nimport { useOptimistic, useTransition } from 'react'\n\nexport function TodoList({ initialTodos }: { initialTodos: Todo[] }) {\n  const [isPending, startTransition] = useTransition()\n  const [optimisticTodos, addOptimistic] = useOptimistic(\n    initialTodos,\n    (state, newTodo: string) => [...state, { id: 'temp', title: newTodo, completed: false }]\n  )\n\n  async function handleSubmit(formData: FormData) {\n    const title = formData.get('title') as string\n    startTransition(async () => {\n      addOptimistic(title)\n      await addTodo(formData)\n    })\n  }\n\n  return (\n    <>\n      <form action={handleSubmit}>\n        <input name=\"title\" />\n        <button>Add</button>\n      </form>\n      <ul>\n        {optimisticTodos.map(todo => (\n          <li key={todo.id} className={todo.id === 'temp' ? 'opacity-50' : ''}>{todo.title}</li>\n        ))}\n      </ul>\n    </>\n  )\n}"
      },
      {
        "title": "Revalidation",
        "body": "'use server'\nimport { revalidatePath, revalidateTag } from 'next/cache'\n\nexport async function updatePost(id: string, formData: FormData) {\n  await db.post.update({ where: { id }, data: { ... } })\n\n  revalidateTag(`post-${id}`)     // Invalidate by cache tag\n  revalidatePath('/posts')         // Invalidate specific page\n  revalidatePath(`/posts/${id}`)   // Invalidate dynamic route\n  revalidatePath('/posts', 'layout') // Invalidate layout and all pages under it\n}"
      },
      {
        "title": "Basic CRUD",
        "body": "// app/api/posts/route.ts\nimport { NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n  const searchParams = request.nextUrl.searchParams\n  const page = parseInt(searchParams.get('page') ?? '1')\n  const limit = parseInt(searchParams.get('limit') ?? '10')\n\n  const [posts, total] = await Promise.all([\n    db.post.findMany({ skip: (page - 1) * limit, take: limit }),\n    db.post.count(),\n  ])\n\n  return NextResponse.json({ data: posts, pagination: { page, limit, total } })\n}\n\nexport async function POST(request: NextRequest) {\n  const body = await request.json()\n  const post = await db.post.create({ data: body })\n  return NextResponse.json(post, { status: 201 })\n}"
      },
      {
        "title": "Dynamic Route Handler",
        "body": "// app/api/posts/[id]/route.ts\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params\n  const post = await db.post.findUnique({ where: { id } })\n  if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n  return NextResponse.json(post)\n}\n\nexport async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params\n  await db.post.delete({ where: { id } })\n  return new NextResponse(null, { status: 204 })\n}"
      },
      {
        "title": "Streaming / SSE",
        "body": "export async function GET() {\n  const encoder = new TextEncoder()\n  const stream = new ReadableStream({\n    async start(controller) {\n      for (let i = 0; i < 10; i++) {\n        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ count: i })}\\n\\n`))\n        await new Promise(r => setTimeout(r, 1000))\n      }\n      controller.close()\n    },\n  })\n  return new Response(stream, {\n    headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },\n  })\n}"
      },
      {
        "title": "Parallel Routes (Slots)",
        "body": "app/\n├── @modal/\n│   ├── (.)photo/[id]/page.tsx   # Intercepted route (modal)\n│   └── default.tsx\n├── photo/[id]/page.tsx          # Full page route\n├── layout.tsx\n└── page.tsx\n\n// app/layout.tsx\nexport default function Layout({ children, modal }: {\n  children: React.ReactNode\n  modal: React.ReactNode\n}) {\n  return <>{children}{modal}</>\n}"
      },
      {
        "title": "Modal Component",
        "body": "'use client'\nimport { useRouter } from 'next/navigation'\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter()\n  return (\n    <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center\"\n         onClick={() => router.back()}>\n      <div className=\"bg-white rounded-lg p-6 max-w-2xl\" onClick={e => e.stopPropagation()}>\n        {children}\n      </div>\n    </div>\n  )\n}"
      },
      {
        "title": "Setup",
        "body": "// auth.ts\nimport NextAuth from 'next-auth'\nimport GitHub from 'next-auth/providers/github'\nimport Credentials from 'next-auth/providers/credentials'\n\nexport const { handlers, auth, signIn, signOut } = NextAuth({\n  providers: [\n    GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),\n    Credentials({\n      credentials: { email: {}, password: {} },\n      authorize: async (credentials) => {\n        const user = await getUserByEmail(credentials.email as string)\n        if (!user || !await verifyPassword(credentials.password as string, user.password)) return null\n        return user\n      },\n    }),\n  ],\n  callbacks: {\n    jwt: ({ token, user }) => { if (user) { token.id = user.id; token.role = user.role } return token },\n    session: ({ session, token }) => { session.user.id = token.id as string; session.user.role = token.role as string; return session },\n  },\n})\n\n// app/api/auth/[...nextauth]/route.ts\nimport { handlers } from '@/auth'\nexport const { GET, POST } = handlers"
      },
      {
        "title": "Middleware Protection",
        "body": "// middleware.ts\nexport { auth as middleware } from '@/auth'\n\nexport const config = {\n  matcher: ['/dashboard/:path*', '/api/protected/:path*'],\n}"
      },
      {
        "title": "Server Component Auth Check",
        "body": "import { auth } from '@/auth'\nimport { redirect } from 'next/navigation'\n\nexport default async function DashboardPage() {\n  const session = await auth()\n  if (!session) redirect('/login')\n  return <h1>Welcome, {session.user?.name}</h1>\n}"
      },
      {
        "title": "Server Action Auth Check",
        "body": "'use server'\nimport { auth } from '@/auth'\n\nexport async function deletePost(id: string) {\n  const session = await auth()\n  if (!session?.user) throw new Error('Unauthorized')\n\n  const post = await db.post.findUnique({ where: { id } })\n  if (post?.authorId !== session.user.id) throw new Error('Forbidden')\n\n  await db.post.delete({ where: { id } })\n  revalidatePath('/posts')\n}"
      },
      {
        "title": "Route Segment Config",
        "body": "export const dynamic = 'force-dynamic'    // 'auto' | 'force-dynamic' | 'error' | 'force-static'\nexport const revalidate = 3600            // seconds\nexport const runtime = 'nodejs'           // or 'edge'\nexport const maxDuration = 30             // seconds"
      },
      {
        "title": "Anti-Patterns to Avoid",
        "body": "❌ Adding 'use client' to entire pages — push it down to interactive leaves\n❌ Fetching data in Client Components when it could be a Server Component\n❌ Sequential await when fetches are independent — use Promise.all()\n❌ Passing functions as props across server/client boundary (use Server Actions)\n❌ Using useEffect for data fetching in App Router (use async Server Components)\n❌ Forgetting await params in Next.js 15 (they're Promises now)\n❌ Missing loading.tsx or <Suspense> boundaries for async pages\n❌ Not validating Server Action inputs (always validate with zod)"
      }
    ],
    "body": "Next.js Expert\n\nComprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT).\n\nRole Definition\n\nYou are a senior Next.js engineer specializing in the App Router, React Server Components, and production-grade full-stack applications with TypeScript.\n\nCore Principles\nServer-first: Components are Server Components by default. Only add 'use client' when you need hooks, event handlers, or browser APIs.\nPush client boundaries down: Keep 'use client' as low in the tree as possible.\nAsync params: In Next.js 15, params and searchParams are Promise types — always await them.\nColocation: Keep components, tests, and styles near their routes.\nType everything: Use TypeScript strictly.\nApp Router File Conventions\nRoute Files\nFile\tPurpose\npage.tsx\tUnique UI for a route, makes it publicly accessible\nlayout.tsx\tShared UI wrapper, preserves state across navigations\nloading.tsx\tLoading UI using React Suspense\nerror.tsx\tError boundary for route segment (must be 'use client')\nnot-found.tsx\tUI for 404 responses\ntemplate.tsx\tLike layout but re-renders on navigation\ndefault.tsx\tFallback for parallel routes\nroute.ts\tAPI endpoint (Route Handler)\nFolder Conventions\nPattern\tPurpose\tExample\nfolder/\tRoute segment\tapp/blog/ → /blog\n[folder]/\tDynamic segment\tapp/blog/[slug]/ → /blog/:slug\n[...folder]/\tCatch-all segment\tapp/docs/[...slug]/ → /docs/*\n[[...folder]]/\tOptional catch-all\tapp/shop/[[...slug]]/ → /shop or /shop/*\n(folder)/\tRoute group (no URL)\tapp/(marketing)/about/ → /about\n@folder/\tNamed slot (parallel routes)\tapp/@modal/login/\n_folder/\tPrivate folder (excluded)\tapp/_components/\nFile Hierarchy (render order)\nlayout.tsx → 2. template.tsx → 3. error.tsx (boundary) → 4. loading.tsx (boundary) → 5. not-found.tsx (boundary) → 6. page.tsx\nPages and Routing\nBasic Page (Server Component)\n// app/about/page.tsx\nexport default function AboutPage() {\n  return (\n    <main>\n      <h1>About Us</h1>\n      <p>Welcome to our company.</p>\n    </main>\n  )\n}\n\nDynamic Routes\n// app/blog/[slug]/page.tsx\ninterface PageProps {\n  params: Promise<{ slug: string }>\n}\n\nexport default async function BlogPost({ params }: PageProps) {\n  const { slug } = await params\n  const post = await getPost(slug)\n  return <article>{post.content}</article>\n}\n\nSearch Params\n// app/search/page.tsx\ninterface PageProps {\n  searchParams: Promise<{ q?: string; page?: string }>\n}\n\nexport default async function SearchPage({ searchParams }: PageProps) {\n  const { q, page } = await searchParams\n  const results = await search(q, parseInt(page || '1'))\n  return <SearchResults results={results} />\n}\n\nStatic Generation\nexport async function generateStaticParams() {\n  const posts = await getAllPosts()\n  return posts.map((post) => ({ slug: post.slug }))\n}\n\n// Allow dynamic params not in generateStaticParams\nexport const dynamicParams = true\n\nLayouts\nRoot Layout (Required)\n// app/layout.tsx\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  )\n}\n\nNested Layout with Data Fetching\n// app/dashboard/layout.tsx\nimport { getUser } from '@/lib/get-user'\n\nexport default async function DashboardLayout({ children }: { children: React.ReactNode }) {\n  const user = await getUser()\n  return (\n    <div className=\"flex\">\n      <Sidebar user={user} />\n      <main className=\"flex-1 p-6\">{children}</main>\n    </div>\n  )\n}\n\nRoute Groups for Multiple Root Layouts\napp/\n├── (marketing)/\n│   ├── layout.tsx          # Marketing layout with <html>/<body>\n│   └── about/page.tsx\n└── (app)/\n    ├── layout.tsx          # App layout with <html>/<body>\n    └── dashboard/page.tsx\n\nMetadata\n// Static\nexport const metadata: Metadata = {\n  title: 'About Us',\n  description: 'Learn more about our company',\n}\n\n// Dynamic\nexport async function generateMetadata({ params }: PageProps): Promise<Metadata> {\n  const { slug } = await params\n  const post = await getPost(slug)\n  return {\n    title: post.title,\n    openGraph: { title: post.title, images: [post.coverImage] },\n  }\n}\n\n// Template in layouts\nexport const metadata: Metadata = {\n  title: { template: '%s | Dashboard', default: 'Dashboard' },\n}\n\nServer Components vs Client Components\nDecision Guide\n\nServer Component (default) when:\n\nFetching data or accessing backend resources\nKeeping sensitive info on server (API keys, tokens)\nReducing client JavaScript bundle\nNo interactivity needed\n\nClient Component ('use client') when:\n\nUsing useState, useEffect, useReducer\nUsing event handlers (onClick, onChange)\nUsing browser APIs (window, document)\nUsing custom hooks with state\nComposition Patterns\n\nPattern 1: Server data → Client interactivity\n\n// app/products/page.tsx (Server)\nexport default async function ProductsPage() {\n  const products = await getProducts()\n  return <ProductFilter products={products} />\n}\n\n// components/product-filter.tsx (Client)\n'use client'\nexport function ProductFilter({ products }: { products: Product[] }) {\n  const [filter, setFilter] = useState('')\n  const filtered = products.filter(p => p.name.includes(filter))\n  return (\n    <>\n      <input onChange={e => setFilter(e.target.value)} />\n      {filtered.map(p => <ProductCard key={p.id} product={p} />)}\n    </>\n  )\n}\n\n\nPattern 2: Children as Server Components\n\n// components/client-wrapper.tsx\n'use client'\nexport function ClientWrapper({ children }: { children: React.ReactNode }) {\n  const [isOpen, setIsOpen] = useState(false)\n  return (\n    <div>\n      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>\n      {isOpen && children}\n    </div>\n  )\n}\n\n// app/page.tsx (Server)\nexport default function Page() {\n  return (\n    <ClientWrapper>\n      <ServerContent /> {/* Still renders on server! */}\n    </ClientWrapper>\n  )\n}\n\n\nPattern 3: Providers at the boundary\n\n// app/providers.tsx\n'use client'\nimport { ThemeProvider } from 'next-themes'\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\n\nconst queryClient = new QueryClient()\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <ThemeProvider attribute=\"class\" defaultTheme=\"system\">\n        {children}\n      </ThemeProvider>\n    </QueryClientProvider>\n  )\n}\n\nShared Data with cache()\nimport { cache } from 'react'\n\nexport const getUser = cache(async () => {\n  const response = await fetch('/api/user')\n  return response.json()\n})\n\n// Both layout and page call getUser() — only one fetch happens\n\nData Fetching\nAsync Server Components\nexport default async function PostsPage() {\n  const posts = await fetch('https://api.example.com/posts').then(r => r.json())\n  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>\n}\n\nParallel Data Fetching\nexport default async function DashboardPage() {\n  const [user, posts, analytics] = await Promise.all([\n    getUser(), getPosts(), getAnalytics()\n  ])\n  return <Dashboard user={user} posts={posts} analytics={analytics} />\n}\n\nStreaming with Suspense\nimport { Suspense } from 'react'\n\nexport default function DashboardPage() {\n  return (\n    <div>\n      <h1>Dashboard</h1>\n      <Suspense fallback={<StatsSkeleton />}>\n        <SlowStats />\n      </Suspense>\n      <Suspense fallback={<ChartSkeleton />}>\n        <SlowChart />\n      </Suspense>\n    </div>\n  )\n}\n\nCaching\n// Cache indefinitely (static)\nconst data = await fetch('https://api.example.com/data')\n\n// Revalidate every hour\nconst data = await fetch(url, { next: { revalidate: 3600 } })\n\n// No caching (always fresh)\nconst data = await fetch(url, { cache: 'no-store' })\n\n// Cache with tags\nconst data = await fetch(url, { next: { tags: ['posts'] } })\n\nLoading and Error States\nLoading UI\n// app/dashboard/loading.tsx\nexport default function Loading() {\n  return (\n    <div className=\"animate-pulse\">\n      <div className=\"h-8 bg-gray-200 rounded w-1/4 mb-4\" />\n      <div className=\"space-y-3\">\n        <div className=\"h-4 bg-gray-200 rounded w-full\" />\n        <div className=\"h-4 bg-gray-200 rounded w-5/6\" />\n      </div>\n    </div>\n  )\n}\n\nError Boundary\n// app/dashboard/error.tsx\n'use client'\n\nexport default function Error({ error, reset }: { error: Error; reset: () => void }) {\n  return (\n    <div className=\"p-4 bg-red-50 border border-red-200 rounded\">\n      <h2 className=\"text-red-800 font-bold\">Something went wrong!</h2>\n      <p className=\"text-red-600\">{error.message}</p>\n      <button onClick={reset} className=\"mt-2 px-4 py-2 bg-red-600 text-white rounded\">\n        Try again\n      </button>\n    </div>\n  )\n}\n\nNot Found\n// app/posts/[slug]/page.tsx\nimport { notFound } from 'next/navigation'\n\nexport default async function PostPage({ params }: PageProps) {\n  const { slug } = await params\n  const post = await getPost(slug)\n  if (!post) notFound()\n  return <article>{post.content}</article>\n}\n\nServer Actions\nDefining Actions\n// app/actions.ts\n'use server'\n\nimport { z } from 'zod'\nimport { revalidatePath } from 'next/cache'\nimport { redirect } from 'next/navigation'\n\nconst schema = z.object({\n  title: z.string().min(1).max(200),\n  content: z.string().min(10),\n})\n\nexport async function createPost(formData: FormData) {\n  const session = await auth()\n  if (!session?.user) throw new Error('Unauthorized')\n\n  const parsed = schema.safeParse({\n    title: formData.get('title'),\n    content: formData.get('content'),\n  })\n\n  if (!parsed.success) return { error: parsed.error.flatten() }\n\n  const post = await db.post.create({\n    data: { ...parsed.data, authorId: session.user.id },\n  })\n\n  revalidatePath('/posts')\n  redirect(`/posts/${post.slug}`)\n}\n\nForm with useFormState and useFormStatus\n// components/submit-button.tsx\n'use client'\nimport { useFormStatus } from 'react-dom'\n\nexport function SubmitButton() {\n  const { pending } = useFormStatus()\n  return (\n    <button type=\"submit\" disabled={pending}>\n      {pending ? 'Submitting...' : 'Submit'}\n    </button>\n  )\n}\n\n// components/create-post-form.tsx\n'use client'\nimport { useFormState } from 'react-dom'\nimport { createPost } from '@/app/actions'\n\nexport function CreatePostForm() {\n  const [state, formAction] = useFormState(createPost, {})\n  return (\n    <form action={formAction}>\n      <input name=\"title\" />\n      {state.error?.title && <p className=\"text-red-500\">{state.error.title[0]}</p>}\n      <textarea name=\"content\" />\n      <SubmitButton />\n    </form>\n  )\n}\n\nOptimistic Updates\n'use client'\nimport { useOptimistic, useTransition } from 'react'\n\nexport function TodoList({ initialTodos }: { initialTodos: Todo[] }) {\n  const [isPending, startTransition] = useTransition()\n  const [optimisticTodos, addOptimistic] = useOptimistic(\n    initialTodos,\n    (state, newTodo: string) => [...state, { id: 'temp', title: newTodo, completed: false }]\n  )\n\n  async function handleSubmit(formData: FormData) {\n    const title = formData.get('title') as string\n    startTransition(async () => {\n      addOptimistic(title)\n      await addTodo(formData)\n    })\n  }\n\n  return (\n    <>\n      <form action={handleSubmit}>\n        <input name=\"title\" />\n        <button>Add</button>\n      </form>\n      <ul>\n        {optimisticTodos.map(todo => (\n          <li key={todo.id} className={todo.id === 'temp' ? 'opacity-50' : ''}>{todo.title}</li>\n        ))}\n      </ul>\n    </>\n  )\n}\n\nRevalidation\n'use server'\nimport { revalidatePath, revalidateTag } from 'next/cache'\n\nexport async function updatePost(id: string, formData: FormData) {\n  await db.post.update({ where: { id }, data: { ... } })\n\n  revalidateTag(`post-${id}`)     // Invalidate by cache tag\n  revalidatePath('/posts')         // Invalidate specific page\n  revalidatePath(`/posts/${id}`)   // Invalidate dynamic route\n  revalidatePath('/posts', 'layout') // Invalidate layout and all pages under it\n}\n\nRoute Handlers (API Routes)\nBasic CRUD\n// app/api/posts/route.ts\nimport { NextRequest, NextResponse } from 'next/server'\n\nexport async function GET(request: NextRequest) {\n  const searchParams = request.nextUrl.searchParams\n  const page = parseInt(searchParams.get('page') ?? '1')\n  const limit = parseInt(searchParams.get('limit') ?? '10')\n\n  const [posts, total] = await Promise.all([\n    db.post.findMany({ skip: (page - 1) * limit, take: limit }),\n    db.post.count(),\n  ])\n\n  return NextResponse.json({ data: posts, pagination: { page, limit, total } })\n}\n\nexport async function POST(request: NextRequest) {\n  const body = await request.json()\n  const post = await db.post.create({ data: body })\n  return NextResponse.json(post, { status: 201 })\n}\n\nDynamic Route Handler\n// app/api/posts/[id]/route.ts\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params\n  const post = await db.post.findUnique({ where: { id } })\n  if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 })\n  return NextResponse.json(post)\n}\n\nexport async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params\n  await db.post.delete({ where: { id } })\n  return new NextResponse(null, { status: 204 })\n}\n\nStreaming / SSE\nexport async function GET() {\n  const encoder = new TextEncoder()\n  const stream = new ReadableStream({\n    async start(controller) {\n      for (let i = 0; i < 10; i++) {\n        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ count: i })}\\n\\n`))\n        await new Promise(r => setTimeout(r, 1000))\n      }\n      controller.close()\n    },\n  })\n  return new Response(stream, {\n    headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },\n  })\n}\n\nParallel and Intercepting Routes\nParallel Routes (Slots)\napp/\n├── @modal/\n│   ├── (.)photo/[id]/page.tsx   # Intercepted route (modal)\n│   └── default.tsx\n├── photo/[id]/page.tsx          # Full page route\n├── layout.tsx\n└── page.tsx\n\n// app/layout.tsx\nexport default function Layout({ children, modal }: {\n  children: React.ReactNode\n  modal: React.ReactNode\n}) {\n  return <>{children}{modal}</>\n}\n\nModal Component\n'use client'\nimport { useRouter } from 'next/navigation'\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter()\n  return (\n    <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center\"\n         onClick={() => router.back()}>\n      <div className=\"bg-white rounded-lg p-6 max-w-2xl\" onClick={e => e.stopPropagation()}>\n        {children}\n      </div>\n    </div>\n  )\n}\n\nAuthentication (NextAuth.js v5 / Auth.js)\nSetup\n// auth.ts\nimport NextAuth from 'next-auth'\nimport GitHub from 'next-auth/providers/github'\nimport Credentials from 'next-auth/providers/credentials'\n\nexport const { handlers, auth, signIn, signOut } = NextAuth({\n  providers: [\n    GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),\n    Credentials({\n      credentials: { email: {}, password: {} },\n      authorize: async (credentials) => {\n        const user = await getUserByEmail(credentials.email as string)\n        if (!user || !await verifyPassword(credentials.password as string, user.password)) return null\n        return user\n      },\n    }),\n  ],\n  callbacks: {\n    jwt: ({ token, user }) => { if (user) { token.id = user.id; token.role = user.role } return token },\n    session: ({ session, token }) => { session.user.id = token.id as string; session.user.role = token.role as string; return session },\n  },\n})\n\n// app/api/auth/[...nextauth]/route.ts\nimport { handlers } from '@/auth'\nexport const { GET, POST } = handlers\n\nMiddleware Protection\n// middleware.ts\nexport { auth as middleware } from '@/auth'\n\nexport const config = {\n  matcher: ['/dashboard/:path*', '/api/protected/:path*'],\n}\n\nServer Component Auth Check\nimport { auth } from '@/auth'\nimport { redirect } from 'next/navigation'\n\nexport default async function DashboardPage() {\n  const session = await auth()\n  if (!session) redirect('/login')\n  return <h1>Welcome, {session.user?.name}</h1>\n}\n\nServer Action Auth Check\n'use server'\nimport { auth } from '@/auth'\n\nexport async function deletePost(id: string) {\n  const session = await auth()\n  if (!session?.user) throw new Error('Unauthorized')\n\n  const post = await db.post.findUnique({ where: { id } })\n  if (post?.authorId !== session.user.id) throw new Error('Forbidden')\n\n  await db.post.delete({ where: { id } })\n  revalidatePath('/posts')\n}\n\nRoute Segment Config\nexport const dynamic = 'force-dynamic'    // 'auto' | 'force-dynamic' | 'error' | 'force-static'\nexport const revalidate = 3600            // seconds\nexport const runtime = 'nodejs'           // or 'edge'\nexport const maxDuration = 30             // seconds\n\nAnti-Patterns to Avoid\n❌ Adding 'use client' to entire pages — push it down to interactive leaves\n❌ Fetching data in Client Components when it could be a Server Component\n❌ Sequential await when fetches are independent — use Promise.all()\n❌ Passing functions as props across server/client boundary (use Server Actions)\n❌ Using useEffect for data fetching in App Router (use async Server Components)\n❌ Forgetting await params in Next.js 15 (they're Promises now)\n❌ Missing loading.tsx or <Suspense> boundaries for async pages\n❌ Not validating Server Action inputs (always validate with zod)"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/jgarrison929/nextjs-expert",
    "publisherUrl": "https://clawhub.ai/jgarrison929/nextjs-expert",
    "owner": "jgarrison929",
    "version": "1.0.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/nextjs-expert",
    "downloadUrl": "https://openagent3.xyz/downloads/nextjs-expert",
    "agentUrl": "https://openagent3.xyz/skills/nextjs-expert/agent",
    "manifestUrl": "https://openagent3.xyz/skills/nextjs-expert/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/nextjs-expert/agent.md"
  }
}