Token导航 LogoToken导航

Agent Skill · 研究检索

nextjs-expert(Next.js expert 前端)

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

nextjs-expert 是研究检索类 Agent Skill,可用宿主为 OpenClaw,通过ClawHub获取,权限为操作浏览器;许可证为 MIT-0,第三方扫描已通过,按安装量在研究检索类的 73,675 个 Skill 里排第 433。

总安装 290,072周安装 12,333GitHub Stars 12下载 101,624核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
MIT-0
版本
1.0.0

安装方式

通过对话安装

复制提示词发给支持本地命令或 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

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

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

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 OpenClaw;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 openclaw skills install nextjs-expert;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要操作浏览器,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

nextjs-expert 怎么安装?

在终端执行 openclaw skills install nextjs-expert。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

nextjs-expert 支持哪些 AI 工具?

公开资料标注支持 OpenClaw。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

nextjs-expert 需要什么权限?

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

nextjs-expert 是免费的吗?

来源标注的许可证是 MIT-0。本站不代收任何费用,也不托管安装包;能否商用、要不要署名,以许可证原文为准。

nextjs-expert 用的人多吗?

公开数据是累计安装 290,072 次,近一周安装 12,333 次,下载 101,624 次,GitHub 12 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

nextjs-expert 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 专注 OpenClaw 一种宿主,触发方式和使用习惯不用来回切换。
  • 许可证明确为 MIT-0,商用和分发边界好判断。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 OpenClaw,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。
  • 需要许可证清楚、并且有第三方扫描结果的团队项目。

不太适合

  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
find-skills

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
nextjs-expert

Next.js expert 前端 · 本页

290,07212操作浏览器MIT-0通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

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)

平台分布

OpenClaw

97.86%
按下载量换算99,449

安全审计

VirusTotal

通过

ClawScan

通过

Static analysis

未展示

权限和风险

操作浏览器

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

安装前确认

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

继续浏览同类 Skills