[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-nextjs-weekly-ensurestatic-locks-static-output":44,"search-suggestions":60,"quiz-article-nextjs-weekly-ensurestatic-locks-static-output":108,"path-membership-nextjs-weekly-ensurestatic-locks-static-output":496},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a1972e-c515-74b9-af89-7593fff2d664","nextjs-weekly-ensurestatic-locks-static-output","PRACTICE_QUIZ","Next.js ensureStatic","Test yourself on what ensureStatic guarantees, its four levels, what the 'navigation' level validates, and the inheritance rules across layouts and pages.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},10,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",166,{"slug":66,"name":67,"articles":68},"javascript","Javascript",133,{"slug":70,"name":71,"articles":72},"frontend","Frontend",94,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",66,{"slug":78,"name":79,"articles":80},"css","Css",50,{"slug":82,"name":83,"articles":84},"performance","Performance",28,{"slug":86,"name":87,"articles":88},"typescript","Typescript",24,{"slug":90,"name":91,"articles":92},"react","React",20,{"slug":94,"name":95,"articles":96},"programming","Programming",16,{"slug":98,"name":99,"articles":96},"browser","Browser",{"slug":101,"name":102,"articles":103},"node","Node",14,{"slug":105,"name":106,"articles":107},"html","Html",11,{"id":109,"slug":46,"title":110,"subtitle":52,"excerpt":111,"coverUrl":112,"locale":13,"readingMinutes":113,"publishedAt":114,"viewCount":115,"likeCount":19,"commentCount":19,"author":116,"vertical":121,"topic":122,"tags":125,"_count":132,"playground":134,"body":136,"bodyMd":441,"seo":442,"translationGroupId":444,"series":445,"podcastUrl":52,"verticalId":5,"thread":487,"assessments":489,"translations":492,"quiz":494},"01a1972e-c476-72af-9154-03327c89a57d","Next.js 16.4 ensureStatic: Locking Routes Fully Static","Next.js 16.4 stabilizes ensureStatic: lock a route's shell, prefetch, or full navigation to static output at build time, with a copy-paste cheat sheet.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-ensurestatic-locks-static-output.png",13,"2026-11-03T06:10:26.449Z",44,{"id":117,"name":118,"username":119,"avatarUrl":52,"headline":120},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":123,"name":124},"nextjs","Nextjs",[126,127,130,131],{"slug":123,"name":124,"color":52},{"slug":128,"name":129,"color":52},"caching","Caching",{"slug":62,"name":63,"color":52},{"slug":74,"name":75,"color":52},{"assessments":133},1,{"slug":46,"title":135},"Next.js ensureStatic — interactive playground",{"blocks":137,"version":133},[138,142,145,150,153,162,165,168,181,185,188,194,198,201,204,207,210,215,218,224,246,249,252,255,258,262,265,268,272,275,279,282,286,289,296,299,302,305,312,316,319,324,327,330,338,341,344,347,350,353,356,359,362,365,368,371,374,377,382,385,388,391,394,397,400,408,411,414,417,423,426,429,432,435],{"id":139,"html":140,"type":141},"b1","\u003Cp>You ship a route you&#39;re sure is fully static. A teammate adds one \u003Ccode>fetch()\u003C\u002Fcode> call to a shared layout three weeks later — no cache directive, just a quick lookup they needed for a banner. Nothing errors. The build succeeds. The route still &quot;looks&quot; static in dev. It isn&#39;t anymore: that one call now forces the whole subtree to render on every request, and nobody notices until the latency dashboard does.\u003C\u002Fp>","paragraph",{"id":143,"html":144,"type":141},"b2","\u003Cp>This is written against \u003Cstrong>Next.js 16.4.3\u003C\u002Fstrong> (verified against the framework&#39;s own GitHub release notes and documentation as of October 2026). The guardrail for exactly this scenario — \u003Ccode>ensureStatic\u003C\u002Fcode> — was stabilized in \u003Cstrong>16.4.0\u003C\u002Fstrong>, after shipping unstable for a few releases. This series mentioned it in passing two weeks ago; this episode is the first real look at what it actually checks, and what it refuses to let you ship.\u003C\u002Fp>",{"id":146,"html":147,"text":148,"type":149,"level":31},"b3","What you&#39;ll learn","What you'll learn","heading",{"id":151,"html":152,"type":141},"b4","\u003Cp>By the end of this article you&#39;ll be able to:\u003C\u002Fp>",{"id":154,"type":155,"items":156,"ordered":18},"b5","list",[157,158,159,160,161],"Explain what \u003Ccode>ensureStatic\u003C\u002Fcode> actually guarantees, and why &quot;the page renders fast in dev&quot; isn&#39;t the same guarantee","Pick the right level — \u003Ccode>&#39;shell&#39;\u003C\u002Fcode>, \u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode>, or \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> — for a given route","Predict exactly which build failures \u003Ccode>ensureStatic = &#39;navigation&#39;\u003C\u002Fcode> will produce, before you hit them","Apply the inheritance rules correctly across layouts, nested segments, and parallel routes","Tell \u003Ccode>ensureStatic\u003C\u002Fcode> apart from \u003Ccode>instant\u003C\u002Fcode>, a different, dev-time-only check with a similar name",{"id":163,"html":164,"type":141},"b6","\u003Cp>\u003Cstrong>Who this is for:\u003C\u002Fstrong> you&#39;re comfortable with the App Router and Server Components, and you&#39;ve enabled Cache Components (\u003Ccode>cacheComponents: true\u003C\u002Fcode>) or are about to. If \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>, \u003Ccode>cacheLife()\u003C\u002Fcode>, and the App Shell are new to you, this series&#39; \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob\">Cache Components Explained\u003C\u002Fa> and \u003Ca href=\"https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-163-instant-navigations-how-partial-prefetching-works-5a6h\">Instant Navigations: How Partial Prefetching Works\u003C\u002Fa> cover the pieces \u003Ccode>ensureStatic\u003C\u002Fcode> builds on — worth reading first, not required before this one makes sense.\u003C\u002Fp>",{"id":166,"html":167,"text":167,"type":149,"level":31},"b7","Contents",{"id":169,"type":155,"items":170,"ordered":18},"b8",[171,172,173,174,175,176,177,178,179,180],"\u003Ca href=\"#the-problem-static-with-no-guarantee\">The problem: &quot;static&quot; with no guarantee\u003C\u002Fa>","\u003Ca href=\"#the-mental-model-four-levels-one-escalating-promise\">The mental model: four levels, one escalating promise\u003C\u002Fa>","\u003Ca href=\"#staged-walkthrough-shell-then-prefetch-then-navigation\">Staged walkthrough: shell, then prefetch, then navigation\u003C\u002Fa>","\u003Ca href=\"#what-navigation-actually-validates\">What \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> actually validates\u003C\u002Fa>","\u003Ca href=\"#edge-cases-and-inheritance-rules\">Edge cases and inheritance rules\u003C\u002Fa>","\u003Ca href=\"#ensurestatic-vs-instant\">\u003Ccode>ensureStatic\u003C\u002Fcode> vs \u003Ccode>instant\u003C\u002Fcode>\u003C\u002Fa>","\u003Ca href=\"#best-practices\">Best practices\u003C\u002Fa>","\u003Ca href=\"#faq\">FAQ\u003C\u002Fa>","\u003Ca href=\"#cheat-sheet\">Cheat sheet\u003C\u002Fa>","\u003Ca href=\"#key-takeaways\">Key takeaways\u003C\u002Fa>",{"id":182,"html":183,"text":184,"type":149,"level":31},"b9","The problem: &quot;static&quot; with no guarantee","The problem: \"static\" with no guarantee",{"id":186,"html":187,"type":141},"b10","\u003Cp>Say you run a documentation site. Every page lives under a shared layout — header, sidebar nav, footer — and the page content itself is cached:\u003C\u002Fp>",{"id":189,"code":190,"type":191,"language":192,"highlight":193},"b11","\u002F\u002F app\u002Fdocs\u002Flayout.tsx — looks static, isn't guaranteed to stay that way\nexport default function DocsLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Cdiv className=\"docs-shell\">\n      \u003CHeader \u002F>\n      \u003CSidebar \u002F>\n      \u003Cmain>{children}\u003C\u002Fmain>\n    \u003C\u002Fdiv>\n  )\n}","code","tsx",[],{"id":195,"code":196,"type":191,"language":192,"highlight":197},"b12","\u002F\u002F app\u002Fdocs\u002F[slug]\u002Fpage.tsx\nasync function getDoc(slug: string) {\n  \"use cache\"\n  return db.docs.findBySlug(slug)\n}\n\nexport default async function DocPage({ params }: { params: Promise\u003C{ slug: string }> }) {\n  const { slug } = await params\n  const doc = await getDoc(slug)\n  return \u003CArticle doc={doc} \u002F>\n}",[],{"id":199,"html":200,"type":141},"b13","\u003Cp>This is genuinely fast today. Nothing in the visible code is wrong. But nothing here \u003Cem>asserts\u003C\u002Fem> that it stays fast. Three weeks later someone adds a personalized &quot;you&#39;re on the free plan&quot; banner to \u003Ccode>&lt;Header \u002F&gt;\u003C\u002Fcode>, reading a cookie with a plain \u003Ccode>fetch\u003C\u002Fcode> to an account API — no \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>, because it&#39;s &quot;just a header, it&#39;s small.&quot; That one read makes the header request-scoped, which makes the layout request-scoped, which pulls every page under \u003Ccode>app\u002Fdocs\u002F\u003C\u002Fcode> back to rendering per request. The build still succeeds. Nothing in the PR review catches it, because the diff is three lines in a component nobody thinks of as &quot;the caching layer.&quot;\u003C\u002Fp>",{"id":202,"html":203,"type":141},"b14","\u003Cp>\u003Ccode>ensureStatic\u003C\u002Fcode> exists so that instead of discovering this in a latency graph, you discover it as a build failure, pointing at the exact file and the exact API call that broke the guarantee — the moment it&#39;s introduced, not the moment someone notices the site got slower.\u003C\u002Fp>",{"id":205,"html":206,"text":206,"type":149,"level":31},"b15","The mental model: four levels, one escalating promise",{"id":208,"html":209,"type":141},"b16","\u003Cp>\u003Ccode>ensureStatic\u003C\u002Fcode> is a route segment config — exported from a \u003Ccode>page.tsx\u003C\u002Fcode> or \u003Ccode>layout.tsx\u003C\u002Fcode>, the same place you&#39;d put \u003Ccode>dynamic\u003C\u002Fcode> or \u003Ccode>revalidate\u003C\u002Fcode> in the Pages Router-era App Router code. It requires Cache Components (\u003Ccode>cacheComponents: true\u003C\u002Fcode>) to be enabled; without it, exporting \u003Ccode>ensureStatic\u003C\u002Fcode> fails the build outright rather than doing nothing.\u003C\u002Fp>",{"id":211,"code":212,"type":191,"language":213,"highlight":214},"b17","type EnsureStatic = 'auto' | 'shell' | 'prefetch' | 'navigation' | false","ts",[],{"id":216,"html":217,"type":141},"b18","\u003Cp>Each value names a different \u003Cem>amount of the route&#39;s navigation lifecycle\u003C\u002Fem> that must be static — and that lifecycle is the same one last episode&#39;s App Shell and Partial Prefetching work defines:\u003C\u002Fp>",{"id":219,"type":155,"items":220,"ordered":17},"b19",[221,222,223],"\u003Cstrong>A default \u003Ccode>&lt;Link&gt;\u003C\u002Fcode>\u003C\u002Fstrong> (no \u003Ccode>prefetch\u003C\u002Fcode> prop) loads the \u003Cstrong>App Shell\u003C\u002Fstrong> — the parts of the route that don&#39;t depend on URL data.","\u003Cstrong>\u003Ccode>&lt;Link prefetch={true}&gt;\u003C\u002Fcode>\u003C\u002Fstrong> also resolves that specific link&#39;s URL-dependent data ahead of the click.","\u003Cstrong>The actual navigation\u003C\u002Fstrong> renders whatever is left after someone clicks.",{"id":225,"head":226,"rows":229,"type":245},"b20",[227,228],"Value","What must be static",[230,233,236,239,242],[231,232],"\u003Ccode>&#39;auto&#39;\u003C\u002Fcode> (default — same as omitting the export)","Nothing extra required",[234,235],"\u003Ccode>&#39;shell&#39;\u003C\u002Fcode>","The App Shell loaded by a plain \u003Ccode>&lt;Link&gt;\u003C\u002Fcode>",[237,238],"\u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode>","The App Shell \u003Cstrong>and\u003C\u002Fstrong> every per-link prefetch",[240,241],"\u003Ccode>&#39;navigation&#39;\u003C\u002Fcode>","The complete rendered route, including what streams in after the click",[243,244],"\u003Ccode>false\u003C\u002Fcode>","No requirement — and it blocks child segments from requiring one either","table",{"id":247,"html":248,"type":141},"b21","\u003Cp>Read it as an escalating promise: \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> is the cheapest to satisfy (it only constrains what&#39;s shared across every route under that layout), \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> is the strongest (it constrains the entire page, every time, for everyone). \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> and \u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode> only mean anything when Partial Prefetching is on — without it there&#39;s no separate shell stage to pin down, so \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> quietly does nothing and logs a warning. \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> is different: it only needs Cache Components, not Partial Prefetching, because it&#39;s asserting something about the full render, not about a separate shell stage.\u003C\u002Fp>",{"id":250,"html":251,"text":251,"type":149,"level":31},"b22","Staged walkthrough: shell, then prefetch, then navigation",{"id":253,"html":254,"type":141},"b23","\u003Cp>Take a small storefront: a cart layout shared by everything, product pages, and static buying guides.\u003C\u002Fp>",{"id":256,"html":257,"type":141},"b24","\u003Cp>\u003Cstrong>Step 1 — lock the shell.\u003C\u002Fstrong> The cart layout reads a cookie to show an item count — personalized, so it can&#39;t be part of the shell. Wrap it in \u003Ccode>&lt;Suspense&gt;\u003C\u002Fcode> with a static fallback, then assert the rest of the layout stays shell-eligible:\u003C\u002Fp>",{"id":259,"code":260,"type":191,"language":192,"highlight":261},"b25","\u002F\u002F app\u002F(store)\u002Flayout.tsx\nexport const ensureStatic = 'shell'\n\nexport default function StoreLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003C>\n      \u003Cnav>\n        \u003CLogo \u002F>\n        \u003CSuspense fallback={\u003CCartBadge count={null} \u002F>}>\n          \u003CCartCount \u002F> {\u002F* reads a cookie — fine, it's inside Suspense *\u002F}\n        \u003C\u002FSuspense>\n      \u003C\u002Fnav>\n      {children}\n    \u003C\u002F>\n  )\n}",[],{"id":263,"html":264,"type":141},"b26","\u003Cp>If anyone later moves cookie-reading code above that \u003Ccode>Suspense\u003C\u002Fcode> boundary — directly into \u003Ccode>StoreLayout\u003C\u002Fcode> — the build fails immediately, naming the offending file. That&#39;s the whole point: the mistake from the earlier docs example gets caught here instead of in production.\u003C\u002Fp>",{"id":266,"html":267,"type":141},"b27","\u003Cp>\u003Cstrong>Step 2 — lock prefetch.\u003C\u002Fstrong> Product pages are cached with \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode>, but the &quot;in your cart&quot; badge on each product card is personalized. \u003Ccode>ensureStatic = &#39;prefetch&#39;\u003C\u002Fcode> on the product list route asserts every one of those per-link prefetches resolves to static output — so hovering over twenty product cards doesn&#39;t fire twenty personalized round-trips:\u003C\u002Fp>",{"id":269,"code":270,"type":191,"language":192,"highlight":271},"b28","\u002F\u002F app\u002F(store)\u002Fproducts\u002Fpage.tsx\nexport const ensureStatic = 'prefetch'\n\nasync function getProducts() {\n  \"use cache\"\n  cacheLife({ stale: 300, revalidate: 3600, expire: 86400 })\n  return db.products.findAll()\n}",[],{"id":273,"html":274,"type":141},"b29","\u003Cp>\u003Cstrong>Step 3 — lock full navigation.\u003C\u002Fstrong> Buying guides are plain marketing content — no personalization, no request-time data, ever. That&#39;s the one case worth the strongest guarantee:\u003C\u002Fp>",{"id":276,"code":277,"type":191,"language":192,"highlight":278},"b30","\u002F\u002F app\u002F(store)\u002Fguides\u002F[slug]\u002Fpage.tsx\nexport const ensureStatic = 'navigation'\n\nexport async function generateStaticParams() {\n  const guides = await db.guides.findAllSlugs() \u002F\u002F must enumerate every slug\n  return guides.map((g) => ({ slug: g.slug }))\n}\n\nasync function getGuide(slug: string) {\n  \"use cache\"\n  cacheLife({ stale: 3600, revalidate: 86400, expire: false })\n  return db.guides.findBySlug(slug)\n}",[],{"id":280,"html":281,"type":141},"b31","\u003Cp>Here the whole page — not just a shared layout slice — must render to static output, for every one of the slugs \u003Ccode>generateStaticParams\u003C\u002Fcode> returns.\u003C\u002Fp>",{"id":283,"html":284,"text":285,"type":149,"level":31},"b32","What \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> actually validates","What 'navigation' actually validates",{"id":287,"html":288,"type":141},"b33","\u003Cp>\u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> is the level worth understanding precisely, because it&#39;s the one that turns real mistakes into real build errors instead of silent regressions. With \u003Ccode>cacheComponents\u003C\u002Fcode> on, exporting \u003Ccode>ensureStatic = &#39;navigation&#39;\u003C\u002Fcode> from a page makes the build (and the dev server) reject:\u003C\u002Fp>",{"id":290,"type":155,"items":291,"ordered":18},"b34",[292,293,294,295],"\u003Cstrong>Uncached data\u003C\u002Fstrong> — a bare \u003Ccode>fetch()\u003C\u002Fcode> or database call with no \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> wrapping it, and \u003Ccode>connection()\u003C\u002Fcode> (the explicit &quot;this must be dynamic&quot; escape hatch)","\u003Cstrong>Runtime-only APIs\u003C\u002Fstrong> — \u003Ccode>cookies()\u003C\u002Fcode>, \u003Ccode>headers()\u003C\u002Fcode>, or \u003Ccode>searchParams\u003C\u002Fcode> read on the server","\u003Cstrong>Cache entries that are effectively dynamic anyway\u003C\u002Fstrong> — \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> with an \u003Ccode>expire\u003C\u002Fcode> under five minutes, a \u003Ccode>stale\u003C\u002Fcode> under 30 seconds, or \u003Ccode>revalidate: 0\u003C\u002Fcode>; a cache that invalidates that fast isn&#39;t meaningfully static","\u003Cstrong>Request-dependent promises passed into a Client Component\u003C\u002Fstrong> — even if the server code itself looks cached, handing a promise whose value depends on the current request across the server\u002Fclient boundary reintroduces a per-request dependency",{"id":297,"html":298,"type":141},"b35","\u003Cp>Dynamic segments must also export a \u003Ccode>generateStaticParams()\u003C\u002Fcode> that returns the complete set of params — a partial list isn&#39;t enough once you&#39;ve promised the whole route is static. The same constraints apply to \u003Ccode>generateMetadata\u003C\u002Fcode> and \u003Ccode>generateViewport\u003C\u002Fcode>, since metadata generated from a live \u003Ccode>cookies()\u003C\u002Fcode> read would just as quietly break the promise.\u003C\u002Fp>",{"id":300,"html":301,"text":301,"type":149,"level":31},"b36","Edge cases and inheritance rules",{"id":303,"html":304,"type":141},"b37","\u003Cp>A few rules exist specifically to stop you from creating a guarantee that only holds in one place:\u003C\u002Fp>",{"id":306,"type":155,"items":307,"ordered":18},"b38",[308,309,310,311],"\u003Cstrong>A child segment can tighten freely, but can&#39;t explicitly downgrade.\u003C\u002Fstrong> A layout exporting \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> can have a page under it export \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> (stronger) — that builds fine. Exporting \u003Ccode>&#39;auto&#39;\u003C\u002Fcode> on that same page also builds fine: \u003Ccode>&#39;auto&#39;\u003C\u002Fcode> is always treated as &quot;no opinion,&quot; so it never conflicts with an ancestor&#39;s value — the layout&#39;s \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> requirement just keeps applying to the page underneath it. What actually fails the build is a child exporting a \u003Cem>weaker concrete level\u003C\u002Fem> than an ancestor already set, e.g. a page exporting \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> under a layout that requires \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> — that&#39;s a real contradiction, not a deferral, and Next.js rejects it by name.","\u003Cstrong>\u003Ccode>false\u003C\u002Fcode> and a level don&#39;t mix in the same tree.\u003C\u002Fstrong> If a parent is \u003Ccode>false\u003C\u002Fcode> and a child tries to set \u003Ccode>&#39;shell&#39;\u003C\u002Fcode>, the build fails — \u003Ccode>false\u003C\u002Fcode> isn&#39;t &quot;no opinion,&quot; it&#39;s an explicit &quot;nothing here is required to be static,&quot; and a child can&#39;t retroactively impose a requirement its parent already waived.","\u003Cstrong>Parallel routes rendered together must agree.\u003C\u002Fstrong> If two parallel slots render side by side, they must all share the same \u003Ccode>ensureStatic\u003C\u002Fcode> value, leave it unset, or use \u003Ccode>&#39;auto&#39;\u003C\u002Fcode> — one slot can&#39;t be \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> while its sibling is \u003Ccode>&#39;shell&#39;\u003C\u002Fcode>.","\u003Cstrong>A value applies to the whole route it&#39;s exported from, parent layouts included\u003C\u002Fstrong> — exporting it from a deep page doesn&#39;t exempt the layouts above it from the same requirement.",{"id":313,"html":314,"text":315,"type":149,"level":31},"b39","\u003Ccode>ensureStatic\u003C\u002Fcode> vs \u003Ccode>instant\u003C\u002Fcode>","ensureStatic vs instant",{"id":317,"html":318,"type":141},"b40","\u003Cp>They sound similar and ship in the same release, which invites confusing them. They check different things:\u003C\u002Fp>",{"id":320,"type":155,"items":321,"ordered":18},"b41",[322,323],"\u003Cstrong>\u003Ccode>ensureStatic\u003C\u002Fcode>\u003C\u002Fstrong> is a build-time (and dev-time) \u003Cstrong>enforcement\u003C\u002Fstrong>. Export it, and the build fails if the promise doesn&#39;t hold. It&#39;s a contract.","\u003Cstrong>\u003Ccode>instant\u003C\u002Fcode>\u003C\u002Fstrong> is a \u003Cstrong>dev-time signal\u003C\u002Fstrong>, not an enforced contract — it&#39;s a way to see, during development, whether a given navigation would paint instantly under the current code, without failing your build if it wouldn&#39;t.",{"id":325,"html":326,"type":141},"b42","\u003Cp>Use \u003Ccode>instant\u003C\u002Fcode> while you&#39;re exploring which routes are candidates for a stronger guarantee. Use \u003Ccode>ensureStatic\u003C\u002Fcode> once you&#39;ve decided one of them needs that guarantee enforced going forward, not just observed today.\u003C\u002Fp>",{"id":328,"html":329,"text":329,"type":149,"level":31},"b43","Best practices",{"id":331,"type":155,"items":332,"ordered":18},"b44",[333,334,335,336,337],"\u003Cstrong>Start at \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> on your root and section layouts\u003C\u002Fstrong>, not \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> on every page — the shell is shared infrastructure, and locking it first catches the highest-leverage mistakes (a stray cookie read in a header) for the least ongoing cost.","\u003Cstrong>Reserve \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> for routes that are genuinely public and non-personalized\u003C\u002Fstrong> — marketing pages, docs, buying guides. Forcing it onto a route that legitimately needs request-time data just means fighting the build instead of fixing the model.","\u003Cstrong>Add it when a route is already correct, not to force it correct.\u003C\u002Fstrong> \u003Ccode>ensureStatic\u003C\u002Fcode> is a regression guard, not a refactoring tool — get the caching right first with \u003Ccode>&quot;use cache&quot;\u003C\u002Fcode> and \u003Ccode>cacheLife()\u003C\u002Fcode>, confirm it behaves, then lock it in.","\u003Cstrong>Treat a new \u003Ccode>ensureStatic\u003C\u002Fcode> build failure as a real signal, not an obstacle.\u003C\u002Fstrong> It is almost always telling you that something just became request-scoped that your team didn&#39;t intend to make request-scoped.","\u003Cstrong>Don&#39;t apply \u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode> to a route with dozens of simultaneously-visible links\u003C\u002Fstrong> unless the per-link data is genuinely cheap and cached — it&#39;s an opt-in cost, not a free upgrade from \u003Ccode>&#39;shell&#39;\u003C\u002Fcode>.",{"id":339,"html":340,"text":340,"type":149,"level":31},"b45","FAQ",{"id":342,"html":343,"type":141},"b46","\u003Cp>\u003Cstrong>Does \u003Ccode>ensureStatic\u003C\u002Fcode> work without Cache Components?\u003C\u002Fstrong>\nNo. It requires \u003Ccode>cacheComponents: true\u003C\u002Fcode>; exporting it without that flag fails the build rather than silently doing nothing.\u003C\u002Fp>",{"id":345,"html":346,"type":141},"b47","\u003Cp>\u003Cstrong>Can I export \u003Ccode>ensureStatic\u003C\u002Fcode> from a Client Component?\u003C\u002Fstrong>\nNo — doing so throws an error. It&#39;s a server-side route segment config, like \u003Ccode>dynamic\u003C\u002Fcode> or \u003Ccode>revalidate\u003C\u002Fcode> before it.\u003C\u002Fp>",{"id":348,"html":349,"type":141},"b48","\u003Cp>\u003Cstrong>Does \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> do anything if I haven&#39;t enabled Partial Prefetching?\u003C\u002Fstrong>\nNo. \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> and \u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode> only have teeth once Partial Prefetching is on — without it, Next.js logs a warning and the export has no effect. \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> doesn&#39;t have this dependency; it works from Cache Components alone.\u003C\u002Fp>",{"id":351,"html":352,"type":141},"b49","\u003Cp>\u003Cstrong>What if I only want to guard one slow subtree, not the whole route?\u003C\u002Fstrong>\n\u003Ccode>ensureStatic\u003C\u002Fcode> is a whole-route (or whole-layout) assertion. For a single subtree, the docs point to deferring it explicitly with \u003Ccode>await prefetch()\u003C\u002Fcode> (skip it during link prefetch) or \u003Ccode>await navigation()\u003C\u002Fcode> (skip it until the click) instead of reaching for a route-level export.\u003C\u002Fp>",{"id":354,"html":355,"type":141},"b50","\u003Cp>\u003Cstrong>Is this the same as the old \u003Ccode>export const dynamic = &#39;force-static&#39;\u003C\u002Fcode>?\u003C\u002Fstrong>\nNo, and this is the important distinction. \u003Ccode>force-static\u003C\u002Fcode> (and \u003Ccode>dynamic\u003C\u002Fcode> generally) sets the rendering mode for the \u003Cem>entire\u003C\u002Fem> route as one unit, pre-Cache-Components. \u003Ccode>ensureStatic\u003C\u002Fcode> is built for a world where one route can legitimately mix prerendered, cached, and request-time content — it asserts a static guarantee over a specific \u003Cem>slice\u003C\u002Fem> (shell, prefetch, or full navigation) of a route that&#39;s otherwise allowed to be mixed.\u003C\u002Fp>",{"id":357,"html":358,"type":141},"b51","\u003Cp>\u003Cstrong>Will I hit this if I&#39;m still on Next.js 15?\u003C\u002Fstrong>\nNo — \u003Ccode>ensureStatic\u003C\u002Fcode> is a 16.4+ API, and it depends on Cache Components, which itself isn&#39;t the default until a later major. On 15.x, write around it rather than approximating it with \u003Ccode>force-static\u003C\u002Fcode>, which doesn&#39;t carry the same per-slice guarantee.\u003C\u002Fp>",{"id":360,"html":361,"type":141},"b52","\u003C!-- playground:start -->",{"id":363,"html":364,"text":364,"type":149,"level":31},"b53","🎮 Try it yourself",{"id":366,"html":367,"type":141},"b54","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":369,"html":370,"type":141},"b55","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":372,"html":373,"type":141},"b56","\u003C!-- playground:end -->",{"id":375,"html":376,"text":376,"type":149,"level":31},"b57","Cheat sheet",{"id":378,"code":379,"type":191,"language":380,"highlight":381},"b58","ensureStatic values (route segment config, requires cacheComponents: true):\n\n  'auto'        — default, same as omitting the export. No extra requirement.\n  'shell'       — the App Shell (needs Partial Prefetching; else: warning, no-op)\n  'prefetch'    — App Shell + every per-link prefetch (needs Partial Prefetching)\n  'navigation'  — the full rendered route (Cache Components alone is enough)\n  false         — no requirement; blocks children from requiring one\n\n'navigation' build fails on:\n  • fetch()\u002Fdb call with no \"use cache\"        • cookies() \u002F headers() \u002F searchParams\n  • connection()                                • use cache with expire \u003C 5m, stale \u003C 30s, or revalidate: 0\n  • request-dependent promise passed to a Client Component\n  • dynamic segment missing a complete generateStaticParams()\n\nInheritance:\n  • child can set a stronger concrete level freely (shell → prefetch → navigation)\n  • child exporting 'auto' is always compatible — defers to the ancestor's level, never fails the build\n  • child exporting a weaker CONCRETE level than an ancestor already set → build fails\n  • false + a level in the same tree → build fails\n  • parallel routes rendered together must share the same value (or 'auto'\u002Funset)\n\nNot the same as: dynamic = 'force-static' (whole-route, pre-Cache-Components)\nNot the same as: instant (dev-time signal, not an enforced build contract)","plain",[],{"id":383,"html":384,"type":141},"b59","\u003C!-- quiz:start -->",{"id":386,"html":387,"text":387,"type":149,"level":31},"b60","🧠 Test yourself",{"id":389,"html":390,"type":141},"b61","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output\u002Fquiz\">Take the 10-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":392,"html":393,"type":141},"b62","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":395,"html":396,"type":141},"b63","\u003C!-- quiz:end -->",{"id":398,"html":399,"text":399,"type":149,"level":31},"b64","Key takeaways",{"id":401,"type":155,"items":402,"ordered":18},"b65",[403,404,405,406,407],"\u003Ccode>ensureStatic\u003C\u002Fcode> turns &quot;I&#39;m pretty sure this route is static&quot; into something the build actually checks, with four levels of increasing strength: \u003Ccode>&#39;auto&#39;\u003C\u002Fcode>, \u003Ccode>&#39;shell&#39;\u003C\u002Fcode>, \u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode>, \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode>.","It requires Cache Components; \u003Ccode>&#39;shell&#39;\u003C\u002Fcode> and \u003Ccode>&#39;prefetch&#39;\u003C\u002Fcode> additionally require Partial Prefetching, while \u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> stands on Cache Components alone.","\u003Ccode>&#39;navigation&#39;\u003C\u002Fcode> validates specifically: no uncached fetches, no runtime APIs, no short-lived cache entries, no request-scoped promises crossing into Client Components, and a complete \u003Ccode>generateStaticParams()\u003C\u002Fcode>.","A child can freely set a stronger level, and \u003Ccode>&#39;auto&#39;\u003C\u002Fcode> on a child never conflicts — but explicitly exporting a weaker \u003Cem>concrete\u003C\u002Fem> level than an ancestor already set fails the build, and parallel routes rendered together must agree.","It&#39;s a regression guard for routes you&#39;ve already made static, not a tool for making them static — and it&#39;s a different thing entirely from the dev-time \u003Ccode>instant\u003C\u002Fcode> signal.",{"id":409,"html":410,"type":141},"b66","\u003Cp>That cookie read someone adds to a shared header three weeks from now doesn&#39;t have to be a latency regression you discover in a dashboard. With \u003Ccode>ensureStatic\u003C\u002Fcode> on the layout it depends on, it&#39;s a build failure with your name on the stack trace — which is a much better place to find it.\u003C\u002Fp>",{"id":412,"html":413,"type":141},"b67","\u003C!-- related:start -->",{"id":415,"html":416,"text":416,"type":149,"level":31},"b68","📚 Read next",{"id":418,"type":155,"items":419,"ordered":18},"b69",[420,421,422],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag\">Next.js 16 updateTag vs revalidateTag: What Changed\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary\">Next.js proxy.ts Explained (with Cheat Sheet)\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming\">Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16\u003C\u002Fa>",{"id":424,"html":425,"type":141},"b70","\u003C!-- related:end -->",{"id":427,"type":428},"b71","divider",{"id":430,"html":431,"type":141},"b72","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":433,"html":434,"type":141},"b73","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":436,"type":155,"items":437,"ordered":18},"b74",[438,439,440],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You ship a route you're sure is fully static. A teammate adds one `fetch()` call to a shared layout three weeks later — no cache directive, just a quick lookup they needed for a banner. Nothing errors. The build succeeds. The route still \"looks\" static in dev. It isn't anymore: that one call now forces the whole subtree to render on every request, and nobody notices until the latency dashboard does.\n\nThis is written against **Next.js 16.4.3** (verified against the framework's own GitHub release notes and documentation as of October 2026). The guardrail for exactly this scenario — `ensureStatic` — was stabilized in **16.4.0**, after shipping unstable for a few releases. This series mentioned it in passing two weeks ago; this episode is the first real look at what it actually checks, and what it refuses to let you ship.\n\n## What you'll learn\n\nBy the end of this article you'll be able to:\n\n- Explain what `ensureStatic` actually guarantees, and why \"the page renders fast in dev\" isn't the same guarantee\n- Pick the right level — `'shell'`, `'prefetch'`, or `'navigation'` — for a given route\n- Predict exactly which build failures `ensureStatic = 'navigation'` will produce, before you hit them\n- Apply the inheritance rules correctly across layouts, nested segments, and parallel routes\n- Tell `ensureStatic` apart from `instant`, a different, dev-time-only check with a similar name\n\n**Who this is for:** you're comfortable with the App Router and Server Components, and you've enabled Cache Components (`cacheComponents: true`) or are about to. If `\"use cache\"`, `cacheLife()`, and the App Shell are new to you, this series' [Cache Components Explained](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-cache-components-explained-with-cheat-sheet-55ob) and [Instant Navigations: How Partial Prefetching Works](https:\u002F\u002Fdev.to\u002Fparsajiravand\u002Fnextjs-163-instant-navigations-how-partial-prefetching-works-5a6h) cover the pieces `ensureStatic` builds on — worth reading first, not required before this one makes sense.\n\n## Contents\n\n- [The problem: \"static\" with no guarantee](#the-problem-static-with-no-guarantee)\n- [The mental model: four levels, one escalating promise](#the-mental-model-four-levels-one-escalating-promise)\n- [Staged walkthrough: shell, then prefetch, then navigation](#staged-walkthrough-shell-then-prefetch-then-navigation)\n- [What `'navigation'` actually validates](#what-navigation-actually-validates)\n- [Edge cases and inheritance rules](#edge-cases-and-inheritance-rules)\n- [`ensureStatic` vs `instant`](#ensurestatic-vs-instant)\n- [Best practices](#best-practices)\n- [FAQ](#faq)\n- [Cheat sheet](#cheat-sheet)\n- [Key takeaways](#key-takeaways)\n\n## The problem: \"static\" with no guarantee\n\nSay you run a documentation site. Every page lives under a shared layout — header, sidebar nav, footer — and the page content itself is cached:\n\n```tsx\n\u002F\u002F app\u002Fdocs\u002Flayout.tsx — looks static, isn't guaranteed to stay that way\nexport default function DocsLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Cdiv className=\"docs-shell\">\n      \u003CHeader \u002F>\n      \u003CSidebar \u002F>\n      \u003Cmain>{children}\u003C\u002Fmain>\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n```tsx\n\u002F\u002F app\u002Fdocs\u002F[slug]\u002Fpage.tsx\nasync function getDoc(slug: string) {\n  \"use cache\"\n  return db.docs.findBySlug(slug)\n}\n\nexport default async function DocPage({ params }: { params: Promise\u003C{ slug: string }> }) {\n  const { slug } = await params\n  const doc = await getDoc(slug)\n  return \u003CArticle doc={doc} \u002F>\n}\n```\n\nThis is genuinely fast today. Nothing in the visible code is wrong. But nothing here *asserts* that it stays fast. Three weeks later someone adds a personalized \"you're on the free plan\" banner to `\u003CHeader \u002F>`, reading a cookie with a plain `fetch` to an account API — no `\"use cache\"`, because it's \"just a header, it's small.\" That one read makes the header request-scoped, which makes the layout request-scoped, which pulls every page under `app\u002Fdocs\u002F` back to rendering per request. The build still succeeds. Nothing in the PR review catches it, because the diff is three lines in a component nobody thinks of as \"the caching layer.\"\n\n`ensureStatic` exists so that instead of discovering this in a latency graph, you discover it as a build failure, pointing at the exact file and the exact API call that broke the guarantee — the moment it's introduced, not the moment someone notices the site got slower.\n\n## The mental model: four levels, one escalating promise\n\n`ensureStatic` is a route segment config — exported from a `page.tsx` or `layout.tsx`, the same place you'd put `dynamic` or `revalidate` in the Pages Router-era App Router code. It requires Cache Components (`cacheComponents: true`) to be enabled; without it, exporting `ensureStatic` fails the build outright rather than doing nothing.\n\n```ts\ntype EnsureStatic = 'auto' | 'shell' | 'prefetch' | 'navigation' | false\n```\n\nEach value names a different *amount of the route's navigation lifecycle* that must be static — and that lifecycle is the same one last episode's App Shell and Partial Prefetching work defines:\n\n1. **A default `\u003CLink>`** (no `prefetch` prop) loads the **App Shell** — the parts of the route that don't depend on URL data.\n2. **`\u003CLink prefetch={true}>`** also resolves that specific link's URL-dependent data ahead of the click.\n3. **The actual navigation** renders whatever is left after someone clicks.\n\n| Value | What must be static |\n| --- | --- |\n| `'auto'` (default — same as omitting the export) | Nothing extra required |\n| `'shell'` | The App Shell loaded by a plain `\u003CLink>` |\n| `'prefetch'` | The App Shell **and** every per-link prefetch |\n| `'navigation'` | The complete rendered route, including what streams in after the click |\n| `false` | No requirement — and it blocks child segments from requiring one either |\n\nRead it as an escalating promise: `'shell'` is the cheapest to satisfy (it only constrains what's shared across every route under that layout), `'navigation'` is the strongest (it constrains the entire page, every time, for everyone). `'shell'` and `'prefetch'` only mean anything when Partial Prefetching is on — without it there's no separate shell stage to pin down, so `'shell'` quietly does nothing and logs a warning. `'navigation'` is different: it only needs Cache Components, not Partial Prefetching, because it's asserting something about the full render, not about a separate shell stage.\n\n## Staged walkthrough: shell, then prefetch, then navigation\n\nTake a small storefront: a cart layout shared by everything, product pages, and static buying guides.\n\n**Step 1 — lock the shell.** The cart layout reads a cookie to show an item count — personalized, so it can't be part of the shell. Wrap it in `\u003CSuspense>` with a static fallback, then assert the rest of the layout stays shell-eligible:\n\n```tsx\n\u002F\u002F app\u002F(store)\u002Flayout.tsx\nexport const ensureStatic = 'shell'\n\nexport default function StoreLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003C>\n      \u003Cnav>\n        \u003CLogo \u002F>\n        \u003CSuspense fallback={\u003CCartBadge count={null} \u002F>}>\n          \u003CCartCount \u002F> {\u002F* reads a cookie — fine, it's inside Suspense *\u002F}\n        \u003C\u002FSuspense>\n      \u003C\u002Fnav>\n      {children}\n    \u003C\u002F>\n  )\n}\n```\n\nIf anyone later moves cookie-reading code above that `Suspense` boundary — directly into `StoreLayout` — the build fails immediately, naming the offending file. That's the whole point: the mistake from the earlier docs example gets caught here instead of in production.\n\n**Step 2 — lock prefetch.** Product pages are cached with `\"use cache\"`, but the \"in your cart\" badge on each product card is personalized. `ensureStatic = 'prefetch'` on the product list route asserts every one of those per-link prefetches resolves to static output — so hovering over twenty product cards doesn't fire twenty personalized round-trips:\n\n```tsx\n\u002F\u002F app\u002F(store)\u002Fproducts\u002Fpage.tsx\nexport const ensureStatic = 'prefetch'\n\nasync function getProducts() {\n  \"use cache\"\n  cacheLife({ stale: 300, revalidate: 3600, expire: 86400 })\n  return db.products.findAll()\n}\n```\n\n**Step 3 — lock full navigation.** Buying guides are plain marketing content — no personalization, no request-time data, ever. That's the one case worth the strongest guarantee:\n\n```tsx\n\u002F\u002F app\u002F(store)\u002Fguides\u002F[slug]\u002Fpage.tsx\nexport const ensureStatic = 'navigation'\n\nexport async function generateStaticParams() {\n  const guides = await db.guides.findAllSlugs() \u002F\u002F must enumerate every slug\n  return guides.map((g) => ({ slug: g.slug }))\n}\n\nasync function getGuide(slug: string) {\n  \"use cache\"\n  cacheLife({ stale: 3600, revalidate: 86400, expire: false })\n  return db.guides.findBySlug(slug)\n}\n```\n\nHere the whole page — not just a shared layout slice — must render to static output, for every one of the slugs `generateStaticParams` returns.\n\n## What `'navigation'` actually validates\n\n`'navigation'` is the level worth understanding precisely, because it's the one that turns real mistakes into real build errors instead of silent regressions. With `cacheComponents` on, exporting `ensureStatic = 'navigation'` from a page makes the build (and the dev server) reject:\n\n- **Uncached data** — a bare `fetch()` or database call with no `\"use cache\"` wrapping it, and `connection()` (the explicit \"this must be dynamic\" escape hatch)\n- **Runtime-only APIs** — `cookies()`, `headers()`, or `searchParams` read on the server\n- **Cache entries that are effectively dynamic anyway** — `\"use cache\"` with an `expire` under five minutes, a `stale` under 30 seconds, or `revalidate: 0`; a cache that invalidates that fast isn't meaningfully static\n- **Request-dependent promises passed into a Client Component** — even if the server code itself looks cached, handing a promise whose value depends on the current request across the server\u002Fclient boundary reintroduces a per-request dependency\n\nDynamic segments must also export a `generateStaticParams()` that returns the complete set of params — a partial list isn't enough once you've promised the whole route is static. The same constraints apply to `generateMetadata` and `generateViewport`, since metadata generated from a live `cookies()` read would just as quietly break the promise.\n\n## Edge cases and inheritance rules\n\nA few rules exist specifically to stop you from creating a guarantee that only holds in one place:\n\n- **A child segment can tighten freely, but can't explicitly downgrade.** A layout exporting `'shell'` can have a page under it export `'navigation'` (stronger) — that builds fine. Exporting `'auto'` on that same page also builds fine: `'auto'` is always treated as \"no opinion,\" so it never conflicts with an ancestor's value — the layout's `'shell'` requirement just keeps applying to the page underneath it. What actually fails the build is a child exporting a *weaker concrete level* than an ancestor already set, e.g. a page exporting `'shell'` under a layout that requires `'navigation'` — that's a real contradiction, not a deferral, and Next.js rejects it by name.\n- **`false` and a level don't mix in the same tree.** If a parent is `false` and a child tries to set `'shell'`, the build fails — `false` isn't \"no opinion,\" it's an explicit \"nothing here is required to be static,\" and a child can't retroactively impose a requirement its parent already waived.\n- **Parallel routes rendered together must agree.** If two parallel slots render side by side, they must all share the same `ensureStatic` value, leave it unset, or use `'auto'` — one slot can't be `'navigation'` while its sibling is `'shell'`.\n- **A value applies to the whole route it's exported from, parent layouts included** — exporting it from a deep page doesn't exempt the layouts above it from the same requirement.\n\n## `ensureStatic` vs `instant`\n\nThey sound similar and ship in the same release, which invites confusing them. They check different things:\n\n- **`ensureStatic`** is a build-time (and dev-time) **enforcement**. Export it, and the build fails if the promise doesn't hold. It's a contract.\n- **`instant`** is a **dev-time signal**, not an enforced contract — it's a way to see, during development, whether a given navigation would paint instantly under the current code, without failing your build if it wouldn't.\n\nUse `instant` while you're exploring which routes are candidates for a stronger guarantee. Use `ensureStatic` once you've decided one of them needs that guarantee enforced going forward, not just observed today.\n\n## Best practices\n\n- **Start at `'shell'` on your root and section layouts**, not `'navigation'` on every page — the shell is shared infrastructure, and locking it first catches the highest-leverage mistakes (a stray cookie read in a header) for the least ongoing cost.\n- **Reserve `'navigation'` for routes that are genuinely public and non-personalized** — marketing pages, docs, buying guides. Forcing it onto a route that legitimately needs request-time data just means fighting the build instead of fixing the model.\n- **Add it when a route is already correct, not to force it correct.** `ensureStatic` is a regression guard, not a refactoring tool — get the caching right first with `\"use cache\"` and `cacheLife()`, confirm it behaves, then lock it in.\n- **Treat a new `ensureStatic` build failure as a real signal, not an obstacle.** It is almost always telling you that something just became request-scoped that your team didn't intend to make request-scoped.\n- **Don't apply `'prefetch'` to a route with dozens of simultaneously-visible links** unless the per-link data is genuinely cheap and cached — it's an opt-in cost, not a free upgrade from `'shell'`.\n\n## FAQ\n\n**Does `ensureStatic` work without Cache Components?**\nNo. It requires `cacheComponents: true`; exporting it without that flag fails the build rather than silently doing nothing.\n\n**Can I export `ensureStatic` from a Client Component?**\nNo — doing so throws an error. It's a server-side route segment config, like `dynamic` or `revalidate` before it.\n\n**Does `'shell'` do anything if I haven't enabled Partial Prefetching?**\nNo. `'shell'` and `'prefetch'` only have teeth once Partial Prefetching is on — without it, Next.js logs a warning and the export has no effect. `'navigation'` doesn't have this dependency; it works from Cache Components alone.\n\n**What if I only want to guard one slow subtree, not the whole route?**\n`ensureStatic` is a whole-route (or whole-layout) assertion. For a single subtree, the docs point to deferring it explicitly with `await prefetch()` (skip it during link prefetch) or `await navigation()` (skip it until the click) instead of reaching for a route-level export.\n\n**Is this the same as the old `export const dynamic = 'force-static'`?**\nNo, and this is the important distinction. `force-static` (and `dynamic` generally) sets the rendering mode for the *entire* route as one unit, pre-Cache-Components. `ensureStatic` is built for a world where one route can legitimately mix prerendered, cached, and request-time content — it asserts a static guarantee over a specific *slice* (shell, prefetch, or full navigation) of a route that's otherwise allowed to be mixed.\n\n**Will I hit this if I'm still on Next.js 15?**\nNo — `ensureStatic` is a 16.4+ API, and it depends on Cache Components, which itself isn't the default until a later major. On 15.x, write around it rather than approximating it with `force-static`, which doesn't carry the same per-slice guarantee.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Cheat sheet\n\n```\nensureStatic values (route segment config, requires cacheComponents: true):\n\n  'auto'        — default, same as omitting the export. No extra requirement.\n  'shell'       — the App Shell (needs Partial Prefetching; else: warning, no-op)\n  'prefetch'    — App Shell + every per-link prefetch (needs Partial Prefetching)\n  'navigation'  — the full rendered route (Cache Components alone is enough)\n  false         — no requirement; blocks children from requiring one\n\n'navigation' build fails on:\n  • fetch()\u002Fdb call with no \"use cache\"        • cookies() \u002F headers() \u002F searchParams\n  • connection()                                • use cache with expire \u003C 5m, stale \u003C 30s, or revalidate: 0\n  • request-dependent promise passed to a Client Component\n  • dynamic segment missing a complete generateStaticParams()\n\nInheritance:\n  • child can set a stronger concrete level freely (shell → prefetch → navigation)\n  • child exporting 'auto' is always compatible — defers to the ancestor's level, never fails the build\n  • child exporting a weaker CONCRETE level than an ancestor already set → build fails\n  • false + a level in the same tree → build fails\n  • parallel routes rendered together must share the same value (or 'auto'\u002Funset)\n\nNot the same as: dynamic = 'force-static' (whole-route, pre-Cache-Components)\nNot the same as: instant (dev-time signal, not an enforced build contract)\n```\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 10-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## Key takeaways\n\n- `ensureStatic` turns \"I'm pretty sure this route is static\" into something the build actually checks, with four levels of increasing strength: `'auto'`, `'shell'`, `'prefetch'`, `'navigation'`.\n- It requires Cache Components; `'shell'` and `'prefetch'` additionally require Partial Prefetching, while `'navigation'` stands on Cache Components alone.\n- `'navigation'` validates specifically: no uncached fetches, no runtime APIs, no short-lived cache entries, no request-scoped promises crossing into Client Components, and a complete `generateStaticParams()`.\n- A child can freely set a stronger level, and `'auto'` on a child never conflicts — but explicitly exporting a weaker *concrete* level than an ancestor already set fails the build, and parallel routes rendered together must agree.\n- It's a regression guard for routes you've already made static, not a tool for making them static — and it's a different thing entirely from the dev-time `instant` signal.\n\nThat cookie read someone adds to a shared header three weeks from now doesn't have to be a latency regression you discover in a dashboard. With `ensureStatic` on the layout it depends on, it's a build failure with your name on the stack trace — which is a much better place to find it.\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [Next.js 16 updateTag vs revalidateTag: What Changed](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-updatetag-vs-revalidatetag)\n- [Next.js proxy.ts Explained (with Cheat Sheet)](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-middleware-to-proxy-network-boundary)\n- [Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-route-handlers-caching-streaming)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":110,"canonical":443,"description":111},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fnextjs-weekly-ensurestatic-locks-static-output","01a1972e-c476-72af-9154-07fff218ff5e",{"name":446,"part":51,"total":51,"items":447},"Next.js Deep Dive",[448,452,457,461,466,470,474,478,482,486],{"slug":449,"title":450,"publishedAt":451,"readingMinutes":103},"nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","2026-09-01T11:24:15.087Z",{"slug":453,"title":454,"publishedAt":455,"readingMinutes":456},"nextjs-weekly-server-actions-mutations-security","Next.js Server Actions: Mutations & Security (Cheat Sheet)","2026-09-08T10:57:00.975Z",15,{"slug":458,"title":459,"publishedAt":460,"readingMinutes":103},"nextjs-weekly-parallel-intercepting-routes-modals","Next.js Parallel & Intercepting Routes: Modals Done Right","2026-09-15T17:50:47.218Z",{"slug":462,"title":463,"publishedAt":464,"readingMinutes":465},"nextjs-weekly-middleware-to-proxy-network-boundary","Next.js proxy.ts Explained (with Cheat Sheet)","2026-09-22T19:36:08.416Z",12,{"slug":467,"title":468,"publishedAt":469,"readingMinutes":456},"nextjs-weekly-route-handlers-caching-streaming","Next.js Route Handlers: GET Stopped Caching in 15 — How to Cache in 16","2026-09-29T09:16:08.176Z",{"slug":471,"title":472,"publishedAt":473,"readingMinutes":103},"nextjs-weekly-streaming-metadata-head","Next.js Streaming Metadata: Why Your `\u003Chead>` Looks Incomplete","2026-10-06T06:10:31.468Z",{"slug":475,"title":476,"publishedAt":477,"readingMinutes":456},"nextjs-weekly-image-priority-quality-changes","Next.js 16 Image: priority Deprecated, quality Now Allowlisted","2026-10-13T06:10:29.815Z",{"slug":479,"title":480,"publishedAt":481,"readingMinutes":103},"nextjs-weekly-instant-navigations-partial-prefetching","Next.js 16.3 Instant Navigations: How Partial Prefetching Works","2026-10-20T06:10:59.936Z",{"slug":483,"title":484,"publishedAt":485,"readingMinutes":103},"nextjs-weekly-updatetag-vs-revalidatetag","Next.js 16 updateTag vs revalidateTag: What Changed","2026-10-27T06:11:02.727Z",{"slug":46,"title":110,"publishedAt":114,"readingMinutes":113},{"id":488,"locked":18},"01a1972e-c4b7-7459-a148-a37cb6fb74ad",[490],{"id":45,"slug":46,"title":48,"_count":491},{"questions":51},[493],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":495,"questionCount":51},{"questions":51},[]]