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.

206 lines8.2 KB Kimi Frontend
targetModels
Kimi K3Kimi K2.6Kimi K2 FamilyFuture Kimi 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 Kimi: scripts/model-profiles.json -->

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names; report any out-of-scope change instead of making it.


#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