Claude Fable 5.1 & GPT-6 Astra packages are live

Nextjs

Free

Next.js App Router frontend rules — routing and layouts, rendering strategy per route, images and fonts, streaming, and the client boundary.

207 lines8.2 KB Grok Frontend
targetModels
Grok 4.6Grok 4.5Grok 4 FamilyGrok Code FastFuture Grok Models
name
nextjs
category
Frontend
description
Next.js App Router frontend rules — routing and layouts, rendering strategy per route, images and fonts, streaming, and the client boundary.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Grok: scripts/model-profiles.json -->

#Non-negotiable

The constraints hoisted below override anything later in this document. Read them first; the rest is rationale.


#Purpose

Rules for the frontend side of a Next.js App Router application: file conventions, where each route renders, and the built-in components that exist because the naive version is slow.

Server-side concerns — route handlers, Server Actions, caching semantics — are Backend/nextjs.


#File conventions do real work

graphql
app/
  layout.tsx            # root shell — <html>, <body>, providers. Never re-renders on navigation
  page.tsx              # /
  loading.tsx           # automatic Suspense boundary for this segment
  error.tsx             # client error boundary; must be a client component
  not-found.tsx         # 404 for this subtree
  products/
    layout.tsx          # persists across /products/* navigations
    page.tsx            # /products
    [slug]/page.tsx     # /products/:slug
    @modal/…            # parallel route — intercepted modals
  • A layout preserves state across navigation within its subtree. Put the navigation, sidebar and providers there, not in each page.
  • loading.tsx is a Suspense fallback the framework wires for you. Without it the navigation blocks until data resolves and nothing tells the user anything.
  • error.tsx must be "use client" and receives a reset() function — offer a retry rather than a dead end.
  • not-found.tsx plus notFound() returns a real 404 for a missing record. → Frontend/routing

Route groups (marketing) organise without affecting the URL; private folders _components are excluded from routing entirely.


#Decide rendering per route

Next.js infers static or dynamic from what a route uses. Reading cookies(), headers(), searchParams or an uncached fetch makes it dynamic.

bash
next build      # ○ Static   ● SSG   ƒ Dynamic — read this output every release
RouteShould be
Marketing, docs, blogStatic
Product catalogueStatic with revalidation (revalidate)
Personalised dashboardDynamic
Anything authenticatedDynamic, uncached

A page you expected to be dynamic rendering statically is a correctness bug — it means one user's data was baked into a shared HTML file. Check the build output rather than assuming.

generateStaticParams pre-renders known dynamic paths at build time; combine it with revalidate for a large catalogue rather than generating every page on every build. → Backend/nextjs


#Push the client boundary down

"use client" marks an entry point: everything it imports, transitively, ships to the browser.

tsx
// The page stays on the server; only the interactive control is a client component
export default async function ProductPage({ params }) {
  const product = await getProduct(params.slug);
  return (<article><ProductDetails product={product} /><AddToCart id={product.id} /></article>);
}
  • Never put "use client" at the top of a layout or page.
  • Pass server-rendered content into interactive shells as children, so heavy content stays out of the bundle.
  • Props crossing the boundary are serialised into the HTML — project explicit fields, never a whole database row. → Frontend/server-components

#Use the built-in components

tsx
import Image from "next/image";
import Link from "next/link";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-sans" });

<Image src={product.image} alt={product.name} width={800} height={600}
       priority sizes="(max-width: 768px) 100vw, 800px" />
<Link href={`/products/${slug}`} prefetch></Link>
ComponentWhat it does that plain HTML does not
next/imageFormat negotiation, responsive srcset, reserved space, lazy by default
next/linkClient navigation plus prefetch on viewport entry
next/fontSelf-hosts, subsets, and applies size-adjust so the swap does not shift layout
next/scriptLoading strategies (afterInteractive, lazyOnload, worker)

Rules that are easy to get wrong:

  • priority on the LCP image, and never lazy-load it.
  • Always supply sizes for a responsive image, or the browser downloads the largest candidate.
  • next/font eliminates the third-party font request entirely — a Google Fonts <link> costs a connection and a round trip before any text renders.
  • Third-party scripts through next/script with an explicit strategy; a bare <script> in the head blocks rendering. → Frontend/performance

#Stream instead of blocking

tsx
export default async function Page() {
  return (
    <>
      <Header />                                   {/* renders immediately */}
      <Suspense fallback={<OrdersSkeleton />}>
        <Orders />                                 {/* streams when its query resolves */}
      </Suspense>
    </>
  );
}

Wrap slow sections so the fast part of the page is visible immediately. Size the skeleton to match the content, or streaming trades a slow page for a shifting one.

Start independent fetches together (Promise.all) — sequential awaits in a server component create a server-side waterfall that streaming does not fix.


#Anti-patterns

Anti-patternWhy it failsFix
"use client" on a layout or pageThe whole subtree ships to the browserMark interactive leaves
Whole database rows as client propsSerialised into the HTMLExplicit projection
Not checking next build outputPersonalised routes rendered staticallyRead static/dynamic per route
Providers in every pageState lost on navigationProviders in a layout
No loading.tsxNavigation blocks with no feedbackAdd the boundary
error.tsx without reset()Users hit a dead endOffer a retry
Missing not-found handlingDeleted records render an empty pagenotFound() plus the file
<img> instead of next/imageNo responsive sizes, no reserved spaceUse the component
Lazy-loading the LCP imageDelays the metric it definespriority
next/image without sizesLargest candidate downloaded on mobileProvide sizes
Google Fonts via <link>Extra connection before text rendersnext/font
Bare third-party <script>Blocks renderingnext/script with a strategy
No <Suspense> around slow dataThe page waits for the slowest queryStream
Sequential independent awaitsServer-side waterfallPromise.all
Mis-sized skeletonsLayout shift on resolveMatch content dimensions

#Checklist

  • Verify: Shared shell, navigation and providers live in layouts, not pages
  • Verify: Every segment has loading.tsx, error.tsx and a not-found path
  • Verify: error.tsx offers a working retry
  • Verify: next build output is reviewed; each route's static/dynamic status is intended
  • Verify: No authenticated or personalised route renders statically
  • Verify: generateStaticParams is used for known dynamic paths
  • Verify: "use client" appears only at interactive leaves
  • Verify: Props crossing to client components are explicitly projected
  • Verify: Server content is passed into client shells as children
  • Verify: Images use next/image with dimensions and sizes
  • Verify: The LCP image is priority and never lazy-loaded
  • Verify: Fonts use next/font with display: swap
  • Verify: Third-party scripts use next/script with an explicit strategy
  • Verify: Slow sections are wrapped in <Suspense> with correctly sized fallbacks
  • Verify: Independent data fetches run in parallel