In Nuxt 4, what does routeRules '/dashboard/**': { ssr: false } change for the dashboard, and what do you give up compared with SSR?
answer
- the server still answers the request
- empty #__nuxt root, loading template
- middleware and fetches move to browser
- no content for crawlers
- the rest of the app stays SSR
basics
~20 sWith '/dashboard/**': { ssr: false }, Nuxt 4 answers dashboard URLs with an app shell: an empty #__nuxt root plus the optional loading template. Middleware, data fetching and rendering move to the browser, at the cost of indexable HTML and an early first paint.
solid answer
~50 sFor paths matching `'/dashboard/**'`, Nitro still receives the request but skips rendering the Vue app. It returns a cached shell: head tags, script and style links, the public runtime config, an empty `<div id="__nuxt">`, and `app/spa-loading-template.html` beside it if the file exists. The payload marks the page as not server-rendered, and no data is embedded. In the browser the app boots, runs route middleware (for example the auth redirect) and fetches data, then renders. The gains are no server rendering cost, free use of browser APIs and no hydration mismatches. The losses are an empty page for crawlers and link previews, a slower first paint on weak devices, and auth that only takes effect once the JavaScript runs, so the dashboard's APIs must enforce access themselves. Unlike app-wide `ssr: false`, every other route keeps SSR.
code
ts · 7 lines// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// shell only; rendered, guarded and fetched in the browser
'/dashboard/**': { ssr: false },
},
})go deeper
Recall that ssr: false on a route sends an empty shell and lets the browser render the page.
Explain what the shell contains, where middleware and data fetching now run, and how the loading template is shown.
Weigh the trade: SEO and first paint against server cost and browser APIs, and keep data endpoints guarded, since a client redirect protects nothing.
Decide which product areas may be client-only and set the rule that any public, indexable surface keeps server rendering.
## What the server sends now With `routeRules: { '/dashboard/**': { ssr: false } }`, a request for `/dashboard/reports` still reaches the Nitro server, but Nuxt marks it **no-SSR** and uses its SPA renderer instead of rendering the Vue app: - the response is a **shell**, computed once and reused: head tags, links to scripts and styles, and the public part of the runtime config; - the app root, `<div id="__nuxt">`, is **empty**; - if `app/spa-loading-template.html` exists, its markup is rendered **beside** the root in Nuxt 4 and stays visible until the app mounts; - the payload records `serverRendered: false`, and no fetched data is embedded, since nothing was fetched. ## What moves into the browser 1. The browser downloads and runs the app bundle. 2. The router resolves `/dashboard/reports` and runs its **route middleware**. This is the first time any middleware runs for this request, because the server never ran the app. 3. The page's data fetching runs in the browser; `useFetch` and `useAsyncData` behave as they do on a client-side navigation. 4. The page renders into `#__nuxt`, replacing the loading template. In-app navigation from a server-rendered page into `/dashboard` involves no HTML request at all, so the rule only affects direct loads and reloads. ## The loading experience Between the shell arriving and the app mounting, the visitor sees whatever the loading template provides: - with an `app/spa-loading-template.html` file, its markup is shown; - with `spaLoadingTemplate: true` in `nuxt.config.ts` and no file, Nuxt uses its built-in default template; - with neither, nothing is rendered, and the page stays blank until the app draws itself. Keep the template light, with inline styles and no scripts, since it has to appear before any bundle has loaded. Apart from the head tags, it is also the only content a crawler or link preview sees for these URLs. ## Gains and losses | | SSR, the default | `ssr: false` for the dashboard | |---|---|---| | First response | full HTML | shell only | | Server work per request | a full render | nearly none | | Crawlers and link previews | see content | see an empty root | | First meaningful paint | early, before JS | after the JS downloads and runs | | Browser APIs in components | need guards | usable anywhere | | Hydration mismatches | possible | impossible, nothing to hydrate | | Auth redirect on a direct load | server-side, before any HTML | after the app boots | For a signed-in dashboard, the losses are usually acceptable: nobody needs it indexed, users return often, and the browser caches the bundle between visits. ## Per-route versus app-wide - **App-wide `ssr: false`** in `nuxt.config.ts` turns every page into a shell. It allows a purely static deployment with `nuxt generate`, which writes an `index.html` entry plus `200.html` and `404.html` fallbacks. - **Per-route `ssr: false`** keeps SSR for marketing, blog and product pages, which matter for search. The app still needs its Nitro server, because the other routes depend on it. ## Security and correctness notes - The shell contains no dashboard data, so serving it to a signed-out visitor leaks nothing; the **data endpoints** must still check the session, because a client-side redirect is not a security boundary. - Don't put a cache rule on shell routes expecting a speed-up: the shell is already rendered once and reused. - Components shared between dashboard and SSR pages must stay SSR-safe, because they still render on the server elsewhere. ## Narrower alternatives - `<ClientOnly>` or a `.client.vue` component for a single browser-only widget on an SSR page. - Keeping SSR while skipping only the server-side fetch of per-user data is a data-fetching option, not a rendering mode.
- A signed-out visitor opens /dashboard/reports directly. What happens, step by step?The server returns the shell without running any middleware. The browser boots the app, the router runs the auth middleware for `/dashboard/reports`, and it redirects to the login page client-side. The visitor briefly sees the loading template, never dashboard data, provided `/api/reports` itself rejects requests without a session.
- When would you make the whole app ssr: false instead of one subtree?When nothing in it needs indexing or a fast first paint for new visitors, such as an internal back-office tool. Then `nuxt generate` can emit static shells, including `index.html`, `200.html` and `404.html`, and no Node server is needed for pages. With public marketing or blog pages in the same app, the per-route rule is the better fit.
saying these in an interview costs you the question
- With ssr: false the server returns a 404 for dashboard URLs
- Route middleware still redirects on the server for ssr: false routes
- A client-side auth redirect is enough to protect dashboard data
- ssr: false on one route turns off SSR for the whole app
- ssr: false pages still hydrate server-rendered markup
- A per-route ssr: false app can be hosted as static files alone