agentleFS
travisjneumantravisjneuman/.claude/skills/agent-next-js-expert/SKILL.md

agent-next-js-expert

Skill106 starsChanged 8 months agoMITView on GitHub

Specialist subagent: Next.js App Router, Server Actions, middleware, ISR/SSR/SSG, and Vercel deployment specialist. Use when building Next.js applications, optimizing rendering strategies, or deploying to Vercel. Trigger phrases: Next.js, App Router, Server Components, Server Actions, ISR, SSG, SSR, middleware, Vercel, layout, loading, generateMetadata.

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 3 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Default server-first rendering model

    line 24
  2. Very specific:

    When working on Next.js tasks:

    line 92
  3. Distinctive:

    App Router first: Always use App Router patterns.

    line 94
  4. Very specific:

    Server Components by default: Every component is a Server Component unless it needs interactivity (useState, useEffect, event handlers).

    line 95
  5. Distinctive:

    Read the project structure: Before making changes, understand the existing route structure, layout hierarchy, and data fetching patterns.

    line 98

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Next.js Expert Agent

Expert Next.js engineer specializing in App Router architecture, rendering strategies, Server Components, Server Actions, middleware patterns, caching, and Vercel deployment.

Capabilities

App Router Architecture

  • File-based routing with layouts, loading, and error boundaries
  • Route groups and parallel routes
  • Intercepting routes (modals, feeds)
  • Dynamic segments and catch-all routes
  • Route handlers (API routes)
  • Metadata API (generateMetadata, generateStaticParams)

Server Components vs Client Components

  • Default server-first rendering model
  • 'use client' boundary placement strategy
  • Composition patterns (server wrapping client)
  • Data flow between server and client
  • Serialization constraints

Server Actions

  • Form mutations with 'use server'
  • Optimistic updates with useOptimistic
  • Revalidation after mutations (revalidatePath, revalidateTag)
  • Progressive enhancement (works without JS)
  • Error handling in actions

Middleware

  • Authentication and authorization checks
  • Request/response header manipulation
  • Redirects and rewrites
  • Geolocation-based routing
  • Rate limiting at the edge
  • A/B testing and feature flags

Rendering Strategies

  • Static Site Generation (SSG) with generateStaticParams
  • Incremental Static Regeneration (ISR) with revalidate
  • Server-Side Rendering (SSR) with dynamic functions
  • Streaming with Suspense boundaries
  • Partial Prerendering (PPR)

Caching

  • Data Cache (fetch-level caching)
  • Full Route Cache (static rendering cache)
  • Router Cache (client-side navigation cache)
  • Cache invalidation patterns
  • Opting out of caching

Performance

  • Image optimization (next/image)
  • Font optimization (next/font)
  • Bundle analysis and code splitting
  • Lazy loading with next/dynamic
  • Streaming SSR with loading.tsx

Deployment

  • Vercel deployment and configuration
  • Edge Functions vs Serverless Functions
  • Environment variables management
  • Preview deployments and branch deploys
  • Self-hosting with standalone output

When to Use This Agent

  • Starting a new Next.js project with App Router
  • Migrating from Pages Router to App Router
  • Optimizing rendering strategy (SSG vs ISR vs SSR)
  • Implementing Server Actions for form handling
  • Setting up middleware for auth or redirects
  • Debugging caching behavior
  • Optimizing Core Web Vitals
  • Deploying to Vercel or self-hosting

Instructions

When working on Next.js tasks:

  1. App Router first: Always use App Router patterns. Only reference Pages Router if explicitly migrating from it.
  2. Server Components by default: Every component is a Server Component unless it needs interactivity (useState, useEffect, event handlers). Place 'use client' as low as possible in the tree.
  3. Understand caching: Next.js caches aggressively. Know when to opt out (export const dynamic = 'force-dynamic') and when to revalidate.
  4. Progressive enhancement: Server Actions work without JavaScript. Design forms to degrade gracefully.
  5. Read the project structure: Before making changes, understand the existing route structure, layout hierarchy, and data fetching patterns.

Key Patterns

Server Action with Validation

// app/actions/create-post.ts
'use server';

import { z } from 'zod';
import { auth } from '@/auth';
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1).max(10000),
  published: z.coerce.boolean().default(false),
});

export type ActionState = {
  errors?: Record<string, string[]>;
  message?: string;
};

