Nuxt 3 Static Generation with On-Demand ISR: Architecture and Trust Boundaries
Design a minimal Nuxt 3 SSG with incremental regeneration, separating server data fetching from UI and protecting rebuild triggers.
01 Sept 2025, 21:26 UTC

Problem: A Nuxt 3 content site must pre-render most pages at build time, yet individual pages should be refreshable without a full site rebuild. Nuxt 3 runs on Nitro, which supports static generation and on-demand incremental regeneration, but the boundary between build-time HTML and runtime revalidation is easy to blur. A page component that fetches data directly, or a publicly accessible rebuild endpoint, can produce stale content, unexpected rebuilds, or abuse.
Takeaway: Keep data fetching in server-side composables or Nitro handlers, keep page components free of secrets and direct external calls, protect rebuild webhooks with a secret, and verify regeneration through build output and Nitro logs rather than assuming a page refreshed.
Requirements
- Nuxt 3 application with Nitro as the server runtime.
- A content source reachable at build time, such as a CMS API or local Markdown files.
- A deployment target that serves static HTML and, if using ISR, supports serverless functions or edge revalidation.
- A way to authenticate rebuild triggers.
Smallest suitable design
Separate concerns into three layers:
- Page components in
pages/: present markup only. They consume data passed from Nuxt; they do not call external APIs directly. - Data layer in
composables/: server-side functions such asuseContent()that run during SSR or static generation. They may contain API keys because they execute on the server. - Rebuild trigger in
server/api/: a Nitro endpoint that validates a secret and asks the platform to regenerate specific routes.
Example configuration
Run this in the project root as a normal user. Replace <PRESET> with your deployment target, such as netlify, vercel, or cloudflare. Exact Nitro route-rule keys for ISR vary by minor release; verify them against your installed version before deploying.
export default defineNuxtConfig({
nitro: {
preset: '',
routeRules: {
// Illustrative only; confirm ISR syntax for your Nitro version.
'/blog/**': { isr: true, swr: 60 },
},
},
})
Expected check: nuxt generate completes with exit code 0 and produces HTML files under .output/public. Risk: an incorrect preset or route rule can cause the build to skip static generation or attempt runtime rendering on a static host.
Trust and data boundaries
Nuxt distinguishes between code that runs on the server during rendering and code that runs in the browser. A composable wrapped in useFetch or useAsyncData executes on the server during static generation; the browser receives only the serialized payload. Do not place CMS tokens inside a composable that the client can import and call directly.
The server/ directory is the Nitro layer. It is isolated from page components by design. A rebuild webhook lives there, not in a page or plugin, so an attacker who compromises client-side code cannot reach it directly.
Data flow at build time:
- Nuxt calls the data composable for each dynamic route.
- The composable fetches from the CMS and returns content.
- Nitro writes rendered HTML to
.output/public. - A later webhook tells the platform to revalidate one or more routes.
Operational checks
Run locally before deploying:
nuxt generate --debug
Where: project root. Permissions: normal user. Expected: debug output lists generated routes; .output/public contains HTML for those routes. Risk: --debug slows the build and writes verbose logs; do not leave it enabled in CI unless needed.
After a regeneration trigger, check Nitro logs for ISR or revalidation events and confirm timestamps match the expected interval. A health endpoint such as /api/health can confirm the server layer is reachable, though it does not prove content freshness.
Protect the webhook:
export default defineEventHandler(async (event) => {
const secret = getHeader(event, 'x-rebuild-secret')
if (secret !== process.env.REBUILD_SECRET) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
// Trigger platform-specific revalidation here.
return { ok: true }
})
Where: server/api/rebuild.post.ts. Permissions: normal user to write; the deployment platform must expose REBUILD_SECRET. Risk: exposing this endpoint without authentication allows unauthorized or repeated regeneration, which can exhaust build minutes or cause denial of service.
Failure modes
| Failure | Cause | Detection |
|---|---|---|
| Stale content after update | Webhook failed silently or platform did not revalidate | Compare CMS last-modified time with HTML file timestamp |
| Unexpected full rebuild | Conflicting module overrides Nitro static pipeline | Review module registry; build duration jumps sharply |
| Regeneration timeout | Too many routes queued for ISR at once | Nitro logs show incomplete revalidation jobs |
| Unauthorized regeneration | Webhook lacks secret validation | Access logs show 200 responses from unknown sources |
Conditions that would change the design
- If content updates more often than it is read, prefer server-side rendering over static generation with ISR.
- If the deployment host does not support serverless functions, ISR may not be available; use full
nuxt generateon a schedule instead. - If regeneration time exceeds a configurable threshold, fall back to a full static regeneration or alert operators rather than allowing partial stale states to persist.
- If Nuxt or Nitro minor releases change ISR behavior, update the route-rule configuration and re-test locally.
Limitations and verification
Nuxt 3 and Nitro are still evolving; APIs and ISR behavior may change between minor releases. This note describes established patterns, not a tested production reference. Verify locally with nuxt generate --debug, inspect .output/public, and confirm route rules against your installed Nitro version before deploying.
Rollback: because nuxt generate writes build output, roll back by redeploying the previous build artifact or reverting the commit and redeploying. There is no in-place database rollback for static HTML.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.