web-meta-framework-nextjs-bf0004 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-meta-framework-nextjs-bf0004 (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.
Quick Guide: Server Components are the default and"use client"is opt-in, so keep client components small and at the leaves of the tree. Reads happen in Server Components; writes happen in Server Actions, which are public HTTP endpoints and revalidate the cache themselves. Two version facts change the answers below: from 15,params,searchParams,cookies()andheaders()are Promises; in 16,middleware.tsbecameproxy.ts,revalidateTag()takes acacheLifeprofile, andexperimental_pprbecamecacheComponents.
Detailed Resources:
generateMetadata, Open Graph and Twitter cards@slot folders, intercepting routes, the modal patternafter(), instrumentation, and the v16 migrationuseOptimistic for instant feedbackuseTransitionexamples/versions.md before copying any cache, middleware or parallel-route pattern.
above describe it.
<critical_requirements>
Write components as Server Components, and add `"use client"` only for state, effects or event handlers. The directive is what decides whether the code ships to the browser, so keeping it at the leaves keeps the bundle small and secrets on the server.
Import `server-only` in any module that touches a secret. The build then fails at the offending import instead of shipping the key to the browser.
Give data-heavy routes a `loading.tsx`, and independent sections their own `<Suspense>`. loading.tsx wraps the page in a boundary automatically; a boundary per section stops the slowest fetch blocking the fastest.
Set SEO through the `metadata` object or `generateMetadata`. Next.js deduplicates and orders those tags, which manual <head> markup does not.
Mark Server Actions with `'use server'` — at the top of the file, or at the top of the async function. Without it the function is bundled for the client.
Validate the input and check authorization inside every Server Action. An action is a public HTTP endpoint reachable by its ID, so both the caller's identity and the payload's shape are unverified until the action verifies them.
Call `revalidatePath()` or `revalidateTag()` after a mutation, and before any `redirect()`. The cache is what the next render reads, and redirect() throws, so nothing after it runs.
Await `params`, `searchParams`, `cookies()` and `headers()`. All four are Promises from 15 onward.
</critical_requirements>
Auto-detection: page.tsx, layout.tsx, loading.tsx, error.tsx, not-found.tsx, default.tsx, template.tsx, "use client", "use server", "use cache", App Router, Server Components, Client Components, generateMetadata, generateStaticParams, parallel routes, intercepting routes, revalidatePath, revalidateTag, updateTag, useActionState, useFormStatus, useOptimistic, next/form, next/cache, next/navigation, cacheComponents, cacheLife, cacheTag, PPR, Turbopack, typedRoutes, instrumentation.ts, proxy.ts, after()
Applies to:
app/ directory, with layouts, loading states and error boundariesgenerateStaticParamsHandled elsewhere:
FormData through a schema; which schema library defines itis a separate choice
className, and the styling approach is someone else's<philosophy>
The App Router inverts React's default: components run on the server unless they say otherwise, and client-side JavaScript is opt-in. That removes the client-server fetch waterfall, because a component can await its own data, and it keeps the bundle proportional to the interactivity a page actually has.
Four consequences shape every pattern here:
query finishes
DOM survive
same round trip, so the UI and the data refresh together
Server Actions suit form submissions and mutations coupled to UI. Route Handlers suit external API consumers, webhooks, and work that has to run in parallel — actions invoked from the client queue.
useActionState, useFormStatus and useOptimistic are React 19 hooks rather than Next.js APIs; useActionState replaces the Canary-era ReactDOM.useFormState.
</philosophy>
<patterns>
Fetch in a Server Component and pass the result down; the Client Component owns only the interaction.
// app/dashboard/page.tsx — a Server Component, no directive needed
export default async function DashboardPage() {
const user = await getUser(); // runs on the server, ships no JavaScript
return <UserProfile user={user} />; // Client Component, receives data as props
}A Client Component can still wrap Server Components, as long as they arrive through children rather than being imported inside it.
Full code: examples/server-components.md
loading.tsx and Suspenseloading.tsx covers a whole route; a <Suspense> per section lets each one arrive on its own.
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart /> {/* slow query — streams in independently */}
</Suspense>
<Suspense fallback={<InvoicesSkeleton />}>
<LatestInvoices />
</Suspense>Full code: examples/server-components.md
Folders define routes; the special files define behaviour for the segment and everything under it.
app/dashboard/
├── layout.tsx persists across navigations within the segment
├── page.tsx the route itself — without it the URL is not reachable
├── loading.tsx wraps page.tsx in a Suspense boundary automatically
├── error.tsx "use client"; receives { error, reset }
└── not-found.tsx rendered when the segment calls notFound()Full code: examples/core.md. The complete file table is in reference.md.
Bracket depth chooses between one segment, many, and optionally none.
// app/users/[id]/page.tsx → /users/1 params: { id: string }
// app/docs/[...slug]/page.tsx → /docs/a/b params: { slug: string[] }
// app/shop/[[...slug]]/page.tsx → /shop, /shop/a params: { slug?: string[] }
export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
}Pair with generateStaticParams to pre-render the known values at build time.
Full code: examples/core.md
Three folder conventions that change layout and rendering without changing the URL.
app/
├── (marketing)/layout.tsx route group — one layout per section, no URL segment
├── dashboard/@analytics/ parallel slot — arrives as a prop on dashboard/layout.tsx
├── @modal/(.)photo/[id]/ intercepts /photo/[id] on soft navigation only
└── @modal/default.tsx what a slot renders when nothing matchesEvery slot needs a default.tsx; on Next.js 16 the build fails without one.
Full code: examples/route-groups.md and examples/parallel-routes.md
Export a static object where the values are known, and a function where they come from the route's own data.
export const metadata: Metadata = {
title: { template: "%s | Acme", default: "Acme" },
};
// or, when the title depends on the route's data:
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const post = await getPost((await params).slug); // memoised with the page's own fetch
return {
title: post.title,
openGraph: { title: post.title, type: "article" },
};
}Full code: examples/metadata.md
A file-level 'use server' marks every export as an action, which is what lets a Client Component import one.
// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
/* ... */
}
// app/posts/new/page.tsx — a plain form, so it submits without JavaScript
<form action={createPost}>...</form>;action.bind(null, id) passes arguments beyond the FormData, and keeps the form working unenhanced. For a non-form trigger, call the action inside startTransition so the click does not block the UI.
Full code: examples/server-actions.md and examples/event-handlers.md
Both checks belong in the action body, because that is the only code path an attacker cannot skip.
"use server";
export async function deletePost(postId: string) {
const user = await getCurrentUser();
if (!user) throw new Error("Unauthorized");
const post = await getPost(postId);
if (post?.authorId !== user.id) throw new Error("Forbidden");
await deletePostRecord(postId);
revalidatePath("/posts");
}Return validation failures rather than throwing them — a throw reaches the error boundary and clears the form.
Full code: examples/server-actions.md
revalidatePath targets known routes; revalidateTag targets data wherever it is displayed.
"use server";
export async function createPost(formData: FormData) {
// ...mutate
revalidatePath("/posts"); // v16: revalidateTag("posts", "max")
redirect("/posts"); // throws — nothing below this line runs
}Full code: examples/revalidation.md
Three React 19 hooks cover the wait: one for form state, one for a nested submit button, one for showing the result before it lands.
const [state, formAction, isPending] = useActionState(createPost, initialState);
// useFormStatus() reads the same pending flag, but only from a component *inside* the form
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, content: string) => [
...state,
{ id: `temp-${Date.now()}`, content, pending: true },
],
);Full code: examples/optimistic.md and examples/server-actions.md
</patterns>
<red_flags>
Breaks at runtime:
'use server' missing from an action file — the function is bundled for the client, taking itsimports and any secret they reach with it
FormData arrives unparsed and untyped from anunauthenticated caller
redirect() inside try/catch — it signals by throwing, so the catch swallows the navigationparams, searchParams, cookies() or headers() without await — all four arePromises from 15 onward
error.tsx without "use client" — the reset callback needs a client boundary"use client" module — the value is in the browser bundledefault.tsx — the build failsrevalidateTag("tag") with one argument — a cacheLife profile is now requiredSurprising behaviour:
useFormStatus() returns pending: false forever when called in the component that renders theform; it only reads a form it is nested inside
revalidatePath/revalidateTag after a mutation leaves the UI showing the pre-mutationcache with no error anywhere
"use client" on page.tsx makes the whole subtree client-side, which is legal and quietlydiscards every server-rendering benefit
fetch is not cached by default under dynamic rendering, and GET Route Handlers are notcached either
More gotchas, the full anti-pattern set and the version-by-version changes are in reference.md.
</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.