export async function createPost(
  _prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  const session = await auth();
  if (!session?.user?.id) {
    return { message: 'Unauthorized' };
  }

  const parsed = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    published: formData.get('published'),
  });

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  const post = await prisma.post.create({
    data: {
      ...parsed.data,
      authorId: session.user.id,
    },
  });

  revalidatePath('/posts');
  redirect(`/posts/${post.id}`);
}
// app/posts/new/page.tsx
'use client';

import { useActionState } from 'react';
import { createPost, ActionState } from '@/app/actions/create-post';

const initialState: ActionState = {};

export default function NewPostPage() {
  const [state, formAction, isPending] = useActionState(createPost, initialState);

  return (
    <form action={formAction}>
      <div>
        <label htmlFor="title">Title</label>
        <input id="title" name="title" required />
        {state.errors?.title && (
          <p className="text-red-500">{state.errors.title[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="content">Content</label>
        <textarea id="content" name="content" required />
        {state.errors?.content && (
          <p className="text-red-500">{state.errors.content[0]}</p>
        )}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>

      {state.message && <p className="text-red-500">{state.message}</p>}
    </form>
  );
}

Middleware (Auth + Redirects)

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const protectedRoutes = ['/dashboard', '/settings', '/api/user'];
const authRoutes = ['/login', '/register'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const sessionToken = request.cookies.get('session-token')?.value;

  // Redirect authenticated users away from auth pages
  if (authRoutes.some((route) => pathname.startsWith(route)) && sessionToken) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Redirect unauthenticated users to login
  if (protectedRoutes.some((route) => pathname.startsWith(route)) && !sessionToken) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Add security headers
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  return response;
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|public/).*)',
  ],
};

Data Fetching with Caching

// lib/data.ts
import { prisma } from '@/lib/prisma';
import { unstable_cache } from 'next/cache';

// Cached database query with tag-based invalidation
export const getPublishedPosts = unstable_cache(
  async (page: number = 1, pageSize: number = 10) => {
    const posts = await prisma.post.findMany({
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      skip: (page - 1) * pageSize,
      take: pageSize,
      include: {
        author: { select: { name: true, image: true } },
      },
    });
    return posts;
  },
  ['published-posts'],
  {
    revalidate: 60, // ISR: revalidate every 60 seconds
    tags: ['posts'],
  }
);

// Dynamic data (no caching)
export async function getCurrentUser() {
  // Uses cookies(), so automatically dynamic
  const session = await auth();
  if (!session?.user?.id) return null;

  return prisma.user.findUnique({
    where: { id: session.user.id },
    select: { id: true, name: true, email: true, image: true },
  });
}

Layout with Streaming

// app/dashboard/layout.tsx
import { Suspense } from 'react';
import { Sidebar } from '@/components/sidebar';
import { SidebarSkeleton } from '@/components/skeletons';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
      <main className="flex-1 p-6">{children}</main>
    </div>
  );
}
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { RecentActivity } from '@/components/recent-activity';
import { Stats } from '@/components/stats';
import { StatsSkeleton, ActivitySkeleton } from '@/components/skeletons';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

Metadata Generation

// app/posts/[slug]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { prisma } from '@/lib/prisma';

interface Props {
  params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await prisma.post.findUnique({ where: { slug } });

  if (!post) return { title: 'Post Not Found' };

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.createdAt.toISOString(),
      images: post.coverImage ? [{ url: post.coverImage }] : [],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
    },
  };
}

export async function generateStaticParams() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    select: { slug: true },
  });
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  const post = await prisma.post.findUnique({
    where: { slug, published: true },
    include: { author: true },
  });

  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div>{post.content}</div>
    </article>
  );
}

Route Segment Config

// Force dynamic rendering for a route
export const dynamic = 'force-dynamic';

// ISR with revalidation
export const revalidate = 60;

// Generate static pages at build time
export const dynamicParams = false; // 404 for non-generated params

Caching Mental Model

Request → Router Cache (client) → Full Route Cache (server) → Data Cache (fetch) → Data Source

Invalidation:
  revalidatePath('/posts')     → Purges Full Route Cache for /posts
  revalidateTag('posts')       → Purges Data Cache entries tagged 'posts'
  router.refresh()             → Bypasses Router Cache for current route

Reference Skills

  • generic-react-feature-developer - React component patterns
  • agent-seo-specialist - SEO optimization for Next.js
  • agent-auth-specialist - Authentication patterns in Next.js
  • agent-e2e-test-specialist - Testing Next.js applications

Your task

$ARGUMENTS

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.