SSR, SSG & ISR
@fluenti/nuxt supports all Nuxt rendering modes out of the box. The module handles locale detection, payload serialization, and route generation for each mode automatically. For general SSR concepts, see the SSR & Hydration guide.
| Mode | How it works | Best for |
|---|---|---|
| SSR | Server detects locale per-request, injects into payload, client hydrates | Dynamic apps, personalized content |
SSG (nuxt generate) | Each locale route is pre-rendered with the correct locale from its URL path | Marketing sites, blogs, docs |
| ISR | Pre-rendered with configurable TTL, re-validated on demand | High-traffic sites with periodic updates |
SPA (ssr: false) | Client-side path + cookie detection, no server rendering | Dashboards, internal tools |
SSR Mode (Default)
Section titled “SSR Mode (Default)”Server-side rendering is the default Nuxt behavior. The Fluenti plugin runs the full locale detection chain on every request and stores the result in the Nuxt payload for hydration.
Plugin Lifecycle
Section titled “Plugin Lifecycle”Server Client────── ──────1. Plugin hoists cookie + header reads 1. Read locale from nuxtApp.payload (before any await — required for 2. Skip detection chain entirely Nuxt async local storage) 3. Watch route.path for navigation2. Run detection chain: 4. Sync cookie on locale change path → cookie → header 5. Update <html lang> attribute3. Store locale in nuxtApp.payload4. Set event.context.locale5. Render HTML with detected localeLocale Detection Chain
Section titled “Locale Detection Chain”The detection chain runs in order. The first detector to call ctx.setLocale() wins, and subsequent detectors are skipped:
- Path detector — extracts locale from URL prefix (
/ja/aboutresolves toja) - Cookie detector — reads the
fluenti_localecookie (configurable key) - Header detector — parses the
Accept-Languageheader and negotiates against available locales
You can customize the chain via detectOrder:
export default defineNuxtConfig({ modules: ['@fluenti/nuxt'], fluenti: { locales: ['en', 'ja', 'zh'], defaultLocale: 'en', detectOrder: ['path', 'cookie', 'header'], detectBrowserLanguage: { useCookie: true, cookieKey: 'fluenti_locale', }, },})Cookie-Based Persistence
Section titled “Cookie-Based Persistence”When detectBrowserLanguage.useCookie is enabled, the plugin:
- Server: reads the cookie value and passes it to the detection chain
- Client: watches the reactive
currentLocaleref and writes back to the cookie on every change
This means a user who switches to Japanese on one page will see Japanese on their next visit, even if they navigate to a URL without a locale prefix.
Hydration Safety
Section titled “Hydration Safety”The server stores the detected locale in nuxtApp.payload['fluentiLocale']. On the client:
- If the payload contains a locale, the client uses it directly — no detection chain runs
- This guarantees the client renders the same locale the server did, preventing hydration mismatches
- After hydration, the client watches
route.pathand updates the locale reactively on navigation
// plugins/i18n.ts — reading the detected localeexport default defineNuxtPlugin((nuxtApp) => { // The Nuxt module already detected the locale and stored it in payload const initialLocale = nuxtApp.payload?.['fluentiLocale'] ?? 'en'
const fluenti = createFluenti({ locale: initialLocale, messages: { en, ja, zh }, }) nuxtApp.vueApp.use(fluenti)
// Sync locale when the module's reactive ref changes if (nuxtApp.$fluentiLocale) { watch(nuxtApp.$fluentiLocale, (newLocale: string) => { fluenti.global.setLocale(newLocale) }) }})SSR Configuration
Section titled “SSR Configuration”For pure SSR (no prerendering), disable prerendering explicitly so every request goes through the detection chain:
export default defineNuxtConfig({ modules: ['@fluenti/nuxt'],
// Disable prerendering — locale detection requires live SSR routeRules: { '/**': { prerender: false }, },
fluenti: { locales: ['en', 'ja', 'zh'], defaultLocale: 'en', detectBrowserLanguage: { useCookie: true, cookieKey: 'fluenti_locale', }, },})Static Site Generation (SSG)
Section titled “Static Site Generation (SSG)”nuxt generate pre-renders all locale routes at build time. The output is a set of static HTML files that can be deployed to any CDN or static host.
How SSG Works with Fluenti
Section titled “How SSG Works with Fluenti”During the nuxt generate build:
- The module enables
nitro.prerender.crawlLinksautomatically, so all locale-prefixed routes are discovered from your<NuxtLinkLocale>links - For each route, the path detector extracts the locale from the URL (
/ja/aboutresolves toja) - Cookie and header detectors silently skip (no HTTP context during prerendering) — this is by design
- Each page is rendered in the correct locale and written to disk
Output Structure
Section titled “Output Structure”With strategy: 'prefix_except_default', the generated output looks like:
.output/public/├── index.html ← en (default, no prefix)├── about/index.html ← en├── ja/│ ├── index.html ← ja│ └── about/index.html ← ja├── zh/│ ├── index.html ← zh│ └── about/index.html ← zh└── _nuxt/ ← client bundlesSSG Configuration
Section titled “SSG Configuration”export default defineNuxtConfig({ modules: ['@fluenti/nuxt'],
fluenti: { locales: ['en', 'ja', 'zh'], defaultLocale: 'en', strategy: 'prefix_except_default', detectBrowserLanguage: { useCookie: true, cookieKey: 'fluenti_locale', }, },
// Enable SSG mode nitro: { preset: 'static', },})Prefix Strategy and SSG
Section titled “Prefix Strategy and SSG”When using strategy: 'prefix' (all locales get a URL prefix, including the default), the module replaces the default / prerender route with /<defaultLocale>:
// With strategy: 'prefix', the prerenderer starts from /en instead of /// This is automatic — no manual configuration neededThis ensures the crawler begins from a valid route and discovers all locale variants.
Client-Side Navigation After SSG
Section titled “Client-Side Navigation After SSG”After the static HTML loads and Vue hydrates, client-side navigation works normally:
- Route changes are intercepted by Vue Router (no full page reload)
- The locale ref updates reactively based on the new path
- The cookie is synced on every locale change
NuxtLinkLocalegenerates correct locale-prefixedhrefattributes
Incremental Static Regeneration (ISR)
Section titled “Incremental Static Regeneration (ISR)”ISR combines the performance of static generation with the freshness of server rendering. Pages are pre-rendered and cached, then re-validated after a configurable TTL.
ISR Configuration
Section titled “ISR Configuration”export default defineNuxtConfig({ modules: ['@fluenti/nuxt'],
fluenti: { locales: ['en', 'ja', 'zh'], defaultLocale: 'en', strategy: 'prefix_except_default', detectOrder: ['path'], isr: { enabled: true, ttl: 60, // re-validate every 60 seconds }, },})This auto-generates routeRules for all locale patterns:
// Generated automatically by the module:routeRules: { '/**': { isr: 60 }, // default locale (prefix_except_default) '/ja/**': { isr: 60 }, '/zh/**': { isr: 60 },}ISR Options
Section titled “ISR Options”| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | — | Enable ISR route rules generation |
ttl | number | 3600 | Cache TTL in seconds (how long before re-validation) |
ISR and Locale Detection
Section titled “ISR and Locale Detection”ISR caches responses by URL path. This has an important implication: non-path locale detectors (cookies, headers) can cause the wrong locale to be served from cache.
ISR with Different Strategies
Section titled “ISR with Different Strategies”| Strategy | ISR compatibility | Notes |
|---|---|---|
prefix_except_default | Best | Each locale has a unique URL path |
prefix | Best | All locales have unique prefixed paths |
prefix_and_default | Good | Default locale has two paths (with and without prefix) |
no_prefix | Not recommended | All locales share the same URL — cache serves only one |
domains | Good | ISR caches per-domain, each domain maps to one locale |
Combining ISR with Custom Route Rules
Section titled “Combining ISR with Custom Route Rules”You can mix ISR locale rules with your own route rules. The module merges its generated rules with any existing routeRules:
export default defineNuxtConfig({ modules: ['@fluenti/nuxt'],
// Your custom route rules are preserved routeRules: { '/api/**': { cors: true }, '/admin/**': { ssr: false }, },
fluenti: { locales: ['en', 'ja', 'zh'], defaultLocale: 'en', isr: { enabled: true, ttl: 3600 }, },})SPA Mode
Section titled “SPA Mode”When ssr: false is set, Nuxt runs entirely on the client. There is no server-side locale detection, no payload, and no pre-rendered HTML.
How SPA Locale Detection Works
Section titled “How SPA Locale Detection Works”Without a server, the client-side plugin uses a simplified detection flow:
- Check
nuxtApp.payload['fluentiLocale']— empty in SPA mode - Fall back to path detection: extract locale from
route.path - If no path prefix found, fall back to cookie: read
fluenti_localecookie - If no cookie, use
defaultLocale
SPA Client Boot────────────────1. No payload (ssr: false)2. Extract locale from URL path (/ja/about → ja)3. If no prefix → read cookie4. If no cookie → defaultLocale5. Watch route.path for subsequent navigation6. Sync cookie on locale changeSPA Configuration
Section titled “SPA Configuration”export default defineNuxtConfig({ modules: ['@fluenti/nuxt'],
// SPA mode — no server-side rendering ssr: false,
fluenti: { locales: ['en', 'ja', 'zh'], defaultLocale: 'en', strategy: 'prefix_except_default', detectBrowserLanguage: { useCookie: true, cookieKey: 'fluenti_locale', }, },})Differences from SSR
Section titled “Differences from SSR”| Aspect | SSR | SPA |
|---|---|---|
| Initial HTML | Contains translated content | Empty shell (no translated text) |
| Locale detection | Server runs full chain | Client-only: path, then cookie |
| SEO | Search engines see translated HTML | No translated content in source |
| Hydration | Payload ensures locale match | No hydration (client renders from scratch) |
Accept-Language | Header detector works | Not available (browser API only) |
| First paint | Translated content visible immediately | Brief flash before JS loads |
Cookie Restoration in SPA
Section titled “Cookie Restoration in SPA”A key SPA feature is cookie-based locale restoration. When a user:
- Visits the site and switches to Japanese (cookie set to
ja) - Returns later to
/(no locale prefix) - The cookie detector finds
jaand sets the locale accordingly
This works because in SPA mode, when no path prefix is found, the plugin falls back to the cookie value before defaulting.
Performance Considerations
Section titled “Performance Considerations”- Pros: Correct locale on first paint, full SEO support,
Accept-Languagenegotiation - Cons: Every request runs through the server, detection chain adds minimal overhead
- Tip: Enable
detectBrowserLanguage.useCookieso returning users skip header negotiation
- Pros: Fastest TTFB (pre-built HTML), CDN-friendly, no server runtime
- Cons: Build time scales with
pages x locales, no per-request detection - Tip: Use
NuxtLinkLocaleconsistently so the crawler discovers all locale routes. Enable cookie persistence for client-side locale memory.
- Pros: Combines SSG speed with near-real-time content updates, scales to many locales
- Cons: First request after TTL expires may be slower (re-validation), cache invalidation complexity
- Tip: Use
detectOrder: ['path']to avoid cache/locale mismatches. Set TTL based on how frequently your content changes.
- Pros: Simplest deployment (static files), no server infrastructure
- Cons: No SEO for translated content, flash of untranslated content, no
Accept-Languagesupport - Tip: Always enable cookie persistence so returning users get their preferred locale immediately.
Bundle Size
Section titled “Bundle Size”All rendering modes benefit from Fluenti’s compile-time architecture. Translations are compiled at build time into optimized JavaScript functions, not parsed at runtime. This keeps the runtime overhead minimal regardless of rendering mode.
For large translation catalogs, enable code splitting to load only the translations needed for the current route:
export default { sourceLocale: 'en', locales: ['en', 'ja', 'zh'], codeSplitting: 'dynamic', // split translations per locale}