Frontend
Rendering: CSR, SSR, SSG
CSR, SSR and SSG are not three ways to write your app. They are three ways to resolve the same pages. You pick one in configuration (and, for SSR, by which adapters you stack); the pages you wrote stay identical.
#Choosing a strategy
export default defineBuilderConfig({
rendering: 'ssg' // 'csr' | 'ssr' | 'ssg'
})| Strategy | Type | Description |
|---|---|---|
| csr | client | A pure SPA, rendered in the browser. Ship the Browser adapter. |
| ssr | server + client | Rendered per request on a server/edge adapter, then hydrated in the browser. |
| ssg | build + client | Pre-rendered to static HTML at build (list the routes), then hydrated on load. |
#SSR is a superposition
There is no "SSR mode" to write for. Stack a server (or edge) adapter with the Browser adapter: the server context resolves the first paint, the browser context resolves everything after. The pages never know which one ran them.
import { StoneApp } from '@stone-js/core'
import { Routing } from '@stone-js/router'
import { UseReact } from '@stone-js/use-react'
import { Browser } from '@stone-js/browser-adapter'
import { NodeHttp } from '@stone-js/node-http-adapter'
@Browser() // resolves the interactive app in the browser
@NodeHttp() // ...and the first paint on the server
@UseReact()
@Routing()
@StoneApp({ name: 'tasks-ui' })
export class Application {}import { defineStoneApp } from '@stone-js/core'
import { routerBlueprint } from '@stone-js/router'
import { useReactBlueprint } from '@stone-js/use-react'
import { browserAdapterBlueprint } from '@stone-js/browser-adapter'
import { nodeHttpAdapterBlueprint } from '@stone-js/node-http-adapter'
export const App = defineStoneApp(
{ name: 'tasks-ui' },
[routerBlueprint, useReactBlueprint, browserAdapterBlueprint, nodeHttpAdapterBlueprint]
)#SSG routes
You do not list your routes. Static generation pre-renders the pages your app already declares: the same scanned definitions that drive routing become the pre-render set. Everything hydrates on load, so the page is interactive after the first paint.
Two settings cover what a declaration alone cannot say. ssg.params gives a dynamic segment its values, so parameterised paths expand instead of being skipped; an optional segment also yields the path without it. ssg.routes stays additive, for paths no declaration can produce.
export default defineBuilderConfig({
rendering: 'ssg',
ssg: {
// /:lang?/about -> /about, /en/about, /fr/about
params: { lang: ['en', 'fr'] },
// Anything data-driven, added on top of the derived set.
routes: ['/blog/hello-world']
}
})#SOR: server-only, zero client JS
There is a fourth option the superposition model gives you almost for free. Render on a server or edge adapter but do not stack the Browser adapter: the server produces the HTML and there is nothing to hydrate, so no client bundle ships at all. This is Server-Only Rendering (SOR), the zero-JavaScript mode for content that does not need interactivity, a marketing page, a document, an email view.
| Mode | Type | Description |
|---|---|---|
| SSR (hydrated) | server + Browser | Server render, then the browser hydrates: interactive. |
| SOR (server-only) | server, no Browser | Server render, no client bundle, no hydration: zero JS. |
#Streaming SSR
For large pages, the server can stream HTML as it renders instead of buffering the whole document, so the browser starts painting sooner. Streaming is a render option on the server adapter; the pages you wrote do not change.
#Hydration and the snapshot
On SSR and SSG, the server render serialises the page's data into the HTML as a snapshot; the browser hydrates from it instead of re-fetching, so the first interaction is instant and the loader does not run twice. This is automatic; you write pages, not hydration code.