npx claudepluginhub bee-coded/bee-dev --plugin beeThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
These standards apply when the project stack is nextjs. All agents and implementations must follow these conventions.
Also read skills/standards/frontend/SKILL.md for universal frontend standards (component architecture, accessibility, responsive design, CSS methodology, design quality) that apply alongside these Next.js-specific conventions.
app/ directory. Each folder is a route segment.page.tsx -- The UI for a route. Required to make a route publicly accessible.layout.tsx -- Shared UI that wraps child routes. Persists across navigation (no re-render).loading.tsx -- Instant loading UI shown while route content streams in. Wraps the page in a Suspense boundary automatically.error.tsx -- Error boundary for a route segment. Must be a Client Component. Receives error and reset props.not-found.tsx -- UI for 404 within the route segment. Triggered by notFound() or when no route matches.global-error.tsx -- Error boundary for the root layout. Must be a Client Component. Must define its own <html> and <body> tags since it replaces the root layout.template.tsx -- Like layout but re-mounts on navigation. Use for animations or per-page state reset.default.tsx -- Fallback UI for parallel route slots when the slot's active state cannot be recovered after navigation.(name) for organizing without affecting URL: app/(marketing)/about/page.tsx.[param]. In Next.js 15, params is a Promise and must be awaited.[...param] and optional catch-all with [[...param]].route.ts for API endpoints: app/api/users/route.ts.app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── global-error.tsx # Root error boundary
├── not-found.tsx # Global 404
├── (marketing)/
│ ├── about/page.tsx # /about
│ └── blog/page.tsx # /blog
├── dashboard/
│ ├── layout.tsx # Dashboard layout
│ ├── loading.tsx # Dashboard loading UI
│ ├── error.tsx # Dashboard error boundary
│ ├── page.tsx # /dashboard
│ ├── @sidebar/ # Parallel route slot
│ │ ├── default.tsx # Fallback for sidebar
│ │ └── page.tsx # Sidebar content
│ └── orders/
│ ├── page.tsx # /dashboard/orders
│ └── [id]/page.tsx # /dashboard/orders/:id
├── @modal/ # Parallel route for modals
│ ├── default.tsx # No modal by default
│ └── (.)login/page.tsx # Intercepted /login as modal
└── api/
└── users/route.ts # API: /api/users
app/ is a Server Component unless marked with 'use client'.'use client' only when the component needs: event handlers, hooks (useState, useEffect, useActionState), browser APIs, or third-party client libraries.'use client' as far down the tree as possible.server-only and client-only packages to enforce import boundaries at build time.Decision tree: Use a Client Component only if the component uses hooks, attaches event handlers, accesses browser APIs, or depends on a client-only third-party library. Everything else stays as a Server Component.
// Server Component (default) -- fetches data directly
export default async function OrdersPage() {
const orders = await db.orders.findMany();
return (
<div>
<h1>Orders</h1>
<OrderFilters /> {/* Client Component for interactivity */}
{orders.map((order) => <OrderCard key={order.id} order={order} />)}
</div>
);
}
// Client Component -- handles user interaction
'use client';
export function OrderFilters() {
const [search, setSearch] = useState('');
return <input value={search} onChange={(e) => setSearch(e.target.value)} />;
}
Next.js 15 uses React 19. Use these hooks in Client Components:
useActionState(fn, initialState) -- manage Server Action state with automatic pending tracking. Returns [state, formAction, isPending]. Replaces the deprecated useFormState.useOptimistic(state, updateFn) -- optimistic UI updates during async Server Actions. Show the expected result immediately, roll back on failure.useFormStatus() (from react-dom) -- read parent form submission status. Must be rendered inside a <form>. Use in submit buttons for loading state.use() -- read promises and context directly in render. Can be called conditionally. Replaces useContext for context reading.'use client';
import { useActionState, useOptimistic } from 'react';
import { useFormStatus } from 'react-dom';
import { addItem } from '@/app/actions';
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? 'Adding...' : 'Add'}</button>;
}
function ItemForm({ items }: { items: Item[] }) {
const [optimisticItems, addOptimistic] = useOptimistic(items,
(state, newItem: string) => [...state, { id: crypto.randomUUID(), name: newItem, pending: true }]
);
const [state, formAction] = useActionState(async (prev: ActionState, formData: FormData) => {
const name = formData.get('name') as string;
addOptimistic(name);
return await addItem(prev, formData);
}, { errors: {} });
return (
<form action={formAction}>
<input name="name" required />
{state.errors?.name && <p>{state.errors.name}</p>}
<SubmitButton />
<ul>{optimisticItems.map(i => (
<li key={i.id} style={{ opacity: i.pending ? 0.5 : 1 }}>{i.name}</li>
))}</ul>
</form>
);
}
'use server' directive at the top of the function or file. Use for mutations (form submissions, data updates, deletions).<form action={serverAction}> work without JavaScript -- submission happens server-side even before hydration.redirect() outside try/catch blocks -- it throws internally and must not be caught.revalidatePath() or revalidateTag() after mutations to invalidate cached data.useActionState to display them.'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
const OrderSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
amount: z.coerce.number().positive('Must be positive'),
});
type ActionState = { errors?: Record<string, string[]>; message?: string };
export async function createOrder(prev: ActionState, formData: FormData): Promise<ActionState> {
const validated = OrderSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
amount: formData.get('amount'),
});
if (!validated.success) {
return { errors: validated.error.flatten().fieldErrors };
}
try {
await db.orders.create({ data: validated.data });
} catch (error) {
return { message: 'Database error: failed to create order.' };
}
revalidatePath('/orders');
redirect('/orders'); // Outside try/catch -- redirect throws internally
}
@slotName folders to render multiple page segments simultaneously in the same layout.children.loading.tsx, error.tsx, and independent navigation state.default.tsx is required in each slot as a fallback for soft navigation recovery.// app/dashboard/layout.tsx
export default function DashboardLayout({
children, sidebar, analytics,
}: {
children: React.ReactNode;
sidebar: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div className="grid grid-cols-[250px_1fr]">
<aside>{sidebar}</aside>
<main>{children}{analytics}</main>
</div>
);
}
(.) same level, (..) one level up, (..)(..) two levels up, (...) from root.@modal/(.)photo/[id]/page.tsx intercepts /photo/[id] into a modal slot.await data directly -- no useEffect, no client-side loading states.fetch() with caching options or direct database access (Prisma, Drizzle, raw SQL).<Suspense> boundaries with fallback UI for progressive streaming.Promise.all for multiple independent data sources to avoid waterfalls.route.ts) only for endpoints consumed by external clients (mobile apps, webhooks). For internal data, use Server Components directly.import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const orders = await db.orders.findMany({ where: { status: searchParams.get('status') } });
return NextResponse.json(orders);
}
Breaking change in Next.js 15: fetch() requests are no longer cached by default. Caching is now opt-in.
fetch() is not cached. Every request hits the server.cache: 'force-cache' per fetch, or export const fetchCache = 'default-cache' in a layout/page for all fetches in that segment.next: { revalidate: 60 } caches and re-fetches after 60 seconds.next: { tags: ['orders'] } with revalidateTag('orders') for on-demand invalidation.revalidatePath(path) for on-demand revalidation of a specific route.unstable_cache for caching non-fetch data (database queries, ORM calls). Accepts a function, cache keys, and options with revalidate and tags.cookies(), headers(), or searchParams.// Not cached (Next.js 15 default)
const data = await fetch('https://api.example.com/orders');
// Opt-in to caching with time-based revalidation and tags
const data = await fetch('https://api.example.com/orders', {
next: { revalidate: 60, tags: ['orders'] },
});
// Caching database queries
import { unstable_cache } from 'next/cache';
const getCachedOrders = unstable_cache(
async () => await db.orders.findMany(),
['orders'],
{ revalidate: 3600, tags: ['orders'] }
);
useState for component-level state in Client Components. Keep state close to where it is used.useReducer when state has multiple sub-values or defined transitions.revalidatePath/revalidateTag for mutations. The framework re-fetches server data automatically after revalidation.Detect what the project uses -- check package.json and follow THAT library's conventions. Do NOT introduce a different state library.
createAsyncThunk, RTK Query. Typed hooks (useAppDispatch, useAppSelector). Wrap providers in a Client Component.useStore(selector) over useStore(). Client Components only.QueryClientProvider in a Client Component.observer() HOC, class-based stores if project convention.If no external store is installed: React Context + useReducer for shared client state. For server data, rely on Server Components and Server Actions.
<form action={serverAction}> for progressive enhancement -- works without JavaScript.useActionState for pending states, validation errors, and optimistic UI.useForm, register, handleSubmit, formState. Combine with zodResolver.// Pattern: useActionState form with server-side Zod validation
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { createOrder } from '@/app/actions/orders';
function SubmitButton() {
const { pending } = useFormStatus();
return <button type="submit" disabled={pending}>{pending ? 'Creating...' : 'Create Order'}</button>;
}
export function CreateOrderForm() {
const [state, formAction] = useActionState(createOrder, { errors: {} });
return (
<form action={formAction}>
<input name="name" />
{state.errors?.name && <p>{state.errors.name[0]}</p>}
<input name="email" type="email" />
{state.errors?.email && <p>{state.errors.email[0]}</p>}
{state.message && <p>{state.message}</p>}
<SubmitButton />
</form>
);
}
error.tsx -- per-segment error boundary. Must be a Client Component. Receives error and reset. Catches errors in the page and children but NOT in the layout of the same segment.not-found.tsx -- triggered by notFound() from next/navigation. Define per segment for granular 404 pages.global-error.tsx -- catches errors in the root layout. Must define <html> and <body> tags.useActionState to display. Unexpected errors bubble to the nearest error.tsx.// app/dashboard/error.tsx
'use client';
export default function DashboardError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return (
<div role="alert">
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/global-error.tsx -- must define <html> and <body>
'use client';
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
return (
<html><body>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</body></html>
);
}
next/image for all images. Automatic optimization, lazy loading, responsive sizing, WebP/AVIF conversion.width/height or use fill with a sized parent to prevent layout shift.metadata export for static metadata; generateMetadata for dynamic. Include title, description, Open Graph.next/font for font optimization. Fonts are self-hosted automatically.params in generateMetadata is a Promise and must be awaited.export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const order = await db.orders.findUnique({ where: { id } });
return { title: `Order #${order?.number}`, description: `Details for order ${order?.number}` };
}
middleware.ts at the project root (not inside app/).export const config = { matcher: ['/dashboard/:path*'] }.import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
const response = NextResponse.next();
response.headers.set('x-pathname', request.nextUrl.pathname);
return response;
}
export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'] };
next dev --turbopack for the dev server. Significantly faster HMR and cold starts.--turbo flag is deprecated; use --turbopack.next.config.ts under the turbopack key (not webpack).userEvent for interactions.next/navigation (useRouter, usePathname, redirect) and next/headers (cookies, headers).vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn(), back: vi.fn() }),
usePathname: () => '/dashboard',
redirect: vi.fn(),
}));
test('renders dashboard page with orders', async () => {
const page = await DashboardPage();
render(page);
expect(screen.getByText('Orders')).toBeInTheDocument();
});
test('createOrder validates input and returns errors', async () => {
const formData = new FormData();
formData.set('name', '');
formData.set('email', 'invalid');
const result = await createOrder({ errors: {} }, formData);
expect(result.errors?.name).toBeDefined();
expect(result.errors?.email).toBeDefined();
});
getServerSideProps, getStaticProps, getInitialProps) in App Router projects.'use client' to layouts or pages unless they genuinely need browser APIs or hooks.server-only package to enforce this.useEffect for data fetching in Server Components -- fetch data directly with await.loading.tsx and error.tsx per route segment that fetches data.router.push for simple navigation -- use the <Link> component from next/link.'use client' boundaries unnecessarily -- each boundary adds to the client bundle.fetch() is cached by default -- Next.js 15 requires opt-in with cache: 'force-cache' or next: { revalidate }.redirect() inside a try/catch block -- it throws internally and will be caught.params or searchParams synchronously -- in Next.js 15 they are Promises and must be awaited.useFormState -- it is deprecated in React 19. Use useActionState instead.any type in TypeScript -- define proper interfaces and types..ts/.tsx. Strict mode in tsconfig.json. No implicit any.'use client' only when hooks, event handlers, or browser APIs are needed.loading.tsx and error.tsx per route segment. error.tsx must be a Client Component.global-error.tsx at the root. Catches errors in the root layout.NEXT_PUBLIC_ prefix. Server-only vars never use this prefix.not-found.tsx for custom 404 pages in segments with dynamic params.next/link for navigation. Never router.push for standard page transitions.server-only package on modules with secrets, database clients, or server-only logic.params and searchParams. In Next.js 15, these are Promises in pages, layouts, and generateMetadata.next/image for all images. Never use raw <img> tags.next/dynamic with ssr: false for heavy client components not needed at initial render.metadata export for static, generateMetadata for dynamic.revalidatePath/revalidateTag.Promise.all for parallel data fetching to avoid waterfalls.<form action={serverAction}> works without JS. Layer useActionState and useOptimistic on top.default.tsx in all parallel route slots to prevent 404 on soft navigation.package.json first.useState in Server Components. Server Components cannot use hooks. Move to a Client Component.'use client' directive. Must be the very first line of the file (before imports).fetch() is NOT cached by default. Migrated Next.js 14 code makes redundant requests without explicit opt-in.revalidatePath/revalidateTag after mutations leaves stale data. Over-revalidating with revalidatePath('/') busts the entire cache.Date.now(), Math.random(), window/localStorage during initial render.cookies()/headers() outside allowed contexts. Only work in Server Components, Server Actions, Route Handlers, and middleware.params (Next.js 15). params and searchParams are Promises. Synchronous access yields undefined.redirect() inside try/catch. The internal throw gets caught, preventing the redirect.useFormState instead of useActionState. Deprecated in React 19. useActionState also provides isPending.default.tsx in parallel routes. Causes 404 on soft navigation to unmatched slot routes.useOptimistic leaves ghost data in the UI.revalidate type. Must be a number or false. String '60' silently fails.useEffect + fetch for data a parent Server Component can provide as props.pages/ and app/ for the same routes.getServerSideProps/getStaticProps in App Router. Use async Server Components and generateStaticParams instead.'use client' boundaries. Extract only interactive parts into small Client Components.useEffect for form submissions. Use Server Actions + useActionState instead.useState. Render server-fetched data directly. Use useState only for client-side interactive state.app/user-settings/, never app/UserSettings/.OrderCard.tsx, DashboardSidebar.tsx.page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, route.ts, template.tsx, default.tsx, global-error.tsx.(groupName) folders for logical grouping without URL impact.@slotName. For modals, split views, independent panels.server-only and client-only packages to enforce import boundaries at build time.@/ import alias. Configure in tsconfig.json. Never use deep relative paths.app/actions/orders.ts with 'use server' at the top.app/
(dashboard)/
orders/
page.tsx
loading.tsx
error.tsx
components/
OrderCard.tsx
OrderFilters.tsx
actions.ts
actions/
orders.ts
auth.ts
components/ (shared UI components)
lib/ (utilities, db client, constants)
hooks/ (shared custom hooks)
When looking up framework documentation, use these Context7 library identifiers:
/vercel/next.js (use version /vercel/next.js/v15.1.8 for Next.js 15 specifics) -- App Router, Server Components, Server Actions, middleware, caching, parallel routes, intercepting routes/facebook/react -- hooks, components, useActionState, useOptimistic, use() (used within Client Components)testing-library/react-testing-library -- render, screen, queries, user eventsAlways check Context7 for the latest API when working with Next.js 15 and React 19 features. Training data may be outdated for caching behavior changes, async params, and new React hooks.