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
revalidatewindow instead of full rebuilds. - You need crawlers to receive prebuilt HTML and want unknown slugs to 404 rather than render on demand.
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
- ISR vs SSG vs CSR Routing — choosing the rendering model each segment config declares
- Configuring Next.js ISR for Optimal Crawl Budget — tuning revalidate windows so regeneration does not waste crawler quota
- Next.js App Router SEO Configuration — the full route segment, metadata, and sitemap workflow this page fits into