nextjs-best-practices — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited nextjs-best-practices (Agent Skill) and scored it 91/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} is the classic direct prompt-injection phrasing. Placed in a skill body that the agent reads as trusted instructions, it tries to make the agent abandon its prior rules and follow whatever comes next — a full system-prompt override.
ignore/disregard/forget … previous instructions sentence.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.
Apply these rules when writing or reviewing Next.js code. Covers App Router (Next.js 13+).
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI (Suspense boundary)
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── global-error.tsx # Root-level error boundary
├── (auth)/ # Route group (no URL segment)
│ ├── login/page.tsx
│ └── register/page.tsx
├── [id]/ # Dynamic segment
│ └── page.tsx
├── [...slug]/ # Catch-all segment
└── api/
└── route.ts # Route Handler| File | Purpose |
|---|---|
layout.tsx | Shared UI, persists across navigations |
page.tsx | Unique page UI, makes route publicly accessible |
loading.tsx | Auto-wraps page in <Suspense> |
error.tsx | Error boundary for segment (client component) |
not-found.tsx | Rendered when notFound() is thrown |
route.ts | API endpoint (no co-located page.tsx allowed) |
middleware.ts | Runs before request, at edge |
async/await data fetching directly in component'use client') - required when:useState, useEffect, useReducer, useContextwindow, document, localStorage)onClick, onChange)async - it's invalid'use server' only for Server Actions, not to mark Server ComponentsIn Next.js 15, params, searchParams, cookies(), headers(), and draftMode() are now async. Always await them:
// ✅ Correct - Next.js 15+
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ q: string }>
}) {
const { id } = await params
const { q } = await searchParams
const cookieStore = await cookies()
const token = cookieStore.get('token')
// ...
}// ❌ Wrong - old synchronous pattern
export default function Page({ params }: { params: { id: string } }) {
const { id } = params // broken in Next.js 15
}async function ProductList() {
const products = await db.products.findMany() // Direct DB access, no API needed
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}Promise.all// ❌ Sequential waterfall - slow
const user = await getUser(id)
const posts = await getPosts(user.id)
// ✅ Parallel - fast
const [user, posts] = await Promise.all([getUser(id), getPosts(id)])// 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/page.tsx
import { createPost } from './actions'
export default function Page() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
)
}// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const products = await db.products.findMany()
return NextResponse.json(products)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const product = await db.products.create({ data: body })
return NextResponse.json(product, { status: 201 })
}Always use next/image instead of <img>:
import Image from 'next/image'
// Local image - size known automatically
import photo from './photo.jpg'
<Image src={photo} alt="Description" />
// Remote image - must configure domain in next.config.ts
<Image
src="https://example.com/photo.jpg"
alt="Description"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 50vw"
priority // Add for LCP images above the fold
/>Configure remote domains:
// next.config.ts
const config: NextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'example.com' },
],
},
}Always use next/font instead of <link> tags - it prevents layout shift and self-hosts fonts:
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
export default function RootLayout({ children }) {
return (
<html className={`${inter.variable}`}>
<body>{children}</body>
</html>
)
}// app/error.tsx - must be a Client Component
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}Trigger programmatically:
import { notFound, redirect } from 'next/navigation'
// In any Server Component or Server Action:
if (!user) notFound() // renders not-found.tsx
if (!session) redirect('/login') // HTTP 307 redirect// app/layout.tsx - static
export const metadata = {
title: { template: '%s | My App', default: 'My App' },
description: 'My application description',
}
// app/products/[id]/page.tsx - dynamic
export async function generateMetadata({ params }) {
const { id } = await params
const product = await getProduct(id)
return {
title: product.name,
openGraph: { images: [product.imageUrl] },
}
}Wrap anything that uses useSearchParams() or usePathname() in a Client Component with <Suspense> to avoid CSR bailout:
import { Suspense } from 'react'
import SearchBar from './SearchBar' // contains useSearchParams
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchBar />
</Suspense>
)
}// next.config.ts
const config: NextConfig = {
output: 'standalone',
}FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=base /app/.next/standalone ./
COPY --from=base /app/.next/static ./.next/static
COPY --from=base /app/public ./public
CMD ["node", "server.js"]async to a parent Server Componentparams access without await in Next.js 15+ - add await params<img> instead of next/image - always use next/image for optimization<link> tags for fonts - always use next/fontpage.tsx in the same folder - causes a conflict, use different pathsexport const dynamic = 'force-dynamic' everywhere - use it only when truly needed~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.