Rendering Mode Quick Guide (Practitioner's Required Reading)
5 minutes to decide whether a page should be SSR / ISR / SSG / CSR. For deeper theory + risk background, see Nuxt Rendering Strategy Guide.
This guide aligns with framework's current auth design (device/auth completely client-only — see framework#44).
One-Line Positioning
ISR first + SSR guards paid content + CSR for user state + Hybrid hydrate for mixed pages
The framework does not preset a default rendering strategy (pages must declare defineRouteRules explicitly), but the recommended mental model is laid out below.
Decision Matrix (lookup by page type)
| Page Type | Rendering Strategy | Suggested TTL | Key Notes |
|---|---|---|---|
| Home / Category / List / Ranking / Marketing aggregation | ISR (primary) | Home 600 · category / list / ranking 1800 | Public skeleton via ISR; small user-state fragments (VIP badge, owned-chapter marker) wrapped in <ClientOnly> + onMounted fetch |
| Simple detail page (intro / rating / chapter list, no user state) | ISR | 3600 | Pure public data via ISR, best performance |
| Detail page (public + small user state, most common) | Hybrid 1: ISR shell + CSR user-state hydrate | 3600 | See decision tree + code example below |
| Detail page (public + heavy user state, personalized recommendations dominate) | Hybrid 2: SSR + public/private API separation | No cache | See Theory Guide §10 Public/Private API Separation |
| Reader / Player / Chapter content / Video stream (paid) | SSR + token auth ⚠️ | No cache | ISR / SWR / prerender forbidden (enforced by lint); prevents DOM tampering paywall bypass |
| Static pages (about / terms / EULA) | ISR | 3600 | Header / footer ops config is baked into the HTML and must be refreshable within an hour; no prerender (multi-domain sites would be forced to converge canonical) |
| Profile / Orders / VIP subscription / Wallet | CSR (ssr: false) | No cache | All user state, no SEO value |
| Chat / Tip / Live / Real-time interaction | CSR (ssr: false) | No cache | WebSocket / real-time data |
Marketing landing page (with ?channel_code=) | SSR (preserve channel) or ISR-with-varies | No cache / 600 | channel_code initCode stays server-side |
| Login / Register | CSR (ssr: false) | No cache | Private / user-state pages; ISR / prerender forbidden (hard constraint, same tier as profile pages) |
isronly accepts an integer between 60 and 3600 seconds (isr: truewas removed and fails the build, fw#414). The TTL is an upper bound on "how soon an update must be visible", not a CPU knob: when an entry expires nitro serves the stale copy first (SWR) and re-renders in the background, so the length never affects user wait time. UseNUXT_ISR_TTLto shorten TTLs in test environments instead of editing page declarations.
Detail Page Decision Tree
Does your page need SEO?
├─ No → CSR (ssr: false) (user-state / real-time interaction pages)
└─ Yes → Is it paid content body (chapter / video stream)?
├─ Yes → SSR (with token auth, prevents DOM tampering bypass)
└─ No → How much user-state content visually?
├─ None → Pure ISR
├─ Small (one or two chips / rows) → ISR shell + CSR hydrate (Hybrid 1, common detail pattern)
└─ Large (personalized recommendations dominate) → SSR + public/private API separation (Hybrid 2)
Detail Page Hybrid Strategy 1 Example (most common, memorize this)
<script setup>
defineRouteRules({ isr: 3600 })
// SSR / ISR phase: fetch public body (cache-safe)
const { data: detail } = await useAsyncData(() =>
api.manga.detail({ id: route.params.id }),
)
// User-state fragments not rendered during SSR
const isFavorited = ref(false)
const purchasedChapters = ref<string[]>([])
onMounted(async () => {
// By now framework auth.client has established token; private APIs are safe to call
const [fav, purchased] = await Promise.all([
$fetch('/api/user/favorite/check', { query: { id: route.params.id } }),
$fetch('/api/user/purchased-chapters', { query: { id: route.params.id } }),
])
isFavorited.value = fav.result
purchasedChapters.value = purchased
})
</script>
<template>
<article>
<SectionPublic :detail="detail" />
<ClientOnly>
<SectionUserBadges
:favorited="isFavorited"
:purchased="purchasedChapters"
/>
</ClientOnly>
<SectionChapterList :chapters="detail.chapters" />
</article>
</template>
Performance & experience:
- ISR cache hit → TTFB approaches static-site speed
- User-state fragments appear after client hydrate (slight flash, but only in a small region — doesn't fight the first paint)
- The body uses a public API (does not vary by user) → framework additionally strips identity on the SSR path of cacheable pages (runtime guard L5) → cache-safe
Can/Can't Checklist per Strategy
ISR / SWR / prerender pages
✅ Can:
useFetchpublic API (auth not required)- Render HTML that's identical for all users
- SEO meta uses defaults (site name, default og:image)
- Wrap user-state fragments in
<ClientOnly>
❌ Can't:
- Call private API (token-authenticated)
- Synchronously access
userStore.token / isLoggedIn / nicknamein<script setup>(→ Pinia state baked into payload, broadcast to all users) useRequestHeaders(['cookie' | 'authorization'])(cookie is stripped by Nitro; using these is a leak risk pattern)setCookie/setResponseHeader(response headers don't enter ISR cache — effectively no-op)- Any rendering that "varies by user identity"
- Reading
route.queryon the SSR path 🔴 — the HTML cache is keyed path-only at the reverse proxy (all query strings ignored), so reading query lets?order=hothit the entry built for?order=new— content mismatch. For the three fixes (move the parameter into a path segment / read query inonMountedand backfill / make the pagessr: false) and the boundary requirements for path segments, see Rendering Strategies §4.2⚠️
computedcounts as the SSR path too:const order = computed(() => route.query.order)is evaluated during SSR as soon as SSR data fetching consumes it — do not assume "it's lazy, so it doesn't count". Theswift-web/no-query-in-cacheable-ssrrule flags it immediately; reading query insideonMountedor an event handler is perfectly fine.
SSR (ssr: true default)
✅ Can:
- Everything (token-authenticated paid content, read cookie for login state, personalized first paint)
useFetchprivate API (cookie passes through naturally → useCookie reads token → request util injects automatically)
❌ Can't:
- Rely on responses being reused (each request renders independently, no caching)
- Expect the server to auto-call
loginByDevice(the server never auto-logs-in; logged-in users are recognized via cookie; unauthenticated users are intercepted to login page by middleware)
CSR (ssr: false)
✅ Can:
- All user state, private data interactions
- WebSocket / long-polling / real-time data
- Complex forms / interaction-heavy pages
❌ Can't:
- SEO-critical content (search engines see empty shell)
- First paint that must appear during server phase
"Where Does This Code Go?" Decision Tree
Should this code run during SSR?
├─ Yes → Write in <script setup> sync path
│ But never: touch userStore mutations (login / setInfo / getDeviceId / setAccount)
│ touch useRequestHeaders('cookie' | 'authorization')
│ touch setCookie / setResponseHeader
└─ No → Write in onMounted / watch / event handler
Here you can: access userStore freely; call private APIs; write cookies; any user-state ops
Anti-Pattern Cheatsheet (5 most common mistakes)
❌ Anti-Pattern 1: ISR page reading userStore in <script setup> sync path
<script setup>
defineRouteRules({ isr: 3600 })
const userStore = useUserStore() // ❌ Pinia state baked into payload, broadcast to all users
const nickname = userStore.info?.nickname // ❌ Cross-user leak
</script>
✅ Correct: Move userStore access into onMounted; wrap user-state UI in <ClientOnly>.
❌ Anti-Pattern 2: Paid content using ISR
<script setup>
defineRouteRules({ isr: 3600 }) // ❌ Lint will fail
const { data: chapter } = await useFetch('/api/novel/chapter', {
query: { id, withFavorite: true } // ❌ Paid content baked into cache = free leak
})
</script>
✅ Correct: Paid content must use defineRouteRules({ ssr: true }) or ssr: false.
❌ Anti-Pattern 3: Setting response cookie on ISR page
<script setup>
defineRouteRules({ isr: 3600 })
const event = useRequestEvent()
setCookie(event!, 'visited', '1', { maxAge: 3600 }) // ❌ baked into the cached copy and replayed to every visitor (framework strips it + warns in dev)
</script>
✅ Correct: Move such side-effects (tracking cookie / personalization cookie) to client (onMounted).
❌ Anti-Pattern 4: ISR page calling token-bound private API
<script setup>
defineRouteRules({ isr: 3600 })
const { data } = await useFetch('/api/user/orders') // ❌ User orders baked into ISR cache
</script>
✅ Correct:
- Public data → SSR/ISR
- Private data →
onMounted+$fetch(auth.client has established token) - Detail pages: use public/private split (Hybrid 1)
❌ Anti-Pattern 5: Forgetting defineRouteRules
<script setup>
// ❌ Missing defineRouteRules → falls back to Nuxt default SSR mode → performance waste + lint will fail
</script>
✅ Correct: Every page must explicitly declare defineRouteRules({ ... }). Lint enforces this.
Relationship to the Theory Guide
| Guide | When to read |
|---|---|
| This guide (Quick Guide) | Before writing a new page; when unsure which strategy to pick; for review checklist self-check |
| Nuxt Rendering Strategy Guide (Theory) | Deep dive into ISR risk mechanics; edge cases requiring root-cause understanding; architectural decisions |
Common lint Rules Cheatsheet
| Path pattern | Allowed routeRules | Forbidden |
|---|---|---|
/read/**, /play/**, /chapter/**, /video/**, /watch/**, /audio/**, /episode/**, /player/**, /viewer/** (paid content body, 9 default segments) | ssr: true / ssr: false | isr / swr / prerender |
/mine/**, /orders/**, /settings/**, /chat/**, /live/**, /profile/**, /account/**, /wallet/**, /vip/**, /subscription(s)/**, /dashboard/**, /message(s)/** (user-state / real-time, 14 default segments) | ssr: false | everything else |
| Any page | Must declare defineRouteRules({ ... }) explicitly | omission |
Lint comes from @swift-web/framework/eslint (swift-web/no-isr-on-paid-paths / swift-web/no-isr-on-user-state-paths). The starter eslint.config.ts already extends it — no manual configuration needed. The segments above are the defaults; appliers can override them via the rules' segments options.
Review Self-Check Checklist (run after writing a page)
- Did the page declare
defineRouteRules? Does the strategy match the decision matrix above? - If ISR / SWR / prerender: Is the HTML output identical for all users?
- If ISR / SWR / prerender: Do all
useFetch/requestcalls hit public APIs? - If ISR / SWR / prerender: Does
<script setup>sync path not touchuserStore/useRequestHeaders('cookie')/setCookie? - User-state fragments wrapped in
onMounted+<ClientOnly>? - Paid content body (chapter / video stream) path satisfies lint rules (SSR or CSR, no ISR)?
- At least one multi-user E2E test covers this page?
Still have questions?
- Deep theory + ISR three risk classes + Public/Private API separation: Nuxt Rendering Strategy Guide
- framework v3 design background: framework#44 + #note_2847
- Business profile / framework positioning slogan: See Development Standards §Rendering Strategy