โ† All skills
Tencent SkillHub ยท Developer Tools

Nextjs Expert

Use when building Next.js 14/15 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.

skill openclawclawhub Free
0 Downloads
0 Stars
0 Installs
0 Score
High Signal

Use when building Next.js 14/15 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.

โฌ‡ 0 downloads โ˜… 0 stars Unverified but indexed

Install for OpenClaw

Quick setup
  1. Download the package from Yavira.
  2. Extract the archive and review SKILL.md first.
  3. Import or place the package into your OpenClaw setup.

Requirements

Target platform
OpenClaw
Install method
Manual import
Extraction
Extract archive
Prerequisites
OpenClaw
Primary doc
SKILL.md

Package facts

Download mode
Yavira redirect
Package format
ZIP package
Source platform
Tencent SkillHub
What's included
SKILL.md

Validation

  • Use the Yavira download entry.
  • Review SKILL.md after the package is downloaded.
  • Confirm the extracted package contains the expected setup assets.

Install with your agent

Agent handoff

Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.

  1. Download the package from Yavira.
  2. Extract it into a folder your agent can access.
  3. Paste one of the prompts below and point your agent at the extracted folder.
New install

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.

Upgrade existing

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.

Trust & source

Release facts

Source
Tencent SkillHub
Verification
Indexed source record
Version
1.0.0

Documentation

ClawHub primary doc Primary doc: SKILL.md 39 sections Open source page

Next.js Expert

Comprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT).

Role Definition

You are a senior Next.js engineer specializing in the App Router, React Server Components, and production-grade full-stack applications with TypeScript.

Core Principles

Server-first: Components are Server Components by default. Only add 'use client' when you need hooks, event handlers, or browser APIs. Push client boundaries down: Keep 'use client' as low in the tree as possible. Async params: In Next.js 15, params and searchParams are Promise types โ€” always await them. Colocation: Keep components, tests, and styles near their routes. Type everything: Use TypeScript strictly.

Route Files

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)

Folder Conventions

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/

File Hierarchy (render order)

layout.tsx โ†’ 2. template.tsx โ†’ 3. error.tsx (boundary) โ†’ 4. loading.tsx (boundary) โ†’ 5. not-found.tsx (boundary) โ†’ 6. page.tsx

Basic Page (Server Component)

// app/about/page.tsx export default function AboutPage() { return ( <main> <h1>About Us</h1> <p>Welcome to our company.</p> </main> ) }

Dynamic Routes

// app/blog/[slug]/page.tsx interface PageProps { params: Promise<{ slug: string }> } export default async function BlogPost({ params }: PageProps) { const { slug } = await params const post = await getPost(slug) return <article>{post.content}</article> }

Search Params

// app/search/page.tsx interface PageProps { searchParams: Promise<{ q?: string; page?: string }> } export default async function SearchPage({ searchParams }: PageProps) { const { q, page } = await searchParams const results = await search(q, parseInt(page || '1')) return <SearchResults results={results} /> }

Static Generation

export async function generateStaticParams() { const posts = await getAllPosts() return posts.map((post) => ({ slug: post.slug })) } // Allow dynamic params not in generateStaticParams export const dynamicParams = true

Root Layout (Required)

// app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }

Nested Layout with Data Fetching

// app/dashboard/layout.tsx import { getUser } from '@/lib/get-user' export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const user = await getUser() return ( <div className="flex"> <Sidebar user={user} /> <main className="flex-1 p-6">{children}</main> </div> ) }

Route Groups for Multiple Root Layouts

app/ โ”œโ”€โ”€ (marketing)/ โ”‚ โ”œโ”€โ”€ layout.tsx # Marketing layout with <html>/<body> โ”‚ โ””โ”€โ”€ about/page.tsx โ””โ”€โ”€ (app)/ โ”œโ”€โ”€ layout.tsx # App layout with <html>/<body> โ””โ”€โ”€ dashboard/page.tsx

Metadata

// Static export const metadata: Metadata = { title: 'About Us', description: 'Learn more about our company', } // Dynamic export async function generateMetadata({ params }: PageProps): Promise<Metadata> { const { slug } = await params const post = await getPost(slug) return { title: post.title, openGraph: { title: post.title, images: [post.coverImage] }, } } // Template in layouts export const metadata: Metadata = { title: { template: '%s | Dashboard', default: 'Dashboard' }, }

Decision Guide

Server Component (default) when: Fetching data or accessing backend resources Keeping sensitive info on server (API keys, tokens) Reducing client JavaScript bundle No interactivity needed Client Component ('use client') when: Using useState, useEffect, useReducer Using event handlers (onClick, onChange) Using browser APIs (window, document) Using custom hooks with state

Composition Patterns

