"Should my Nuxt app be SSR or static?" is the wrong question for most projects, because the answer differs per page. A pricing page is the same for every visitor. A product page changes a few times a day. A dashboard is different for every user and should never be indexed. With routeRules, one Nuxt app can give each kind of page its own rendering mode, and the modes you choose decide what your host has to run.
The examples use Nuxt 4.4 with the app/ directory; server/ stays at the project root.
Universal rendering is the default
Out of the box, Nuxt uses server-side rendering (SSR), which its docs call universal rendering. When a browser asks for a URL, Nuxt runs your Vue components on the server and sends back finished HTML. The visitor sees content straight away, and search engines can read it without running JavaScript. Then the same code runs again in the browser and attaches event listeners to that HTML. This step is called hydration, and after it the page behaves like any Vue app: later navigation happens in the browser, without a full page load.
A page that fetches data shows the split clearly:
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
</script>
<template>
<h1>{{ product?.name }}</h1>
<p>{{ product?.description }}</p>
</template>
On the first request, useFetch runs on the server, the HTML arrives with the product in it, and the data travels along in the page payload so the browser does not fetch it a second time.
The cost is a server: something has to run JavaScript for every request that is not cached. The rendering guide calls this default especially suited to content sites: blogs, marketing sites, shops and marketplaces.
The opposite is client-side rendering, a plain single-page app: the server sends an almost empty HTML file and the browser builds the page. You switch the whole app to it with ssr: false in nuxt.config.ts. It is cheaper to host and simpler to write, because no code runs on a server, but the first view waits for JavaScript and crawlers see little.
Build a static site with nuxt generate
If every page can be built ahead of time, you do not need a server at all. nuxt generate builds the app, starts it once, and renders pages to HTML files. This is static site generation, often shortened to SSG. Run it, then preview the result with serve, a small static file server from npm:
npx nuxt generate
npx serve .output/public
Nuxt finds the pages by crawling, as the prerendering guide describes. It renders /, every non-dynamic page in app/pages/, and any routes you list, then follows every <a href> it finds in the HTML and repeats until there are no new links. A dynamic route such as /products/[id] has no fixed address, so its pages are only built when a link points at them. List the ones no link reaches:
export default defineNuxtConfig({
nitro: {
prerender: {
routes: ['/sitemap.xml', '/products/42'],
},
},
})
The output in .output/public is plain files, ready for any static host. Two consequences are easy to miss. Your server/api routes do not exist once the site is live, because there is no server, so pages must get everything they need at build time. And the content only changes when you build again.
Set a rendering mode per route with routeRules
Most real apps sit between these two extremes, and routeRules lets you choose per route. Nuxt calls this hybrid rendering. Rules are keyed by path, and ** matches everything below a path:
export default defineNuxtConfig({
routeRules: {
// Built once at build time, served as static files
'/': { prerender: true },
'/pricing': { prerender: true },
'/blog/**': { prerender: true },
// Rendered on request, cached for an hour, refreshed in the background
'/products/**': { swr: 3600 },
// Rendered only in the browser, with its own layout, kept out of search
'/dashboard/**': {
ssr: false,
appLayout: 'dashboard',
headers: { 'x-robots-tag': 'noindex' },
},
// Old URLs, moved for good
'/shop/**': { redirect: { to: '/products/**', statusCode: 301 } },
},
})
Here is what each rule does:
prerender: truerenders the route during the build and serves the result as a static file.swr: 3600(stale-while-revalidate) caches the server's response for 3,600 seconds. After that, the next visitor still gets the cached page while Nuxt renders a fresh one in the background. Nitro'snode-serveroutput can cache the full response itself.isr(incremental static regeneration) works likeswr, but also puts the response in the hosting platform's CDN cache, on the platforms that support it; the docs name Netlify and Vercel.isr: truekeeps the page cached until your next deploy.ssr: falseskips server rendering for those routes only. The browser gets an empty shell and builds the page itself.appLayoutpicks the layout fromapp/layouts/for every matching route, hereapp/layouts/dashboard.vue, without adefinePageMetacall in each page.headersadds response headers.redirectsends a redirect from the server. It is a temporary 307 unless you pass astatusCode, and a trailing/**on both sides carries the rest of the path across, so/shop/42goes to/products/42.
A ** prerender rule does not find every page under nuxt build. Nuxt prerenders the non-dynamic pages it matches, but blog posts from a dynamic page such as app/pages/blog/[slug].vue are only found through links, and nuxt build follows <a href> links only when you set nitro.prerender.crawlLinks: true (nuxt generate turns it on for you).
That switch is a blunt fix in an app like this one: the crawler then prerenders every page it reaches through a link, product pages included, unless their rule says prerender: false. List the posts in nitro.prerender.routes instead, then check the build log: it lists every route it prerendered, and a post missing from it was not built.
Routes with swr or isr also write a _payload.json next to the HTML, so client-side navigation to a cached page reuses the cached data instead of calling your API again.
Choose a rule for each kind of page
Keep universal rendering as the default and add a rule only where a kind of page gives you a reason. Two questions decide which rule: is the page the same for every visitor, and how often does it change?
- Marketing, docs and blog pages are the same for everyone and change when you deploy. Use
prerender: true. They cost nothing to serve and load fastest. - Product and catalogue pages are the same for everyone, must be indexed, and change during the day. There are often too many to build on every deploy. Use
swrwith a time that matches how stale a page may be, orisron Netlify or Vercel. - Dashboards, account and admin pages are different for each user, sit behind a login and have no search value. Use
ssr: false. Server rendering would only add work, and there is nothing on the page for a crawler anyway. - Everything else, such as search results or pages that mix personal and public content, keeps the default.
What each mode needs from your host
The command you build with decides what you can deploy:
nuxt generateproduces static files only. Any static host or CDN can serve them. Server routes,swr,isrand other hybrid rules do not work here, because nothing runs after the build.nuxt buildproduces a server, plus static files for the prerendered routes. It needs somewhere to run JavaScript: a long-running Node process, or serverless and edge functions on a platform.isrneeds a platform with a CDN cache that understands it. On your own Node server, useswr.
With the default output, you start the server like this, and it listens on port 3000 unless you set PORT:
npx nuxt build
NODE_ENV=production node .output/server/index.mjs
Set NODE_ENV=production when you run it. The deployment docs warn that some dependencies, Vue Router among them, only strip development warnings when it is set.
Pick a deployment preset
Nitro, the server engine inside Nuxt, turns the same app into different outputs through presets. The default is node-server. When you run nuxt generate, a static preset is used instead. When you build in the CI of a supported platform, including Vercel, Netlify and Cloudflare, Nitro detects it and picks that platform's preset without any configuration. To choose a preset yourself, name it in nuxt.config.ts. This one keeps the Node server even where Nitro would detect a platform:
export default defineNuxtConfig({
nitro: {
preset: 'node-server',
},
})
Or set it for a single build, which is easier in CI: NITRO_PRESET=node-cluster npx nuxt build. If both are set, the variable wins for that build.
Nitro 2.13 ships presets for many hosts, among them node-cluster for several Node processes, bun, deno-server, aws-lambda, cloudflare-module and vercel. The deployment guide links to the full list. Your route rules travel with the preset: on Netlify and Vercel, Nuxt writes them into the platform's own routing and cache rules where it can.