Documentation
Docs
Introduction
Getting Started
Tutorial: build an app
Core Concepts
Routing
Server-Side Rendering
Mimir, state management
Pages & Layouts
API Routes
Styling & Theming
Building & Deploying
API Reference
Guides
Recipes
FAQ (use cases)
Server-Side Rendering
Server-Side Rendering is rune's default and its foundation. Every page is rendered to HTML on the server for
the first response, then hydrated into a live app. This chapter covers the why and the how: the ssr()
function, the three rendering strategies, the handoff to the client, the SSR cache, and SEO.
Why SSR
- First paint is content, not a spinner. The browser receives real HTML and shows it immediately, even
before any JavaScript loads.
- SEO and link previews work. Crawlers and unfurlers see the actual page, with the right
<title>, Open
Graph, and meta tags.
- Resilience. The page is meaningful without JS; hydration is an enhancement, not a prerequisite for
visibility.
And because rune hydrates into a single-page app afterward, you do not pay SSR's cost on every click, only the first load is server-rendered; the rest is client navigation.
The one rule: your page runs on BOTH the server and the browser
This is the single most important idea in rune, and the one newcomers miss most. A page file is used twice:
- On the server, to produce the first HTML (and to run
ssr()). - In the browser, to hydrate that HTML, the same component code runs again, attaches to the live DOM,
and takes over navigation.
So everything in a page module is bundled for the browser too, except what you explicitly mark server-only. Two consequences you must respect:
1. ssr() is server-only , and so is anything it imports. If ssr() reads the database
(ekko:db / ekko:db/orm), the filesystem (ekko:fs), or any server module, that import would be bundled
into the browser, where it cannot load. The page would server-render fine and then silently fail to
hydrate (the browser rejects the chunk: a CORS / "module script is text/html" error) , it looks rendered
but is dead: no clicks, no toggles, no forms. Wrap ssr() and its server-only imports in
/* START SSR */ ... /* END SSR */; the client build strips that block. The build now errors if a
server-only ekko:* module reaches the client, so you cannot ship this mistake.
2. Data crosses the boundary through Mimir, not function calls. The server cannot hand a value to the
browser directly. ssr() returns __atoms (a map of atom key → value); rune serializes it into the page,
and the browser hydrates those atoms before the first render. The client reads the atom , it never calls
the server function. For data that depends on per-request input or changes after load, fetch it from an
API route after hydration instead.
That is the whole model: server renders + seeds the atoms → browser hydrates + reads them → the client takes over. Get this boundary right and the rest of this chapter is detail. See Mimir → SSR & hydration for the state side, and Concepts → server vs client for the build rule.
What rune renders, and when
| Route kind | First response | Cached? |
|---|---|---|
Static route with ssr() | full server render of the page | yes, keyed by path |
Static route without ssr() | a shell (layout + hydration data); the client renders the page | no |
Dynamic route (:param, *catch) | a shell with props.params/query/path; client renders | no |
So the trigger for a full, cached server render is: a static path plus an exported ssr(). Add
ssr() to a page you want fully rendered and indexed; omit it (or use a dynamic route) when the body depends
on the client/URL.
The pieces
ssr(), a server-only function on the page module. It returns thetitle, optionalheadtags, and
optional __atoms to seed state. See The ssr() function.
- Strategies ,
eager,background, orlazycontrol when the cache is populated. See
- Hydration , the client attaches to the server markup and picks up the seeded state. See
Hydration (and the deep dive in Concepts → Hydration).
- Caching & invalidation , rendered HTML is cached with optional
ttlandtags;app.invalidate
busts it. See Caching & invalidation.
- SEO ,
createSEO()produces the head tags each render injects. See SEO.
The render, step by step (server)
For an SSR route, the server:
- runs
ssr()(title, head, seed atoms), - seeds Mimir with
__atoms, renderToString(layout(page)), React's renderer, the same tree the client will hydrate,- resolves the page's client assets from the manifest,
- assembles the document with
htmlShell(...)(styles, scripts,modulepreload,__EKKO_DATA__), - stores it in the cache and returns it.
This is the same flow drawn in The rendering pipeline; this chapter zooms into each control you have over it.
A page with SSR
Next: everything ssr() can return, The ssr() function.