dev-nextjs — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited dev-nextjs (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
App Router (default since Next 13, stable): app/ folder, Server Components by default, Server Actions, streaming. Prefer for any new project.
Pages Router: pages/ folder, getServerSideProps/getStaticProps. Legacy, do not add new routes there.
If the project has both, coexist — both can live together, but do not duplicate a route.
Any component in app/ is a Server Component by default. It runs on the server, zero client JS.
// app/posts/page.tsx — Server Component (default)
export default async function PostsPage() {
const posts = await db.posts.findMany(); // Direct SQL OK
return <PostList posts={posts} />;
}"use client"// app/components/SearchBox.tsx
"use client"; // Directive at the top of the file
import { useState } from "react";
export function SearchBox() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}Rule: "use client" only if you need hooks (useState, useEffect) or browser events (onClick, onChange). Otherwise, stay Server Component.
Server Components can import Client Components, but the reverse is not allowed (except via children props).
// OK: Server Component uses a Client Component
export default async function Page() {
const data = await fetch(...);
return <ClientChart data={data} />; // ClientChart is "use client"
}
// OK: Client Component receives a Server Component via children
"use client";
export function Layout({ children }: { children: React.ReactNode }) {
return <div>{children}</div>; // children can be a RSC
}// Forced cache (SSG-like, manual revalidation)
const data = await fetch(url, { cache: "force-cache" });
// No cache (SSR on every request)
const data = await fetch(url, { cache: "no-store" });
// Time-based revalidation (ISR)
const data = await fetch(url, { next: { revalidate: 60 } });
// Tag-based revalidation
const data = await fetch(url, { next: { tags: ["posts"] } });
// Then in a Server Action: revalidateTag("posts")IMPORTANT (Next 15+): fetch is no longer cached by default. You must explicitly set force-cache or next: { revalidate }.
// BAD — waterfall
const user = await getUser();
const posts = await getPosts();
// GOOD — parallel
const [user, posts] = await Promise.all([getUser(), getPosts()]);Functions executed on the server, invoked from the client without a manual API route.
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.posts.create({ data: { title } });
revalidatePath("/posts");
}
// app/posts/new/page.tsx
import { createPost } from "../actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}Pitfalls:
revalidatePath or revalidateTag after a mutationapp/api/*/route.ts replaces pages/api/.
// app/api/posts/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const posts = await db.posts.findMany();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.posts.create({ data: body });
return NextResponse.json(post, { status: 201 });
}Show the page shell immediately, stream the slow content:
import { Suspense } from "react";
export default function Page() {
return (
<div>
<Header /> {/* Render immediately */}
<Suspense fallback={<PostsSkeleton />}>
<SlowPosts /> {/* Stream when ready */}
</Suspense>
</div>
);
}// app/posts/loading.tsx — Automatic loading UI
export default function Loading() {
return <PostsSkeleton />;
}// middleware.ts (at the root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};Pitfall: middleware runs on the Edge Runtime. No Node APIs (fs, crypto.createHash...) without a polyfill.
Replaces manual <head>.
// Static metadata
export const metadata: Metadata = {
title: "My App",
description: "...",
};
// Dynamic metadata (async)
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.id);
return { title: post.title };
}import Image from "next/image";
import { Geist } from "next/font/google";
const geist = Geist({ subsets: ["latin"] });
<Image src="/hero.jpg" alt="" width={1200} height={600} priority />Next loads and hosts fonts locally (no Google request), avoiding FOIT/FOUT.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true, // Auto-memoization optimization
ppr: "incremental", // Partial Prerendering
dynamicIO: true, // Next 15+ granular cache
},
images: {
remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }],
},
};
export default nextConfig;vercel — preview deployvercel --prod — production deployAlternative: self-host with next build && next start (Node 18+).
| Problem | Solution |
|---|---|
| "use client" everywhere | Only add it to components that use hooks/events |
| Unwanted data refetch | Check cache and next.revalidate on the fetch |
| Build errors ERR_DYNAMIC | Mark the page export const dynamic = "force-dynamic" or fix dynamic calls |
| Slow middleware | Reduce the matcher, avoid fetches inside middleware |
| Hydration mismatch | No random/Date.now() in SSR without suppressHydrationWarning |
npm run build # Verify the build + bundle size
npm run build -- --debug # Detailed log
npx @next/bundle-analyzer # Visualize the chunks.claude/rules/nextjs.md: path-specific rules (auto-activation on app/**).claude/rules/performance.md: Core Web Vitalsdev-react-perf: memoization, React lazy loadingqa-chrome: visual audit of Next pages<head> in App Router)IMPORTANT: "use client" is the exception, not the rule. By default, everything is a Server Component.
IMPORTANT: Next 15+: fetch is no longer cached by default. Always specify the cache behavior.
IMPORTANT: Validate Server Action inputs with Zod before mutation.
YOU MUST use revalidatePath or revalidateTag after every mutation to invalidate the cache.
NEVER fetch inside middleware (Edge, slow).
NEVER expose business logic in a Route Handler without checking auth.
Vercel Labs publishes official agent skills at vercel-labs/agent-skills (maintained by Vercel Engineering). Note the repo ships React + Vercel skills, not a dedicated Next.js one:
deploy-to-vercel, vercel-optimize, Web Design Guidelines.Install the vendor skill alongside this one on a Next.js project: the vendor sharpens the React layer and Vercel deploy/optimize, while this foundation skill stays the primary Next.js reference — App Router, Server Components, caching/streaming, Server Actions, middleware, route handlers — plus the opinionated workflow the foundation imposes (TDD-first, security defaults, deploy-safety, anti-patterns).
Install command and full list of validated vendor skills: docs/recipes/recommended-vendor-skills.md. Audit pilot trace: specs/marketplace-audit/dev-skills-pilot-2026-05-05.md.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.