Designing Hybrid Rendering in Nuxt 3 with Nitro routeRules
An architecture note on Nuxt 3 hybrid rendering: classifying routes by staleness and personalization, the smallest routeRules map, the cached-HTML trust boundary, and checks that prove it works.
16 Sept 2025, 17:13 UTC

The Problem: One Rendering Mode Rarely Fits a Whole Site
A mixed-content site usually contains pages with very different needs: a marketing homepage that changes quarterly, a blog that updates daily, and a dashboard that shows per-user data. Picking a single rendering strategy forces a compromise—full static generation (SSG) is fast but goes stale between builds, while full server-side rendering (SSR) keeps everything fresh at the cost of server work for pages that barely change.
Nuxt 3, through its Nitro server engine, supports hybrid rendering: per-route rendering and caching rules defined in one place. This note walks through the requirements, the smallest configuration that satisfies them, the trust boundary that caching introduces, and how to verify the result. Behavior described here reflects Nuxt 3.x as of my knowledge; Nitro's caching semantics have evolved across releases, so confirm option names against your installed nuxt and nitropack versions before relying on them.
Requirements Before You Configure Anything
Write down, per section of the site, two facts: how stale the content may be, and whether it depends on the current user. That produces a simple classification:
- Never changes per user, rarely changes at all → prerender at build time.
- Same for all users, tolerates minutes-to-hours of staleness → cached SSR with revalidation.
- Depends on the session → fully dynamic SSR, or client-only rendering if SEO is irrelevant.
If a page mixes categories (static article plus a "Welcome back, Alice" header), treat it as dynamic and push the personalized fragment to the client.
The Smallest Suitable Design
Default everything to SSR and add routeRules entries only where a requirement from the list above exists. Every rule you add is a rule you must audit later, so the smallest map that satisfies the requirements is the right one.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Static marketing pages: generated once at build time
'/': { prerender: true },
'/about': { prerender: true },
// Blog: cached, revalidated in the background after 1 hour
'/blog/**': { swr: 3600 },
// Dashboard: no SEO need, per-user data — render on the client
'/dashboard/**': { ssr: false }
// Everything else falls through to default SSR, uncached.
}
})
What Each Mode Does
prerender: true— the page is rendered to a static HTML file duringnuxi buildand served as a file. It does not change until the next build (unless you configure incremental revalidation, which is preset-dependent).swr: <seconds>— stale-while-revalidate caching. The first request renders and caches the page; requests within the TTL get the cached copy; after expiry, one request triggers a background refresh while visitors keep receiving the stale copy.ssr: false— the server sends an empty shell and Vue renders entirely in the browser. Useful for authenticated areas, but the page is invisible to crawlers and slower to first content.
An important caveat: swr is implemented via cache headers and Nitro's cache layer, and what actually happens depends on your deployment preset. On a plain Node server, Nitro handles caching internally; on some serverless and edge platforms the headers are advisory and the platform's CDN decides whether to honor them. Treat swr as a request, not a guarantee, until you have verified it on your target platform.
The Trust Boundary: Cached HTML Is Shared HTML
Hybrid rendering creates one security boundary that does not exist in pure SSR: any prerendered or cached response is served to every visitor. The rule that follows is absolute—never let per-user data reach the server-side render of a cached route. A single '/**': { swr: true } wildcard added for convenience can cache a page that embeds a session-derived username and serve it to the wrong person. This is the most common real incident with hybrid rendering.
For pages that mix public and private content, render the public shell on the server and fetch the private fragment on the client:
<script setup lang="ts">
// Runs only in the browser; never baked into cached HTML
const { data: profile } = await useFetch('/api/me', { server: false })
</script>
The second boundary is code location. Files under server/api/ and server/routes/ execute only on the server and are the correct home for database access and third-party credentials. Anything imported from pages/, components/, or app.vue is bundled into client JavaScript—treat every value reachable there as public, including anything not explicitly hidden in runtime config.
Operational Checks After Building
Run these after npx nuxi build (and against the deployed environment, since caching behavior is preset-dependent):
1. Confirm prerendered output exists
Inspect .output/public/. Each prerender: true route should have a corresponding HTML file (e.g., .output/public/about/index.html or about.html, depending on version). A missing file means the route silently fell back to SSR or the crawl failed—check the build log.
2. Confirm cache headers on SWR routes
From any machine that can reach the server, run curl -sI https://your-host/blog/some-post twice. Look for a Cache-Control header containing max-age or s-maxage and stale-while-revalidate directives. Absent headers mean the rule didn't match (check wildcard syntax) or the platform stripped them.
3. Prove user isolation on dynamic routes
Log in as two different users against a route that must stay dynamic and confirm the responses differ. Then re-audit your routeRules for wildcards broad enough to cover that route. Repeat this audit whenever authentication scope changes.
Failure Modes and What Would Change the Design
- Wildcard overreach: broad patterns like
'/**'or'/app/**'catching authenticated pages. Mitigation: keep rules specific and re-audit on every auth change. - Platform divergence:
swrsemantics differ between the Node preset and edge/serverless presets. If you migrate hosts, re-run check 2 before trusting any cached route. - Stale prerenders: a prerendered page only updates on rebuild. If content must change faster than your pipeline allows, move that route from
prerendertoswr. - New personalization: adding per-user content to a previously static page forces a choice—drop the cache rule or move the personalized part to a client-side fetch. Never leave it in the server render.
Because this configuration only changes build and routing behavior, reverting is straightforward: remove the entry from routeRules and redeploy, returning the route to default SSR. The risk is not the rollback—it is the window during which a bad rule was live, which is why the isolation check matters more than the config itself.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.