[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6JC7YHM5VT8XQ1PS5EPQJCC":3,"skill-same:01M6JC7YHM5VT8XQ1PS5EPQJCC":99},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":22,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":71,"commitSha":72,"skillDirectory":73,"forksCount":74,"repositoryHtmlUrl":75,"repositoryHomepageUrl":76,"ownerHtmlUrl":77,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":78,"install":79,"resources":92},"01M6JC7YHM5VT8XQ1PS5EPQJCC","nextjs-expert",null,"Use when building Next.js 14\u002F15 applications with the App Router. Invoke for routing, layouts, Server Components, Client Components, Server Actions, Route Handlers, authentication, middleware, data fetching, caching, revalidation, streaming, Suspense, loading states, error boundaries, dynamic routes, parallel routes, intercepting routes, or any Next.js architecture question.","S3YED\u002Fappie-kit","S3YED","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F62743867?v=4","01M6JBKBRRVP6KMS0VWZ27T3R6","01M6JBKBX0KG30284PW1DBYJT1",14,"MIT","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fcontent\u002Fnextjs-expert\u002FSKILL.md",[17],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",[23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"nextjs expert","nextjs","expert","question","handler","for","Server Components","app","tsx","router","use","components","parallel","Next.js","applications","actions","server","streaming","authentication","architecture","2026-11-10 10:12:30",false,"---\nname: nextjs-expert\nversion: 1.0.0\ndescription: Use when building Next.js 14\u002F15 applications with the App Router. Invoke for routing, layouts, Server Components, Client Components, Server Actions, Route Handlers, authentication, middleware, data fetching, caching, revalidation, streaming, Suspense, loading states, error boundaries, dynamic routes, parallel routes, intercepting routes, or any Next.js architecture question.\ntriggers:\n  - Next.js\n  - Next\n  - nextjs\n  - App Router\n  - Server Components\n  - Client Components\n  - Server Actions\n  - use server\n  - use client\n  - Route Handler\n  - middleware\n  - layout.tsx\n  - page.tsx\n  - loading.tsx\n  - error.tsx\n  - revalidatePath\n  - revalidateTag\n  - NextAuth\n  - Auth.js\n  - generateStaticParams\n  - generateMetadata\nrole: specialist\nscope: implementation\noutput-format: code\n---\n\n# Next.js Expert\n\nComprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT).\n\n## Role 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\n## Core Principles\n\n1. **Server-first**: Components are Server Components by default. Only add `'use client'` when you need hooks, event handlers, or browser APIs.\n2. **Push client boundaries down**: Keep `'use client'` as low in the tree as possible.\n3. **Async params**: In Next.js 15, `params` and `searchParams` are `Promise` types — always `await` them.\n4. **Colocation**: Keep components, tests, and styles near their routes.\n5. **Type everything**: Use TypeScript strictly.\n\n---\n\n## App Router File Conventions\n\n### Route Files\n\n| File | Purpose |\n|------|---------|\n| `page.tsx` | Unique UI for a route, makes it publicly accessible |\n| `layout.tsx` | Shared UI wrapper, preserves state across navigations |\n| `loading.tsx` | Loading UI using React Suspense |\n| `error.tsx` | Error boundary for route segment (must be `'use client'`) |\n| `not-found.tsx` | UI for 404 responses |\n| `template.tsx` | Like layout but re-renders on navigation |\n| `default.tsx` | Fallback for parallel routes |\n| `route.ts` | API endpoint (Route Handler) |\n\n### Folder Conventions\n\n| Pattern | Purpose | Example |\n|---------|---------|---------|\n| `folder\u002F` | Route segment | `app\u002Fblog\u002F` → `\u002Fblog` |\n| `[folder]\u002F` | Dynamic segment | `app\u002Fblog\u002F[slug]\u002F` → `\u002Fblog\u002F:slug` |\n| `[...folder]\u002F` | Catch-all segment | `app\u002Fdocs\u002F[...slug]\u002F` → `\u002Fdocs\u002F*` |\n| `[[...folder]]\u002F` | Optional catch-all | `app\u002Fshop\u002F[[...slug]]\u002F` → `\u002Fshop` or `\u002Fshop\u002F*` |\n| `(folder)\u002F` | Route group (no URL) | `app\u002F(marketing)\u002Fabout\u002F` → `\u002Fabout` |\n| `@folder\u002F` | Named slot (parallel routes) | `app\u002F@modal\u002Flogin\u002F` |\n| `_folder\u002F` | Private folder (excluded) | `app\u002F_components\u002F` |\n\n### File Hierarchy (render order)\n\n1. `layout.tsx` → 2. `template.tsx` → 3. `error.tsx` (boundary) → 4. `loading.tsx` (boundary) → 5. `not-found.tsx` (boundary) → 6. `page.tsx`\n\n---\n\n## Pages and Routing\n\n### Basic Page (Server Component)\n\n```tsx\n\u002F\u002F app\u002Fabout\u002Fpage.tsx\nexport default function AboutPage() {\n  return (\n    \u003Cmain>\n      \u003Ch1>About Us\u003C\u002Fh1>\n      \u003Cp>Welcome to our company.\u003C\u002Fp>\n    \u003C\u002Fmain>\n  )\n}\n```\n\n### Dynamic Routes\n\n```tsx\n\u002F\u002F app\u002Fblog\u002F[slug]\u002Fpage.tsx\ninterface PageProps {\n  params: Promise\u003C{ slug: string }>\n}\n\nexport default async function BlogPost({ params }: PageProps) {\n  const { slug } = await params\n  const post = await getPost(slug)\n  return \u003Carticle>{post.content}\u003C\u002Farticle>\n}\n```\n\n### Search Params\n\n```tsx\n\u002F\u002F app\u002Fsearch\u002Fpage.tsx\ninterface PageProps {\n  searchParams: Promise\u003C{ 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 \u003CSearchResults results={results} \u002F>\n}\n```\n\n### Static Generation\n\n```tsx\nexport async function generateStaticParams() {\n  const posts = await getAllPosts()\n  return posts.map((post) => ({ slug: post.slug }))\n}\n\n\u002F\u002F Allow dynamic params not in generateStaticParams\nexport const dynamicParams = true\n```\n\n---\n\n## Layouts\n\n### Root Layout (Required)\n\n```tsx\n\u002F\u002F app\u002Flayout.tsx\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml lang=\"en\">\n      \u003Cbody>{children}\u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  )\n}\n```\n\n### Nested Layout with Data Fetching\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Flayout.tsx\nimport { getUser } from '@\u002Flib\u002Fget-user'\n\nexport default async function DashboardLayout({ children }: { children: React.ReactNode }) {\n  const user = await getUser()\n  return (\n    \u003Cdiv className=\"flex\">\n      \u003CSidebar user={user} \u002F>\n      \u003Cmain className=\"flex-1 p-6\">{children}\u003C\u002Fmain>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Route Groups for Multiple Root Layouts\n\n```\napp\u002F\n├── (marketing)\u002F\n│   ├── layout.tsx          # Marketing layout with \u003Chtml>\u002F\u003Cbody>\n│   └── about\u002Fpage.tsx\n└── (app)\u002F\n    ├── layout.tsx          # App layout with \u003Chtml>\u002F\u003Cbody>\n    └── dashboard\u002Fpage.tsx\n```\n\n### Metadata\n\n```tsx\n\u002F\u002F Static\nexport const metadata: Metadata = {\n  title: 'About Us',\n  description: 'Learn more about our company',\n}\n\n\u002F\u002F Dynamic\nexport async function generateMetadata({ params }: PageProps): Promise\u003CMetadata> {\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\u002F\u002F Template in layouts\nexport const metadata: Metadata = {\n  title: { template: '%s | Dashboard', default: 'Dashboard' },\n}\n```\n\n---\n\n## Server Components vs Client Components\n\n### Decision Guide\n\n**Server Component (default) when:**\n- Fetching data or accessing backend resources\n- Keeping sensitive info on server (API keys, tokens)\n- Reducing client JavaScript bundle\n- No interactivity needed\n\n**Client Component (`'use client'`) when:**\n- Using `useState`, `useEffect`, `useReducer`\n- Using event handlers (`onClick`, `onChange`)\n- Using browser APIs (`window`, `document`)\n- Using custom hooks with state\n\n### Composition Patterns\n\n**Pattern 1: Server data → Client interactivity**\n\n```tsx\n\u002F\u002F app\u002Fproducts\u002Fpage.tsx (Server)\nexport default async function ProductsPage() {\n  const products = await getProducts()\n  return \u003CProductFilter products={products} \u002F>\n}\n\n\u002F\u002F components\u002Fproduct-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    \u003C>\n      \u003Cinput onChange={e => setFilter(e.target.value)} \u002F>\n      {filtered.map(p => \u003CProductCard key={p.id} product={p} \u002F>)}\n    \u003C\u002F>\n  )\n}\n```\n\n**Pattern 2: Children as Server Components**\n\n```tsx\n\u002F\u002F components\u002Fclient-wrapper.tsx\n'use client'\nexport function ClientWrapper({ children }: { children: React.ReactNode }) {\n  const [isOpen, setIsOpen] = useState(false)\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => setIsOpen(!isOpen)}>Toggle\u003C\u002Fbutton>\n      {isOpen && children}\n    \u003C\u002Fdiv>\n  )\n}\n\n\u002F\u002F app\u002Fpage.tsx (Server)\nexport default function Page() {\n  return (\n    \u003CClientWrapper>\n      \u003CServerContent \u002F> {\u002F* Still renders on server! *\u002F}\n    \u003C\u002FClientWrapper>\n  )\n}\n```\n\n**Pattern 3: Providers at the boundary**\n\n```tsx\n\u002F\u002F app\u002Fproviders.tsx\n'use client'\nimport { ThemeProvider } from 'next-themes'\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query'\n\nconst queryClient = new QueryClient()\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      \u003CThemeProvider attribute=\"class\" defaultTheme=\"system\">\n        {children}\n      \u003C\u002FThemeProvider>\n    \u003C\u002FQueryClientProvider>\n  )\n}\n```\n\n### Shared Data with `cache()`\n\n```tsx\nimport { cache } from 'react'\n\nexport const getUser = cache(async () => {\n  const response = await fetch('\u002Fapi\u002Fuser')\n  return response.json()\n})\n\n\u002F\u002F Both layout and page call getUser() — only one fetch happens\n```\n\n---\n\n## Data Fetching\n\n### Async Server Components\n\n```tsx\nexport default async function PostsPage() {\n  const posts = await fetch('https:\u002F\u002Fapi.example.com\u002Fposts').then(r => r.json())\n  return \u003Cul>{posts.map(p => \u003Cli key={p.id}>{p.title}\u003C\u002Fli>)}\u003C\u002Ful>\n}\n```\n\n### Parallel Data Fetching\n\n```tsx\nexport default async function DashboardPage() {\n  const [user, posts, analytics] = await Promise.all([\n    getUser(), getPosts(), getAnalytics()\n  ])\n  return \u003CDashboard user={user} posts={posts} analytics={analytics} \u002F>\n}\n```\n\n### Streaming with Suspense\n\n```tsx\nimport { Suspense } from 'react'\n\nexport default function DashboardPage() {\n  return (\n    \u003Cdiv>\n      \u003Ch1>Dashboard\u003C\u002Fh1>\n      \u003CSuspense fallback={\u003CStatsSkeleton \u002F>}>\n        \u003CSlowStats \u002F>\n      \u003C\u002FSuspense>\n      \u003CSuspense fallback={\u003CChartSkeleton \u002F>}>\n        \u003CSlowChart \u002F>\n      \u003C\u002FSuspense>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Caching\n\n```tsx\n\u002F\u002F Cache indefinitely (static)\nconst data = await fetch('https:\u002F\u002Fapi.example.com\u002Fdata')\n\n\u002F\u002F Revalidate every hour\nconst data = await fetch(url, { next: { revalidate: 3600 } })\n\n\u002F\u002F No caching (always fresh)\nconst data = await fetch(url, { cache: 'no-store' })\n\n\u002F\u002F Cache with tags\nconst data = await fetch(url, { next: { tags: ['posts'] } })\n```\n\n---\n\n## Loading and Error States\n\n### Loading UI\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Floading.tsx\nexport default function Loading() {\n  return (\n    \u003Cdiv className=\"animate-pulse\">\n      \u003Cdiv className=\"h-8 bg-gray-200 rounded w-1\u002F4 mb-4\" \u002F>\n      \u003Cdiv className=\"space-y-3\">\n        \u003Cdiv className=\"h-4 bg-gray-200 rounded w-full\" \u002F>\n        \u003Cdiv className=\"h-4 bg-gray-200 rounded w-5\u002F6\" \u002F>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Error Boundary\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Ferror.tsx\n'use client'\n\nexport default function Error({ error, reset }: { error: Error; reset: () => void }) {\n  return (\n    \u003Cdiv className=\"p-4 bg-red-50 border border-red-200 rounded\">\n      \u003Ch2 className=\"text-red-800 font-bold\">Something went wrong!\u003C\u002Fh2>\n      \u003Cp className=\"text-red-600\">{error.message}\u003C\u002Fp>\n      \u003Cbutton onClick={reset} className=\"mt-2 px-4 py-2 bg-red-600 text-white rounded\">\n        Try again\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Not Found\n\n```tsx\n\u002F\u002F app\u002Fposts\u002F[slug]\u002Fpage.tsx\nimport { notFound } from 'next\u002Fnavigation'\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 \u003Carticle>{post.content}\u003C\u002Farticle>\n}\n```\n\n---\n\n## Server Actions\n\n### Defining Actions\n\n```tsx\n\u002F\u002F app\u002Factions.ts\n'use server'\n\nimport { z } from 'zod'\nimport { revalidatePath } from 'next\u002Fcache'\nimport { redirect } from 'next\u002Fnavigation'\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('\u002Fposts')\n  redirect(`\u002Fposts\u002F${post.slug}`)\n}\n```\n\n### Form with useFormState and useFormStatus\n\n```tsx\n\u002F\u002F components\u002Fsubmit-button.tsx\n'use client'\nimport { useFormStatus } from 'react-dom'\n\nexport function SubmitButton() {\n  const { pending } = useFormStatus()\n  return (\n    \u003Cbutton type=\"submit\" disabled={pending}>\n      {pending ? 'Submitting...' : 'Submit'}\n    \u003C\u002Fbutton>\n  )\n}\n\n\u002F\u002F components\u002Fcreate-post-form.tsx\n'use client'\nimport { useFormState } from 'react-dom'\nimport { createPost } from '@\u002Fapp\u002Factions'\n\nexport function CreatePostForm() {\n  const [state, formAction] = useFormState(createPost, {})\n  return (\n    \u003Cform action={formAction}>\n      \u003Cinput name=\"title\" \u002F>\n      {state.error?.title && \u003Cp className=\"text-red-500\">{state.error.title[0]}\u003C\u002Fp>}\n      \u003Ctextarea name=\"content\" \u002F>\n      \u003CSubmitButton \u002F>\n    \u003C\u002Fform>\n  )\n}\n```\n\n### Optimistic Updates\n\n```tsx\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    \u003C>\n      \u003Cform action={handleSubmit}>\n        \u003Cinput name=\"title\" \u002F>\n        \u003Cbutton>Add\u003C\u002Fbutton>\n      \u003C\u002Fform>\n      \u003Cul>\n        {optimisticTodos.map(todo => (\n          \u003Cli key={todo.id} className={todo.id === 'temp' ? 'opacity-50' : ''}>{todo.title}\u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002F>\n  )\n}\n```\n\n### Revalidation\n\n```tsx\n'use server'\nimport { revalidatePath, revalidateTag } from 'next\u002Fcache'\n\nexport async function updatePost(id: string, formData: FormData) {\n  await db.post.update({ where: { id }, data: { ... } })\n\n  revalidateTag(`post-${id}`)     \u002F\u002F Invalidate by cache tag\n  revalidatePath('\u002Fposts')         \u002F\u002F Invalidate specific page\n  revalidatePath(`\u002Fposts\u002F${id}`)   \u002F\u002F Invalidate dynamic route\n  revalidatePath('\u002Fposts', 'layout') \u002F\u002F Invalidate layout and all pages under it\n}\n```\n\n---\n\n## Route Handlers (API Routes)\n\n### Basic CRUD\n\n```tsx\n\u002F\u002F app\u002Fapi\u002Fposts\u002Froute.ts\nimport { NextRequest, NextResponse } from 'next\u002Fserver'\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```\n\n### Dynamic Route Handler\n\n```tsx\n\u002F\u002F app\u002Fapi\u002Fposts\u002F[id]\u002Froute.ts\nexport async function GET(request: Request, { params }: { params: Promise\u003C{ 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\u003C{ id: string }> }) {\n  const { id } = await params\n  await db.post.delete({ where: { id } })\n  return new NextResponse(null, { status: 204 })\n}\n```\n\n### Streaming \u002F SSE\n\n```tsx\nexport async function GET() {\n  const encoder = new TextEncoder()\n  const stream = new ReadableStream({\n    async start(controller) {\n      for (let i = 0; i \u003C 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\u002Fevent-stream', 'Cache-Control': 'no-cache' },\n  })\n}\n```\n\n---\n\n## Parallel and Intercepting Routes\n\n### Parallel Routes (Slots)\n\n```\napp\u002F\n├── @modal\u002F\n│   ├── (.)photo\u002F[id]\u002Fpage.tsx   # Intercepted route (modal)\n│   └── default.tsx\n├── photo\u002F[id]\u002Fpage.tsx          # Full page route\n├── layout.tsx\n└── page.tsx\n```\n\n```tsx\n\u002F\u002F app\u002Flayout.tsx\nexport default function Layout({ children, modal }: {\n  children: React.ReactNode\n  modal: React.ReactNode\n}) {\n  return \u003C>{children}{modal}\u003C\u002F>\n}\n```\n\n### Modal Component\n\n```tsx\n'use client'\nimport { useRouter } from 'next\u002Fnavigation'\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter()\n  return (\n    \u003Cdiv className=\"fixed inset-0 bg-black\u002F50 flex items-center justify-center\"\n         onClick={() => router.back()}>\n      \u003Cdiv className=\"bg-white rounded-lg p-6 max-w-2xl\" onClick={e => e.stopPropagation()}>\n        {children}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n---\n\n## Authentication (NextAuth.js v5 \u002F Auth.js)\n\n### Setup\n\n```tsx\n\u002F\u002F auth.ts\nimport NextAuth from 'next-auth'\nimport GitHub from 'next-auth\u002Fproviders\u002Fgithub'\nimport Credentials from 'next-auth\u002Fproviders\u002Fcredentials'\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\u002F\u002F app\u002Fapi\u002Fauth\u002F[...nextauth]\u002Froute.ts\nimport { handlers } from '@\u002Fauth'\nexport const { GET, POST } = handlers\n```\n\n### Middleware Protection\n\n```tsx\n\u002F\u002F middleware.ts\nexport { auth as middleware } from '@\u002Fauth'\n\nexport const config = {\n  matcher: ['\u002Fdashboard\u002F:path*', '\u002Fapi\u002Fprotected\u002F:path*'],\n}\n```\n\n### Server Component Auth Check\n\n```tsx\nimport { auth } from '@\u002Fauth'\nimport { redirect } from 'next\u002Fnavigation'\n\nexport default async function DashboardPage() {\n  const session = await auth()\n  if (!session) redirect('\u002Flogin')\n  return \u003Ch1>Welcome, {session.user?.name}\u003C\u002Fh1>\n}\n```\n\n### Server Action Auth Check\n\n```tsx\n'use server'\nimport { auth } from '@\u002Fauth'\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('\u002Fposts')\n}\n```\n\n---\n\n## Route Segment Config\n\n```tsx\nexport const dynamic = 'force-dynamic'    \u002F\u002F 'auto' | 'force-dynamic' | 'error' | 'force-static'\nexport const revalidate = 3600            \u002F\u002F seconds\nexport const runtime = 'nodejs'           \u002F\u002F or 'edge'\nexport const maxDuration = 30             \u002F\u002F seconds\n```\n\n---\n\n## Anti-Patterns to Avoid\n\n1. ❌ Adding `'use client'` to entire pages — push it down to interactive leaves\n2. ❌ Fetching data in Client Components when it could be a Server Component\n3. ❌ Sequential `await` when fetches are independent — use `Promise.all()`\n4. ❌ Passing functions as props across server\u002Fclient boundary (use Server Actions)\n5. ❌ Using `useEffect` for data fetching in App Router (use async Server Components)\n6. ❌ Forgetting `await params` in Next.js 15 (they're Promises now)\n7. ❌ Missing `loading.tsx` or `\u003CSuspense>` boundaries for async pages\n8. ❌ Not validating Server Action inputs (always validate with zod)\n","# Next.js Expert\n\nComprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT).\n\n## Role 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\n## Core Principles\n\n1. **Server-first**: Components are Server Components by default. Only add `'use client'` when you need hooks, event handlers, or browser APIs.\n2. **Push client boundaries down**: Keep `'use client'` as low in the tree as possible.\n3. **Async params**: In Next.js 15, `params` and `searchParams` are `Promise` types — always `await` them.\n4. **Colocation**: Keep components, tests, and styles near their routes.\n5. **Type everything**: Use TypeScript strictly.\n\n---\n\n## App Router File Conventions\n\n### Route Files\n\n| File | Purpose |\n|------|---------|\n| `page.tsx` | Unique UI for a route, makes it publicly accessible |\n| `layout.tsx` | Shared UI wrapper, preserves state across navigations |\n| `loading.tsx` | Loading UI using React Suspense |\n| `error.tsx` | Error boundary for route segment (must be `'use client'`) |\n| `not-found.tsx` | UI for 404 responses |\n| `template.tsx` | Like layout but re-renders on navigation |\n| `default.tsx` | Fallback for parallel routes |\n| `route.ts` | API endpoint (Route Handler) |\n\n### Folder Conventions\n\n| Pattern | Purpose | Example |\n|---------|---------|---------|\n| `folder\u002F` | Route segment | `app\u002Fblog\u002F` → `\u002Fblog` |\n| `[folder]\u002F` | Dynamic segment | `app\u002Fblog\u002F[slug]\u002F` → `\u002Fblog\u002F:slug` |\n| `[...folder]\u002F` | Catch-all segment | `app\u002Fdocs\u002F[...slug]\u002F` → `\u002Fdocs\u002F*` |\n| `[[...folder]]\u002F` | Optional catch-all | `app\u002Fshop\u002F[[...slug]]\u002F` → `\u002Fshop` or `\u002Fshop\u002F*` |\n| `(folder)\u002F` | Route group (no URL) | `app\u002F(marketing)\u002Fabout\u002F` → `\u002Fabout` |\n| `@folder\u002F` | Named slot (parallel routes) | `app\u002F@modal\u002Flogin\u002F` |\n| `_folder\u002F` | Private folder (excluded) | `app\u002F_components\u002F` |\n\n### File Hierarchy (render order)\n\n1. `layout.tsx` → 2. `template.tsx` → 3. `error.tsx` (boundary) → 4. `loading.tsx` (boundary) → 5. `not-found.tsx` (boundary) → 6. `page.tsx`\n\n---\n\n## Pages and Routing\n\n### Basic Page (Server Component)\n\n```tsx\n\u002F\u002F app\u002Fabout\u002Fpage.tsx\nexport default function AboutPage() {\n  return (\n    \u003Cmain>\n      \u003Ch1>About Us\u003C\u002Fh1>\n      \u003Cp>Welcome to our company.\u003C\u002Fp>\n    \u003C\u002Fmain>\n  )\n}\n```\n\n### Dynamic Routes\n\n```tsx\n\u002F\u002F app\u002Fblog\u002F[slug]\u002Fpage.tsx\ninterface PageProps {\n  params: Promise\u003C{ slug: string }>\n}\n\nexport default async function BlogPost({ params }: PageProps) {\n  const { slug } = await params\n  const post = await getPost(slug)\n  return \u003Carticle>{post.content}\u003C\u002Farticle>\n}\n```\n\n### Search Params\n\n```tsx\n\u002F\u002F app\u002Fsearch\u002Fpage.tsx\ninterface PageProps {\n  searchParams: Promise\u003C{ 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 \u003CSearchResults results={results} \u002F>\n}\n```\n\n### Static Generation\n\n```tsx\nexport async function generateStaticParams() {\n  const posts = await getAllPosts()\n  return posts.map((post) => ({ slug: post.slug }))\n}\n\n\u002F\u002F Allow dynamic params not in generateStaticParams\nexport const dynamicParams = true\n```\n\n---\n\n## Layouts\n\n### Root Layout (Required)\n\n```tsx\n\u002F\u002F app\u002Flayout.tsx\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml lang=\"en\">\n      \u003Cbody>{children}\u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  )\n}\n```\n\n### Nested Layout with Data Fetching\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Flayout.tsx\nimport { getUser } from '@\u002Flib\u002Fget-user'\n\nexport default async function DashboardLayout({ children }: { children: React.ReactNode }) {\n  const user = await getUser()\n  return (\n    \u003Cdiv className=\"flex\">\n      \u003CSidebar user={user} \u002F>\n      \u003Cmain className=\"flex-1 p-6\">{children}\u003C\u002Fmain>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Route Groups for Multiple Root Layouts\n\n```\napp\u002F\n├── (marketing)\u002F\n│   ├── layout.tsx          # Marketing layout with \u003Chtml>\u002F\u003Cbody>\n│   └── about\u002Fpage.tsx\n└── (app)\u002F\n    ├── layout.tsx          # App layout with \u003Chtml>\u002F\u003Cbody>\n    └── dashboard\u002Fpage.tsx\n```\n\n### Metadata\n\n```tsx\n\u002F\u002F Static\nexport const metadata: Metadata = {\n  title: 'About Us',\n  description: 'Learn more about our company',\n}\n\n\u002F\u002F Dynamic\nexport async function generateMetadata({ params }: PageProps): Promise\u003CMetadata> {\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\u002F\u002F Template in layouts\nexport const metadata: Metadata = {\n  title: { template: '%s | Dashboard', default: 'Dashboard' },\n}\n```\n\n---\n\n## Server Components vs Client Components\n\n### Decision Guide\n\n**Server Component (default) when:**\n- Fetching data or accessing backend resources\n- Keeping sensitive info on server (API keys, tokens)\n- Reducing client JavaScript bundle\n- No interactivity needed\n\n**Client Component (`'use client'`) when:**\n- Using `useState`, `useEffect`, `useReducer`\n- Using event handlers (`onClick`, `onChange`)\n- Using browser APIs (`window`, `document`)\n- Using custom hooks with state\n\n### Composition Patterns\n\n**Pattern 1: Server data → Client interactivity**\n\n```tsx\n\u002F\u002F app\u002Fproducts\u002Fpage.tsx (Server)\nexport default async function ProductsPage() {\n  const products = await getProducts()\n  return \u003CProductFilter products={products} \u002F>\n}\n\n\u002F\u002F components\u002Fproduct-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    \u003C>\n      \u003Cinput onChange={e => setFilter(e.target.value)} \u002F>\n      {filtered.map(p => \u003CProductCard key={p.id} product={p} \u002F>)}\n    \u003C\u002F>\n  )\n}\n```\n\n**Pattern 2: Children as Server Components**\n\n```tsx\n\u002F\u002F components\u002Fclient-wrapper.tsx\n'use client'\nexport function ClientWrapper({ children }: { children: React.ReactNode }) {\n  const [isOpen, setIsOpen] = useState(false)\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => setIsOpen(!isOpen)}>Toggle\u003C\u002Fbutton>\n      {isOpen && children}\n    \u003C\u002Fdiv>\n  )\n}\n\n\u002F\u002F app\u002Fpage.tsx (Server)\nexport default function Page() {\n  return (\n    \u003CClientWrapper>\n      \u003CServerContent \u002F> {\u002F* Still renders on server! *\u002F}\n    \u003C\u002FClientWrapper>\n  )\n}\n```\n\n**Pattern 3: Providers at the boundary**\n\n```tsx\n\u002F\u002F app\u002Fproviders.tsx\n'use client'\nimport { ThemeProvider } from 'next-themes'\nimport { QueryClient, QueryClientProvider } from '@tanstack\u002Freact-query'\n\nconst queryClient = new QueryClient()\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return (\n    \u003CQueryClientProvider client={queryClient}>\n      \u003CThemeProvider attribute=\"class\" defaultTheme=\"system\">\n        {children}\n      \u003C\u002FThemeProvider>\n    \u003C\u002FQueryClientProvider>\n  )\n}\n```\n\n### Shared Data with `cache()`\n\n```tsx\nimport { cache } from 'react'\n\nexport const getUser = cache(async () => {\n  const response = await fetch('\u002Fapi\u002Fuser')\n  return response.json()\n})\n\n\u002F\u002F Both layout and page call getUser() — only one fetch happens\n```\n\n---\n\n## Data Fetching\n\n### Async Server Components\n\n```tsx\nexport default async function PostsPage() {\n  const posts = await fetch('https:\u002F\u002Fapi.example.com\u002Fposts').then(r => r.json())\n  return \u003Cul>{posts.map(p => \u003Cli key={p.id}>{p.title}\u003C\u002Fli>)}\u003C\u002Ful>\n}\n```\n\n### Parallel Data Fetching\n\n```tsx\nexport default async function DashboardPage() {\n  const [user, posts, analytics] = await Promise.all([\n    getUser(), getPosts(), getAnalytics()\n  ])\n  return \u003CDashboard user={user} posts={posts} analytics={analytics} \u002F>\n}\n```\n\n### Streaming with Suspense\n\n```tsx\nimport { Suspense } from 'react'\n\nexport default function DashboardPage() {\n  return (\n    \u003Cdiv>\n      \u003Ch1>Dashboard\u003C\u002Fh1>\n      \u003CSuspense fallback={\u003CStatsSkeleton \u002F>}>\n        \u003CSlowStats \u002F>\n      \u003C\u002FSuspense>\n      \u003CSuspense fallback={\u003CChartSkeleton \u002F>}>\n        \u003CSlowChart \u002F>\n      \u003C\u002FSuspense>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Caching\n\n```tsx\n\u002F\u002F Cache indefinitely (static)\nconst data = await fetch('https:\u002F\u002Fapi.example.com\u002Fdata')\n\n\u002F\u002F Revalidate every hour\nconst data = await fetch(url, { next: { revalidate: 3600 } })\n\n\u002F\u002F No caching (always fresh)\nconst data = await fetch(url, { cache: 'no-store' })\n\n\u002F\u002F Cache with tags\nconst data = await fetch(url, { next: { tags: ['posts'] } })\n```\n\n---\n\n## Loading and Error States\n\n### Loading UI\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Floading.tsx\nexport default function Loading() {\n  return (\n    \u003Cdiv className=\"animate-pulse\">\n      \u003Cdiv className=\"h-8 bg-gray-200 rounded w-1\u002F4 mb-4\" \u002F>\n      \u003Cdiv className=\"space-y-3\">\n        \u003Cdiv className=\"h-4 bg-gray-200 rounded w-full\" \u002F>\n        \u003Cdiv className=\"h-4 bg-gray-200 rounded w-5\u002F6\" \u002F>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Error Boundary\n\n```tsx\n\u002F\u002F app\u002Fdashboard\u002Ferror.tsx\n'use client'\n\nexport default function Error({ error, reset }: { error: Error; reset: () => void }) {\n  return (\n    \u003Cdiv className=\"p-4 bg-red-50 border border-red-200 rounded\">\n      \u003Ch2 className=\"text-red-800 font-bold\">Something went wrong!\u003C\u002Fh2>\n      \u003Cp className=\"text-red-600\">{error.message}\u003C\u002Fp>\n      \u003Cbutton onClick={reset} className=\"mt-2 px-4 py-2 bg-red-600 text-white rounded\">\n        Try again\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### Not Found\n\n```tsx\n\u002F\u002F app\u002Fposts\u002F[slug]\u002Fpage.tsx\nimport { notFound } from 'next\u002Fnavigation'\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 \u003Carticle>{post.content}\u003C\u002Farticle>\n}\n```\n\n---\n\n## Server Actions\n\n### Defining Actions\n\n```tsx\n\u002F\u002F app\u002Factions.ts\n'use server'\n\nimport { z } from 'zod'\nimport { revalidatePath } from 'next\u002Fcache'\nimport { redirect } from 'next\u002Fnavigation'\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('\u002Fposts')\n  redirect(`\u002Fposts\u002F${post.slug}`)\n}\n```\n\n### Form with useFormState and useFormStatus\n\n```tsx\n\u002F\u002F components\u002Fsubmit-button.tsx\n'use client'\nimport { useFormStatus } from 'react-dom'\n\nexport function SubmitButton() {\n  const { pending } = useFormStatus()\n  return (\n    \u003Cbutton type=\"submit\" disabled={pending}>\n      {pending ? 'Submitting...' : 'Submit'}\n    \u003C\u002Fbutton>\n  )\n}\n\n\u002F\u002F components\u002Fcreate-post-form.tsx\n'use client'\nimport { useFormState } from 'react-dom'\nimport { createPost } from '@\u002Fapp\u002Factions'\n\nexport function CreatePostForm() {\n  const [state, formAction] = useFormState(createPost, {})\n  return (\n    \u003Cform action={formAction}>\n      \u003Cinput name=\"title\" \u002F>\n      {state.error?.title && \u003Cp className=\"text-red-500\">{state.error.title[0]}\u003C\u002Fp>}\n      \u003Ctextarea name=\"content\" \u002F>\n      \u003CSubmitButton \u002F>\n    \u003C\u002Fform>\n  )\n}\n```\n\n### Optimistic Updates\n\n```tsx\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    \u003C>\n      \u003Cform action={handleSubmit}>\n        \u003Cinput name=\"title\" \u002F>\n        \u003Cbutton>Add\u003C\u002Fbutton>\n      \u003C\u002Fform>\n      \u003Cul>\n        {optimisticTodos.map(todo => (\n          \u003Cli key={todo.id} className={todo.id === 'temp' ? 'opacity-50' : ''}>{todo.title}\u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002F>\n  )\n}\n```\n\n### Revalidation\n\n```tsx\n'use server'\nimport { revalidatePath, revalidateTag } from 'next\u002Fcache'\n\nexport async function updatePost(id: string, formData: FormData) {\n  await db.post.update({ where: { id }, data: { ... } })\n\n  revalidateTag(`post-${id}`)     \u002F\u002F Invalidate by cache tag\n  revalidatePath('\u002Fposts')         \u002F\u002F Invalidate specific page\n  revalidatePath(`\u002Fposts\u002F${id}`)   \u002F\u002F Invalidate dynamic route\n  revalidatePath('\u002Fposts', 'layout') \u002F\u002F Invalidate layout and all pages under it\n}\n```\n\n---\n\n## Route Handlers (API Routes)\n\n### Basic CRUD\n\n```tsx\n\u002F\u002F app\u002Fapi\u002Fposts\u002Froute.ts\nimport { NextRequest, NextResponse } from 'next\u002Fserver'\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```\n\n### Dynamic Route Handler\n\n```tsx\n\u002F\u002F app\u002Fapi\u002Fposts\u002F[id]\u002Froute.ts\nexport async function GET(request: Request, { params }: { params: Promise\u003C{ 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\u003C{ id: string }> }) {\n  const { id } = await params\n  await db.post.delete({ where: { id } })\n  return new NextResponse(null, { status: 204 })\n}\n```\n\n### Streaming \u002F SSE\n\n```tsx\nexport async function GET() {\n  const encoder = new TextEncoder()\n  const stream = new ReadableStream({\n    async start(controller) {\n      for (let i = 0; i \u003C 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\u002Fevent-stream', 'Cache-Control': 'no-cache' },\n  })\n}\n```\n\n---\n\n## Parallel and Intercepting Routes\n\n### Parallel Routes (Slots)\n\n```\napp\u002F\n├── @modal\u002F\n│   ├── (.)photo\u002F[id]\u002Fpage.tsx   # Intercepted route (modal)\n│   └── default.tsx\n├── photo\u002F[id]\u002Fpage.tsx          # Full page route\n├── layout.tsx\n└── page.tsx\n```\n\n```tsx\n\u002F\u002F app\u002Flayout.tsx\nexport default function Layout({ children, modal }: {\n  children: React.ReactNode\n  modal: React.ReactNode\n}) {\n  return \u003C>{children}{modal}\u003C\u002F>\n}\n```\n\n### Modal Component\n\n```tsx\n'use client'\nimport { useRouter } from 'next\u002Fnavigation'\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter()\n  return (\n    \u003Cdiv className=\"fixed inset-0 bg-black\u002F50 flex items-center justify-center\"\n         onClick={() => router.back()}>\n      \u003Cdiv className=\"bg-white rounded-lg p-6 max-w-2xl\" onClick={e => e.stopPropagation()}>\n        {children}\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n---\n\n## Authentication (NextAuth.js v5 \u002F Auth.js)\n\n### Setup\n\n```tsx\n\u002F\u002F auth.ts\nimport NextAuth from 'next-auth'\nimport GitHub from 'next-auth\u002Fproviders\u002Fgithub'\nimport Credentials from 'next-auth\u002Fproviders\u002Fcredentials'\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\u002F\u002F app\u002Fapi\u002Fauth\u002F[...nextauth]\u002Froute.ts\nimport { handlers } from '@\u002Fauth'\nexport const { GET, POST } = handlers\n```\n\n### Middleware Protection\n\n```tsx\n\u002F\u002F middleware.ts\nexport { auth as middleware } from '@\u002Fauth'\n\nexport const config = {\n  matcher: ['\u002Fdashboard\u002F:path*', '\u002Fapi\u002Fprotected\u002F:path*'],\n}\n```\n\n### Server Component Auth Check\n\n```tsx\nimport { auth } from '@\u002Fauth'\nimport { redirect } from 'next\u002Fnavigation'\n\nexport default async function DashboardPage() {\n  const session = await auth()\n  if (!session) redirect('\u002Flogin')\n  return \u003Ch1>Welcome, {session.user?.name}\u003C\u002Fh1>\n}\n```\n\n### Server Action Auth Check\n\n```tsx\n'use server'\nimport { auth } from '@\u002Fauth'\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('\u002Fposts')\n}\n```\n\n---\n\n## Route Segment Config\n\n```tsx\nexport const dynamic = 'force-dynamic'    \u002F\u002F 'auto' | 'force-dynamic' | 'error' | 'force-static'\nexport const revalidate = 3600            \u002F\u002F seconds\nexport const runtime = 'nodejs'           \u002F\u002F or 'edge'\nexport const maxDuration = 30             \u002F\u002F seconds\n```\n\n---\n\n## Anti-Patterns to Avoid\n\n1. ❌ Adding `'use client'` to entire pages — push it down to interactive leaves\n2. ❌ Fetching data in Client Components when it could be a Server Component\n3. ❌ Sequential `await` when fetches are independent — use `Promise.all()`\n4. ❌ Passing functions as props across server\u002Fclient boundary (use Server Actions)\n5. ❌ Using `useEffect` for data fetching in App Router (use async Server Components)\n6. ❌ Forgetting `await params` in Next.js 15 (they're Promises now)\n7. ❌ Missing `loading.tsx` or `\u003CSuspense>` boundaries for async pages\n8. ❌ Not validating Server Action inputs (always validate with zod)\n",{"name":5,"role":48,"scope":49,"version":50,"triggers":51,"description":7,"output-format":70},"specialist","implementation","1.0.0",[36,52,24,53,29,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69],"Next","App Router","Client Components","Server Actions","use server","use client","Route Handler","middleware","layout.tsx","page.tsx","loading.tsx","error.tsx","revalidatePath","revalidateTag","NextAuth","Auth.js","generateStaticParams","generateMetadata","code","master","8ec37df6d71fb664ea19d32c079e7d3ff931a8e0","skills\u002Fcontent\u002Fnextjs-expert",3,"https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit","https:\u002F\u002Fweblyfe.ai","https:\u002F\u002Fgithub.com\u002FS3YED","2026-08-27 01:54:02",{"source":8,"skillName":5,"commands":80,"prompt":90,"zipAvailable":91},[81,84,87],{"tool":82,"command":83},"npx","npx skills add S3YED\u002Fappie-kit --skill nextjs-expert",{"tool":85,"command":86},"bunx","bunx skills add S3YED\u002Fappie-kit --skill nextjs-expert",{"tool":88,"command":89},"pnpm","pnpm dlx skills add S3YED\u002Fappie-kit --skill nextjs-expert","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fcontent\u002Fnextjs-expert\u002FSKILL.md",true,[93],{"path":94,"resourceType":95,"resourceTypeDesc":96,"sizeBytes":97,"sourceHtmlUrl":98},".clawhub\u002Forigin.json","REFERENCE","参考文档",145,"https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fcontent\u002Fnextjs-expert\u002F.clawhub\u002Forigin.json",[100,134,168,193,224,247],{"skillNo":101,"name":102,"nameZh":6,"description":103,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":104,"categories":105,"keywords":115,"publishedAt":43},"01M6JFD4M6N4DSGC7TXEJ5R3J2","ai-music","AI music generation and audio tools: HeartMuLa lyrics-to-music generation, songwriting craft (+ Suno prompts), and audio spectrogram\u002Ffeature visualization.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fwethlete\u002Fai-music\u002FSKILL.md",[106,109,112],{"categoryNo":107,"categoryName":108,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":110,"categoryName":111,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":113,"categoryName":114,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",[116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133],"music","ai music","humanizer","visualization","Tools","Audio","hermes","hermes agent","songwriting","suno","lyrics","prompts","heartmula","craft","generation","feature","ai music generation","music generation",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":148,"publishedAt":43},"01M6JFCZE4XNFQFMNZDHKHT5V5","agentic-webdesign-design-system-ingestion","Use v0 Design Systems 2.0, Bolt Design System Agents, or Lovable to generate production UI from a client's real design system components — not generic Tailwind. Use when building a client website where brand consistency matters.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-design-system-ingestion\u002FSKILL.md",[140,143],{"categoryNo":141,"categoryName":142,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":144,"categoryName":145,"taxonomyType":146,"taxonomyTypeDesc":147},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",[149,150,151,152,153,154,155,34,156,157,158,159,160,161,162,163,164,165,166,167],"design","agentic","design system","system","ingestion","webdesign","agentic webdesign design system ingestion","generate","design systems","website","ui","Tailwind","bolt","lovable","v0","design-system","wed","design system components","appie",{"skillNo":169,"name":170,"nameZh":6,"description":171,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":172,"categories":173,"keywords":176,"publishedAt":43},"01M6JFCVPX3913CFAS6QQJ33ZB","agentic-webdesign-agent-elements","Use 21st.dev Agent Elements — 25 shadcn\u002Fui components purpose-built for AI agent UIs. Tool cards (Bash, Edit, Search, Plan, Subagent, MCP), chat, input, streaming. Open source, shadcn-registry install, no runtime lock-in.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fweb-design\u002Fagentic-webdesign-agent-elements\u002FSKILL.md",[174,175],{"categoryNo":113,"categoryName":114,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":144,"categoryName":145,"taxonomyType":146,"taxonomyTypeDesc":147},[150,177,154,178,179,180,34,181,182,40,183,184,185,186,187,188,189,190,191,192],"elements","agentic webdesign agent elements","MCP","ai agent","registry","search","open source","open","Bash","plan","subagent","edit","install","input","lock","source",{"skillNo":194,"name":195,"nameZh":6,"description":196,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":197,"categories":198,"keywords":203,"publishedAt":43},"01M6JFCQC7H65M9QJ6TRE71BS1","vercel-deployment","Deploy static sites and frontends to Vercel — handles both CLI and REST API paths, including the CLI token sanitization workaround.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fvercel-deployment\u002FSKILL.md",[199,202],{"categoryNo":200,"categoryName":201,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":113,"categoryName":114,"taxonomyType":20,"taxonomyTypeDesc":21},[204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223],"deployment","vercel deployment","vercel","macos","cli","linux","windows","deploy","vercel deploy","REST","api","site","deploy to vercel","token","rest api","host","sites","paths","sanitization","static site",{"skillNo":225,"name":226,"nameZh":6,"description":227,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":228,"categories":229,"keywords":231,"publishedAt":43},"01M6JFCM4SMTXBM5A01BK2YQG1","software-quality-workflows","Umbrella workflow for TDD, debugging, code simplification, spikes, pre-commit review, and debugger-assisted investigation.","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Fsoftware-quality-workflows\u002FSKILL.md",[230],{"categoryNo":113,"categoryName":114,"taxonomyType":20,"taxonomyTypeDesc":21},[232,233,234,235,236,237,238,239,240,70,241,242,243,244,245,246,28],"workflows","quality","software","software quality workflows","review","tdd","workflow","commit","debugging","debugger","pre-commit","investigation","code simplification","user","simplification",{"skillNo":248,"name":249,"nameZh":6,"description":250,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":251,"categories":252,"keywords":260,"publishedAt":43},"01M6JFCGD0M6CEATY01W7YN6DB","react-component-refactoring","Safely refactor large React components into smaller, maintainable pieces using proven patterns","https:\u002F\u002Fgithub.com\u002FS3YED\u002Fappie-kit\u002Fblob\u002Fmaster\u002Fskills\u002Fsoftware-development\u002Freact-component-refactoring\u002FSKILL.md",[253,256,259],{"categoryNo":254,"categoryName":255,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000007","代码审查",{"categoryNo":257,"categoryName":258,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":113,"categoryName":114,"taxonomyType":20,"taxonomyTypeDesc":21},[261,262,263,264,265,266,267,268,207,269,270,34,271,210,272,70,273,274,275,276],"react","component","refactoring","component refactoring","react component","react component refactoring","frontend","writing","safe","typescript","react components","modular","refactor","simplify","patterns","test-driven-development"]