Pattern 1: Server data โ†’ Client interactivity // app/products/page.tsx (Server) export default async function ProductsPage() { const products = await getProducts() return <ProductFilter products={products} /> } // components/product-filter.tsx (Client) 'use client' export function ProductFilter({ products }: { products: Product[] }) { const [filter, setFilter] = useState('') const filtered = products.filter(p => p.name.includes(filter)) return ( <> <input onChange={e => setFilter(e.target.value)} /> {filtered.map(p => <ProductCard key={p.id} product={p} />)} </> ) } Pattern 2: Children as Server Components // components/client-wrapper.tsx 'use client' export function ClientWrapper({ children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false) return ( <div> <button onClick={() => setIsOpen(!isOpen)}>Toggle</button> {isOpen && children} </div> ) } // app/page.tsx (Server) export default function Page() { return ( <ClientWrapper> <ServerContent /> {/* Still renders on server! */} </ClientWrapper> ) } Pattern 3: Providers at the boundary // app/providers.tsx 'use client' import { ThemeProvider } from 'next-themes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const queryClient = new QueryClient() export function Providers({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}> <ThemeProvider attribute="class" defaultTheme="system"> {children} </ThemeProvider> </QueryClientProvider> ) }

Shared Data with cache()

import { cache } from 'react' export const getUser = cache(async () => { const response = await fetch('/api/user') return response.json() }) // Both layout and page call getUser() โ€” only one fetch happens

Async Server Components

export default async function PostsPage() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()) return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul> }

Parallel Data Fetching

export default async function DashboardPage() { const [user, posts, analytics] = await Promise.all([ getUser(), getPosts(), getAnalytics() ]) return <Dashboard user={user} posts={posts} analytics={analytics} /> }

Streaming with Suspense

import { Suspense } from 'react' export default function DashboardPage() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<StatsSkeleton />}> <SlowStats /> </Suspense> <Suspense fallback={<ChartSkeleton />}> <SlowChart /> </Suspense> </div> ) }

Caching

// Cache indefinitely (static) const data = await fetch('https://api.example.com/data') // Revalidate every hour const data = await fetch(url, { next: { revalidate: 3600 } }) // No caching (always fresh) const data = await fetch(url, { cache: 'no-store' }) // Cache with tags const data = await fetch(url, { next: { tags: ['posts'] } })

Loading UI

// app/dashboard/loading.tsx export default function Loading() { return ( <div className="animate-pulse"> <div className="h-8 bg-gray-200 rounded w-1/4 mb-4" /> <div className="space-y-3"> <div className="h-4 bg-gray-200 rounded w-full" /> <div className="h-4 bg-gray-200 rounded w-5/6" /> </div> </div> ) }

Error Boundary

// app/dashboard/error.tsx 'use client' export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div className="p-4 bg-red-50 border border-red-200 rounded"> <h2 className="text-red-800 font-bold">Something went wrong!</h2> <p className="text-red-600">{error.message}</p> <button onClick={reset} className="mt-2 px-4 py-2 bg-red-600 text-white rounded"> Try again </button> </div> ) }

Not Found

// app/posts/[slug]/page.tsx import { notFound } from 'next/navigation' export default async function PostPage({ params }: PageProps) { const { slug } = await params const post = await getPost(slug) if (!post) notFound() return <article>{post.content}</article> }

Defining Actions

// app/actions.ts 'use server' import { z } from 'zod' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' const schema = z.object({ title: z.string().min(1).max(200), content: z.string().min(10), }) export async function createPost(formData: FormData) { const session = await auth() if (!session?.user) throw new Error('Unauthorized') const parsed = schema.safeParse({ title: formData.get('title'), content: formData.get('content'), }) if (!parsed.success) return { error: parsed.error.flatten() } const post = await db.post.create({ data: { ...parsed.data, authorId: session.user.id }, }) revalidatePath('/posts') redirect(`/posts/${post.slug}`) }

Form with useFormState and useFormStatus

// components/submit-button.tsx 'use client' import { useFormStatus } from 'react-dom' export function SubmitButton() { const { pending } = useFormStatus() return ( <button type="submit" disabled={pending}> {pending ? 'Submitting...' : 'Submit'} </button> ) } // components/create-post-form.tsx 'use client' import { useFormState } from 'react-dom' import { createPost } from '@/app/actions' export function CreatePostForm() { const [state, formAction] = useFormState(createPost, {}) return ( <form action={formAction}> <input name="title" /> {state.error?.title && <p className="text-red-500">{state.error.title[0]}</p>} <textarea name="content" /> <SubmitButton /> </form> ) }

Optimistic Updates

'use client' import { useOptimistic, useTransition } from 'react' export function TodoList({ initialTodos }: { initialTodos: Todo[] }) { const [isPending, startTransition] = useTransition() const [optimisticTodos, addOptimistic] = useOptimistic( initialTodos, (state, newTodo: string) => [...state, { id: 'temp', title: newTodo, completed: false }] ) async function handleSubmit(formData: FormData) { const title = formData.get('title') as string startTransition(async () => { addOptimistic(title) await addTodo(formData) }) } return ( <> <form action={handleSubmit}> <input name="title" /> <button>Add</button> </form> <ul> {optimisticTodos.map(todo => ( <li key={todo.id} className={todo.id === 'temp' ? 'opacity-50' : ''}>{todo.title}</li> ))} </ul> </> ) }

