Verified against Claude Code · 2026-07-27
Find out why a route you expected to be static is rendering dynamically on every request
Traces which specific API call or import is forcing an otherwise-static route into dynamic rendering, instead of guessing at a fix from the build output symbol alone.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are diagnosing why a specific route is rendering dynamically on every request when it was expected to be static or ISR-cached. Rendering mode in the App Router is determined by what a route actually does, not by an explicit setting alone, so the fix has to name the specific code responsible, not just restate that the route "is dynamic." ROUTE /pricing EXPECTED RENDERING MODE Fully static — pricing content is the same for every visitor and rarely changes BUILD OUTPUT EVIDENCE The build output marks /pricing with the dynamic symbol instead of the static one, and Next.js's route info logged 'Dynamic server usage: cookies' during the build CODE INVOLVED app/pricing/page.tsx plus app/layout.tsx, which reads cookies() unconditionally in a top-of-tree A/B test check DIAGNOSIS RULES Search the route's component tree, including every layout above it, for a call to a dynamic API: cookies(), headers(), a direct read from searchParams, connection(), or a fetch call with cache: 'no-store' or an explicit revalidate: 0. Any one of these, called anywhere in the tree that renders for this route, including a shared layout the route doesn't own directly, forces the entire route to render dynamically — a static-looking page component can be made dynamic entirely by something in its layout, which is exactly the kind of cause that's easy to miss when the search only looks at the page file itself. Check whether a component is unconditionally calling one of these APIs when it's actually only needed conditionally — a component that reads cookies() to personalize content for logged-in users, but calls it unconditionally even for anonymous visitors who could have gotten a static response, is forcing dynamic rendering for a case that didn't actually need it. If a third-party library or internal utility is the actual source, name that specific import, not just the file that happens to import it, since the fix is either replacing that dependency's usage in this route or accepting the dynamic rendering it requires as a deliberate tradeoff rather than an accidental one. Distinguish between something that must genuinely force dynamic rendering, given what the route actually does — a page that shows the signed-in user's own data has to read a cookie or a session to know who that is — and something that's forcing it unnecessarily where a narrower fix, such as moving the dynamic read into a smaller, isolated Server Component wrapped in its own Suspense boundary, would let the rest of the route stay static or cached while only that one piece renders per-request. OUTPUT FORMAT A table: File | Dynamic trigger found | Is it a genuine requirement or an avoidable cause | Fix if avoidable. Then one paragraph confirming whether, after any recommended fixes, the route would actually render statically or with ISR, or whether it should stay dynamic on purpose given what it does — state that conclusion plainly rather than leaving it implied by the table alone.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Rendering mode in the App Router is a derived fact, not a setting a developer flips directly in most cases — it's determined by whether anything in the route's actual render path, including every shared layout above it, touches a dynamic API, which means the true cause is very often not in the page file a developer opens first to investigate. Searching the entire tree the route renders through, rather than just the page component, targets exactly this: a route can be forced dynamic by something in a root layout three levels up that has nothing to do with the page's own content, and a diagnosis that only reads the page file will conclude, incorrectly, that nothing explains the dynamic build output at all. Separating a genuine requirement from an avoidable cause matters because the correct fix is completely different depending on which one applies — a page that must read a session to show personalized content has no fix beyond accepting dynamic rendering as the right tradeoff, while a page that reads cookies() unconditionally for an A/B test that could have been resolved at the edge, or isolated into a small dynamic component wrapped in its own boundary, has a real fix available, and conflating the two leads either to over-engineering a route that was correctly dynamic all along, or to giving up on making a fixable route static because the unconditional call looked load-bearing when it wasn't. Naming the specific import or call responsible, rather than describing the route generally as "has dynamic behavior," is what makes the finding actionable in a way a reviewer can verify against the actual code — "cookies() in app/layout.tsx, called unconditionally for a check that only matters for a small subset of visitors" is a claim someone can open the file and confirm or refute, while "this route has some dynamic dependencies" isn't, and a diagnosis that isn't checkable doesn't actually save the time a manual investigation would have taken anyway.
What you get back
File | Trigger | Genuine or avoidable | Fix app/layout.tsx | cookies() called unconditionally to check an A/B test cookie for every visitor, including anonymous ones | Avoidable | Move the A/B check into a small Client Component that reads the cookie via document.cookie instead, or isolate it into its own dynamic Server Component wrapped in Suspense so app/pricing/page.tsx itself stays static Conclusion: after moving the A/B check out of the root layout, /pricing has nothing left forcing dynamic rendering and should build as fully static — the pricing content itself never reads a dynamic API anywhere in its own tree.
Verified against
Claude Code Sonnet 4.6 · 2026-07-27
Claude Sonnet 4.6 · 2026-08-03
Changelog
- 2026-07-27 — Initial publish, verified against Claude Code and Claude (Sonnet 4.6) on a Next.js 16 route forced dynamic by a root-layout cookie read.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
