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 TypeRendering StrategySuggested TTLKey Notes
Home / Category / List / Ranking / Marketing aggregationISR (primary)Home 600 · category / list / ranking 1800Public 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)ISR3600Pure public data via ISR, best performance
Detail page (public + small user state, most common)Hybrid 1: ISR shell + CSR user-state hydrate3600See decision tree + code example below
Detail page (public + heavy user state, personalized recommendations dominate)Hybrid 2: SSR + public/private API separationNo cacheSee Theory Guide §10 Public/Private API Separation
Reader / Player / Chapter content / Video stream (paid)SSR + token auth ⚠️No cacheISR / SWR / prerender forbidden (enforced by lint); prevents DOM tampering paywall bypass
Static pages (about / terms / EULA)ISR3600Header / 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 / WalletCSR (ssr: false)No cacheAll user state, no SEO value
Chat / Tip / Live / Real-time interactionCSR (ssr: false)No cacheWebSocket / real-time data
Marketing landing page (with ?channel_code=)SSR (preserve channel) or ISR-with-variesNo cache / 600channel_code initCode stays server-side
Login / RegisterCSR (ssr: false)No cachePrivate / user-state pages; ISR / prerender forbidden (hard constraint, same tier as profile pages)

isr only accepts an integer between 60 and 3600 seconds (isr: true was 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. Use NUXT_ISR_TTL to 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:

  • useFetch public 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 / nickname in <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.query on the SSR path 🔴 — the HTML cache is keyed path-only at the reverse proxy (all query strings ignored), so reading query lets ?order=hot hit the entry built for ?order=new — content mismatch. For the three fixes (move the parameter into a path segment / read query in onMounted and backfill / make the page ssr: false) and the boundary requirements for path segments, see Rendering Strategies §4.2

    ⚠️ computed counts 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". The swift-web/no-query-in-cacheable-ssr rule flags it immediately; reading query inside onMounted or an event handler is perfectly fine.

SSR (ssr: true default)

✅ Can:

  • Everything (token-authenticated paid content, read cookie for login state, personalized first paint)
  • useFetch private 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.

<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

GuideWhen 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 patternAllowed routeRulesForbidden
/read/**, /play/**, /chapter/**, /video/**, /watch/**, /audio/**, /episode/**, /player/**, /viewer/** (paid content body, 9 default segments)ssr: true / ssr: falseisr / swr / prerender
/mine/**, /orders/**, /settings/**, /chat/**, /live/**, /profile/**, /account/**, /wallet/**, /vip/**, /subscription(s)/**, /dashboard/**, /message(s)/** (user-state / real-time, 14 default segments)ssr: falseeverything else
Any pageMust declare defineRouteRules({ ... }) explicitlyomission

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 / request calls hit public APIs?
  • If ISR / SWR / prerender: Does <script setup> sync path not touch userStore / 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?