Revalidation

'use server' import { revalidatePath, revalidateTag } from 'next/cache' export async function updatePost(id: string, formData: FormData) { await db.post.update({ where: { id }, data: { ... } }) revalidateTag(`post-${id}`) // Invalidate by cache tag revalidatePath('/posts') // Invalidate specific page revalidatePath(`/posts/${id}`) // Invalidate dynamic route revalidatePath('/posts', 'layout') // Invalidate layout and all pages under it }

Basic CRUD

// app/api/posts/route.ts import { NextRequest, NextResponse } from 'next/server' export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const page = parseInt(searchParams.get('page') ?? '1') const limit = parseInt(searchParams.get('limit') ?? '10') const [posts, total] = await Promise.all([ db.post.findMany({ skip: (page - 1) * limit, take: limit }), db.post.count(), ]) return NextResponse.json({ data: posts, pagination: { page, limit, total } }) } export async function POST(request: NextRequest) { const body = await request.json() const post = await db.post.create({ data: body }) return NextResponse.json(post, { status: 201 }) }

Dynamic Route Handler

// app/api/posts/[id]/route.ts export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params const post = await db.post.findUnique({ where: { id } }) if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 }) return NextResponse.json(post) } export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params await db.post.delete({ where: { id } }) return new NextResponse(null, { status: 204 }) }

Streaming / SSE

export async function GET() { const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 10; i++) { controller.enqueue(encoder.encode(`data: ${JSON.stringify({ count: i })}\n\n`)) await new Promise(r => setTimeout(r, 1000)) } controller.close() }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }, }) }

Parallel Routes (Slots)

app/ โ”œโ”€โ”€ @modal/ โ”‚ โ”œโ”€โ”€ (.)photo/[id]/page.tsx # Intercepted route (modal) โ”‚ โ””โ”€โ”€ default.tsx โ”œโ”€โ”€ photo/[id]/page.tsx # Full page route โ”œโ”€โ”€ layout.tsx โ””โ”€โ”€ page.tsx // app/layout.tsx export default function Layout({ children, modal }: { children: React.ReactNode modal: React.ReactNode }) { return <>{children}{modal}</> }

Modal Component

'use client' import { useRouter } from 'next/navigation' export function Modal({ children }: { children: React.ReactNode }) { const router = useRouter() return ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center" onClick={() => router.back()}> <div className="bg-white rounded-lg p-6 max-w-2xl" onClick={e => e.stopPropagation()}> {children} </div> </div> ) }

Setup

// auth.ts import NextAuth from 'next-auth' import GitHub from 'next-auth/providers/github' import Credentials from 'next-auth/providers/credentials' export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }), Credentials({ credentials: { email: {}, password: {} }, authorize: async (credentials) => { const user = await getUserByEmail(credentials.email as string) if (!user || !await verifyPassword(credentials.password as string, user.password)) return null return user }, }), ], callbacks: { jwt: ({ token, user }) => { if (user) { token.id = user.id; token.role = user.role } return token }, session: ({ session, token }) => { session.user.id = token.id as string; session.user.role = token.role as string; return session }, }, }) // app/api/auth/[...nextauth]/route.ts import { handlers } from '@/auth' export const { GET, POST } = handlers

Middleware Protection

// middleware.ts export { auth as middleware } from '@/auth' export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'], }

Server Component Auth Check

import { auth } from '@/auth' import { redirect } from 'next/navigation' export default async function DashboardPage() { const session = await auth() if (!session) redirect('/login') return <h1>Welcome, {session.user?.name}</h1> }

Server Action Auth Check

'use server' import { auth } from '@/auth' export async function deletePost(id: string) { const session = await auth() if (!session?.user) throw new Error('Unauthorized') const post = await db.post.findUnique({ where: { id } }) if (post?.authorId !== session.user.id) throw new Error('Forbidden') await db.post.delete({ where: { id } }) revalidatePath('/posts') }

Route Segment Config

export const dynamic = 'force-dynamic' // 'auto' | 'force-dynamic' | 'error' | 'force-static' export const revalidate = 3600 // seconds export const runtime = 'nodejs' // or 'edge' export const maxDuration = 30 // seconds

Anti-Patterns to Avoid

โŒ Adding 'use client' to entire pages โ€” push it down to interactive leaves โŒ Fetching data in Client Components when it could be a Server Component โŒ Sequential await when fetches are independent โ€” use Promise.all() โŒ Passing functions as props across server/client boundary (use Server Actions) โŒ Using useEffect for data fetching in App Router (use async Server Components) โŒ Forgetting await params in Next.js 15 (they're Promises now) โŒ Missing loading.tsx or <Suspense> boundaries for async pages โŒ Not validating Server Action inputs (always validate with zod)

Category context

Code helpers, APIs, CLIs, browser automation, testing, and developer operations.

Source: Tencent SkillHub

Largest current source with strong distribution and engagement signals.

Package contents

Included in package
1 Docs
  • SKILL.md Primary doc