Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

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:

  1. On the server, to produce the first HTML (and to run ssr()).
  2. 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { useAtomValue } from "ekko:rune/mimir";
import { issuesAtom } from "../atoms/issues"; // atom({ key: "app:issues", default: [] })
 
/* START SSR */
import { getIssues } from "../lib/db"; // server-only — stripped from the browser bundle
export function ssr() {
// runs on the SERVER, seeds the atom the client will read (key must match the atom's key)
return { title: "Issues", __atoms: { "app:issues": getIssues() } };
}
/* END SSR */
 
export default function Board() {
const issues = useAtomValue(issuesAtom); // the BROWSER reads what ssr() seeded , no DB here
return <IssueList issues={issues} />;
}

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 kindFirst responseCached?
Static route with ssr()full server render of the pageyes, keyed by path
Static route without ssr()a shell (layout + hydration data); the client renders the pageno
Dynamic route (:param, *catch)a shell with props.params/query/path; client rendersno

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

  1. ssr() , a server-only function on the page module. It returns the title, optional head tags, and

optional __atoms to seed state. See The ssr() function.

  1. Strategies , eager, background, or lazy control when the cache is populated. See

Strategies.

  1. Hydration , the client attaches to the server markup and picks up the seeded state. See

Hydration (and the deep dive in Concepts → Hydration).

  1. Caching & invalidation , rendered HTML is cached with optional ttl and tags; app.invalidate

busts it. See Caching & invalidation.

  1. SEO , createSEO() produces the head tags each render injects. See SEO.

The render, step by step (server)

For an SSR route, the server:

  1. runs ssr() (title, head, seed atoms),
  2. seeds Mimir with __atoms,
  3. renderToString(layout(page)) , React's renderer, the same tree the client will hydrate,
  4. resolves the page's client assets from the manifest,
  5. assembles the document with htmlShell(...) (styles, scripts, modulepreload, __EKKO_DATA__),
  6. 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pages/index.tsx
export function ssr() {
return {
title: "Home, My App",
// optional: seed state the page will read on first render
__atoms: { "site-theme": "dark" },
// optional: extra <head> tags (SEO, etc.)
// head: seo.headTags({ description: "..." }),
};
}
 
export default function Home() {
return <main><h1>Welcome</h1></main>;
}

Next: everything ssr() can return, The ssr() function.