Token导航 LogoToken导航TokenDH.com
研究检索操作浏览器clawhub未标认证来源可访问clear审计通过

nextjs-expertNext.js expert 前端

Agent Skill

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

总安装

290,072

周安装

12,333

GitHub Stars

12

下载量

101,624
OpenClaw

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

MIT-0

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:nextjs-expert(Next.js expert 前端)
来源仓库:https://github.com/jgarrison929/nextjs-expert
安装命令:
openclaw skills install nextjs-expert
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 OpenClaw 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

ClawHubOpenClaw
openclaw skills install nextjs-expert

简介

使用 App Router 构建 Next.js 14/15 应用程序时使用。调用路由、布局、服务器组件、客户端组件、服务器操作、路由处理程序、身份验证、中间件、数据获取、缓存、重新验证、流式传输、挂起、加载状态、错误边界、动态路由、并行路由、拦截路由或任何 Next.js 架构问题。

SKILL.md

name
nextjs-expert
version
1.0.0
description
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.
triggers
role
specialist
scope
implementation
output-format
code

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

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

App Router File Conventions

Route Files

FilePurpose
page.tsxUnique UI for a route, makes it publicly accessible
layout.tsxShared UI wrapper, preserves state across navigations
loading.tsxLoading UI using React Suspense
error.tsxError boundary for route segment (must be 'use client')
not-found.tsxUI for 404 responses
template.tsxLike layout but re-renders on navigation
default.tsxFallback for parallel routes
route.tsAPI endpoint (Route Handler)

Folder Conventions

PatternPurposeExample
folder/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)

  1. layout.tsx → 2. template.tsx → 3. error.tsx (boundary) → 4. loading.tsx (boundary) → 5. not-found.tsx (boundary) → 6. page.tsx

Pages and Routing

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

Layouts

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' },
}

Server Components vs Client Components

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

Data Fetching

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 and Error States

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>
}

Server Actions

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
}

Route Handlers (API Routes)

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 })}\
\
`))
        await new Promise(r => setTimeout(r, 1000))
      }
      controller.close()
    },
  })
  return new Response(stream, {
    headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
  })
}

Parallel and Intercepting Routes

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>
  )
}

Authentication (NextAuth.js v5 / Auth.js)

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

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

适合场景

01

OpenClaw 用户查找和安装 Skill 时

02

用户想查找某类 Agent Skill 时

03

需要根据任务场景推荐可安装能力包时

04

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

OpenClaw

97.86%
按下载量换算99,449

安全审计

VirusTotal

通过

ClawScan

通过

Static analysis

未展示

权限和风险

操作浏览器

该 Skill 可能涉及浏览器控制能力,使用时可能读取或操作网页内容,需要在受控环境中确认权限边界。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills