"SSR or SSG?" is a junior question. The senior answer is "per route": a marketing page is prerendered, a product page is cached and revalidated, a dashboard is client-rendered behind auth, and inside each page some components render only on the server and others hydrate only when visible. Nuxt exposes all of that through routeRules and a handful of component conventions, and the interview tests whether you can map requirements to those knobs.
ssr: false globally, or per route): the server sends an empty shell (spaLoadingTemplate), the browser renders everything. Right for authenticated, personalised, non-indexed areas where server rendering buys nothing.nuxt generate, or routeRules: { '/path': { prerender: true } }): HTML is produced at build time and served as static files. Nitro crawls links from the entry routes; nitro.prerender.routes and the prerender:routes hook add routes the crawler cannot discover. Payloads are extracted to _payload.json per page so client navigation between prerendered pages needs no server.routeRules: swr: seconds renders on demand, caches in Nitro's storage and serves stale while revalidating in the background; isr: seconds | true does the same through the platform's CDN cache on providers that support it (Vercel, Netlify); cache gives direct control over the Nitro cache entry; prerender, ssr: false, redirect, headers complete the set. A rule matches a path pattern, so the decision really is per route.<ClientOnly> with a #fallback renders its children only after hydration; Component.client.vue does the same per component. Use them for browser-only widgets, not to hide hydration bugs.Component.server.vue, experimental.componentIslands) render on the server and ship no JavaScript for the component; <NuxtIsland> renders one on demand. Interactive children can be marked nuxt-client when selectiveClient is on. Right for heavy static output: rendered Markdown, syntax highlighting, big tables.<LazyX hydrate-on-visible | hydrate-on-idle | hydrate-on-interaction | hydrate-on-media-query | hydrate-after | hydrate-when | hydrate-never> renders on the server and defers attaching Vue until the strategy fires. The markup, SEO and LCP are unaffected; the hydration CPU moves off the critical path.swr/isr; personalised → universal with no caching, or client-only if not indexed; expensive and static → server components inside whatever mode the route uses.node-server, cloudflare_module, vercel, netlify, …). The fullstack section covers presets.One app, four rendering strategies, decided in configuration:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/docs/**': { prerender: true },
'/products/**': { swr: 300 }, // fresh enough for prices, one render per five minutes per URL
'/blog/**': { isr: 3600 }, // CDN-level regeneration on platforms that support it
'/account/**': { ssr: false }, // personalised, behind auth, not indexed
'/api/**': { cors: true },
},
experimental: { componentIslands: { selectiveClient: true } },
nitro: {
prerender: {
crawlLinks: true,
routes: ['/sitemap.xml'], // not linked from any page
},
},
})
Routes the crawler cannot find, added from the data source at build:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('prerender:routes', async (routes) => {
const slugs = await $fetch<string[]>('https://cms.example.com/api/product-slugs')
for (const slug of slugs) routes.add(`/products/${slug}`)
})
})
Inside a page, deciding what ships to the browser component by component:
<template>
<article>
<!-- Interactive above the fold: normal hydration -->
<ProductGallery :images="product.gallery" />
<!-- Heavy, static markup: server component, zero client JS. The button inside stays interactive. -->
<ProductDescription :html="product.descriptionHtml">
<AddToCart nuxt-client :product-id="product.id" />
</ProductDescription>
<!-- Below the fold, interactive: rendered now, hydrated when it scrolls into view -->
<LazyProductReviews hydrate-on-visible :product-id="product.id" />
<!-- Needs the browser: rendered only on the client with a placeholder in the server HTML -->
<ClientOnly>
<ProductArViewer :model="product.arModel" />
<template #fallback>
<div class="aspect-video animate-pulse bg-elevated" />
</template>
</ClientOnly>
</article>
</template>
ProductDescription.server.vue is an ordinary SFC whose file name ends in .server.vue; Nuxt renders it on the server and never ships its script.
swr caches the rendered HTML by URL. If the page shows the user's name, the first user's name is served to everyone for the cache duration: a data disclosure, not a performance win. Cache only what is identical for every visitor, and keep personalisation in client-side fetches or in routes without a cache rule.
Wrapping a component in <ClientOnly> to silence a mismatch warning also removes it from the server HTML, so it disappears for crawlers and appears late for users. Fix the cause (hydration section) unless the component genuinely cannot render without a browser.
Which platforms support isr natively today, the current status of experimental SSR streaming, and whether componentIslands and selectiveClient are still behind the experimental flag.
routeRules above, build, run the Node server, and request /products/x three times with curl -i; observe the timing and the cache headers on the second and third response..server.vue and compare the page's client bundle with nuxt analyze before and after.hydrate-on-visible to a below-the-fold component and record a Performance profile of page load; find where its hydration moved to."Per route, not per app, because Nuxt's route rules let me mix them. For each route I ask whether crawlers or link previews need it, whether it is personalised, how often it changes and how expensive it is to render. Public and stable pages are prerendered and served as static files. Public pages that change get swr or isr so they render once per interval instead of per request. Personalised pages render universally without caching, or client-side if they are behind auth and not indexed. Inside any page I then go component by component: server components for heavy static markup, lazy hydration for below-the-fold interactivity, ClientOnly for things that genuinely need a browser."
"Both serve a cached render and regenerate it in the background after the interval. swr is implemented by Nitro: the rendered response is stored in Nitro's cache storage and served from there, so it works on any host that runs the Nitro server, and the cache lives wherever that storage points. isr delegates to the platform: on Vercel or Netlify the CDN holds the page and revalidates it at the edge, which scales without hitting my server. On a platform without native ISR it falls back to behaving like swr. In both cases the cache key is the URL, so neither is safe for personalised content."
Nothing yet. Notes filed here show up on this page and the section overview.
SEO, head and media
useSeoMeta and useHead on Unhead, titles and OG tags, canonical URLs, sitemaps and structured data with the Nuxt SEO modules, and the image, font and script modules that protect LCP and CLS.
Internationalisation
The @nuxtjs/i18n module — routing strategies, lazy-loaded messages, locale detection, SEO tags, formatting with Intl, right-to-left layouts and the hydration trap of a locale that differs between server and client.