Controlling Static and Dynamic Rendering in Next.js App Router

Pin each App Router route to static, incremental, or dynamic rendering on purpose so crawler-facing pages ship as prebuilt HTML instead of drifting into per-request rendering.

When to Use This Approach

Apply this when any of the following is true:

  • A route you expected to be static shows up as Dynamic in the build output and now hits the origin on every crawl.
  • You want incremental regeneration (ISR) with a controlled revalidate window instead of full rebuilds.
  • You need crawlers to receive prebuilt HTML and want unknown slugs to 404 rather than render on demand.

Static vs dynamic rendering decision A route stays static unless it reads request-bound APIs such as cookies or headers, in which case Next.js renders it dynamically per request. Route app segment cookies / headers()? Static / ISR prebuilt HTML Dynamic per-request SSR No Yes

Implementation Steps

Step 1: Set the route segment options

State the rendering intent explicitly rather than relying on inference.

// app/products/[slug]/page.tsx
export const dynamic = 'force-static'; // build to static HTML
export const revalidate = 1800;         // ISR window in seconds (0 = pure static)

Validation: next build lists the route as Static or ISR in the route table.

Step 2: Enumerate known slugs with generateStaticParams

Prebuild every published slug so crawlers land on warm static pages.

// app/products/[slug]/page.tsx
export async function generateStaticParams() {
  const products = await fetchAllPublishedSlugs();
  return products.map((p) => ({ slug: p.slug }));
}

Validation: the build output reports the expected number of prerendered pages for the segment.

Step 3: Close the route with dynamicParams

Return a real 404 for slugs generateStaticParams did not produce, bounding the indexable URL set.

// app/products/[slug]/page.tsx
export const dynamicParams = false; // unknown slugs -> 404, not on-demand render

Validation: curl -s -o /dev/null -w "%{http_code}" https://example.com/products/does-not-exist returns 404.

Step 4: Audit for dynamic-forcing APIs

Search the segment for request-bound calls that silently opt the route into dynamic rendering.

# Flag APIs that force per-request rendering in a route segment
grep -rnE "cookies\(\)|headers\(\)|searchParams|force-dynamic" app/products/

Validation: the command returns no unexpected matches; any hit in a route meant to be static is the cause of accidental dynamic rendering.

Step 5: Confirm the render mode in the build output

Read the authoritative signal — the build route table — rather than trusting dev behaviour.

next build | grep -E "products/\[slug\]"

Validation: the line shows Static () or ISR, not Dynamic (ƒ). If it reads Dynamic, revisit Step 4. For the ISR-specific tuning, see configuring Next.js ISR for optimal crawl budget.

SEO Impact Summary

Signal What improves What breaks if misconfigured
Rendering mode Prebuilt HTML gives crawlers low-TTFB full markup Accidental dynamic rendering pushes every crawl to the origin
Index coverage dynamicParams=false bounds the URL set to known slugs Open dynamic params let crawlers discover unlimited fabricated URLs
Freshness A tuned revalidate window balances currency against origin load revalidate=0 on hot routes floods the origin under crawl spikes
Build determinism generateStaticParams makes the page inventory explicit Missing slugs surface as build gaps rather than silent soft-404s

Measurable signals to watch:

  • The build route table should mark content routes as Static or ISR, not Dynamic.
  • Origin request volume from Googlebot should stay flat during crawl spikes once routes are static.
  • Search Console “Crawled — currently not indexed” for the segment should decline as prebuilt HTML replaces shells.

Edge Cases and Gotchas

A single request-bound call converts the whole segment Reading cookies(), headers(), or the searchParams prop anywhere in the segment forces dynamic rendering for the entire route, not just the component that called it. Isolate request-dependent logic in a leaf client component, or read the data through a route handler instead.

dynamicParams=false plus a new publish means a 404 until rebuild With dynamicParams=false, a slug published after the last build will 404 until the next build runs. If editors publish frequently, set dynamicParams=true so new slugs render on demand via ISR, and accept the first-hit render cost.

force-static silently drops uncacheable data Setting dynamic = 'force-static' makes uncacheable data fetches (those with cache: 'no-store') fall back to empty or cached values rather than erroring. Verify the rendered content is complete after forcing static, especially for personalization-adjacent fetches.

Middleware runs even for static routes Edge middleware executes on every request regardless of the route’s render mode, so heavy middleware can still add latency to otherwise-static pages. Keep middleware lean and push rendering decisions into the segment config, which ties back into the broader ISR, SSG, and CSR routing model.

Frequently Asked Questions

What forces a route to be dynamic? Reading request-bound data forces dynamic rendering: cookies(), headers(), the searchParams prop, or setting export const dynamic = 'force-dynamic'. Any one of these opts the whole segment out of static generation, so a single stray call can convert a prebuilt route to per-request SSR. Audit the segment with the grep in Step 4 when a route unexpectedly renders dynamically.

How do I confirm a route is static? Run next build and read the route table legend. A filled circle marks a statically prerendered route and ISR marks incremental regeneration, while the lambda symbol marks dynamic per-request rendering. The build output is the authoritative source, not local dev behaviour, which can render everything on demand.

What does dynamicParams=false do? It tells Next.js to return a 404 for any dynamic segment value that generateStaticParams did not produce, instead of rendering it on demand. This bounds the indexable URL set to known slugs and prevents crawlers from discovering unlimited fabricated paths, which protects crawl budget in headless deployments.


Part of: Next.js App Router SEO Configuration

Related