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)

The rendering pipeline

This is the single most important diagram in rune. Once it clicks, the rest of the framework is detail.

rune rendering pipeline

Stage 1 , the server renders (first request only)

When a request arrives for an SSR-enabled route, the server (_ssrRender in ekko:rune) does, in order:

  1. Match the route to its page module.
  2. Run ssr() if the page exports one. Its return value provides the title, optional head tags, and

optional __atoms to seed Mimir before rendering. The seeded values are written into the store so the server render and the client hydration agree.

  1. renderToString(...) the React tree. Because you imported @ekko/react-dom/server, this is React's

real renderToString (hooks, context, the works). The page element is created and wrapped in the root layout, exactly the same element tree the client will hydrate, so the markup matches.

  1. Resolve assets from the manifest: the page's client chunk, its imports (preloaded), its CSS, plus the

shared hydration entry and layout chunk.

  1. htmlShell(...) assembles the full document: <head> with styles and SEO tags, the rendered body

inside <div id="__ekko">, <link rel="modulepreload"> for the chunks, the hydration <script type="module">, and a <script id="__EKKO_DATA__" type="application/json"> carrying the hydration payload.

  1. Cache the HTML keyed by path (with the route's ttl and tags) and return it.

The result is a complete HTML page. View source on any rune page, you see real content, not an empty root.

Stage 2 , the browser hydrates

The browser paints the HTML immediately (Stage 1's output needs no JavaScript to be visible). Then:

  1. It loads the hydration bundle named in the manifest (manifest.hydrate).
  2. The bundle reads __EKKO_DATA__: the current page chunk URL, the __routes table, the __layout

chunk, the seeded __atoms, and any props.

  1. It rehydrates Mimir with __atoms (so useAtom reads the server's values, no flicker, no mismatch).
  2. It imports the layout chunk and the page chunk, builds layout(page), and hydrates that React tree

onto the existing DOM, attaching event handlers without re-creating the markup.

Now the page is interactive. Details in Hydration.

From here on, the app is a single-page application:

  1. The user clicks an in-app <Link href="/x"> (or calls navigate("/x")).
  2. The client router matches /x against the __routes table baked into the page.
  3. It imports that route's chunk (cached after the first time) and renders it inside the same layout,

in place. The URL updates via the History API.

  1. No server round-trip. Mimir atoms are untouched, so any state (theme, scroll position you stored, a

form draft) survives the navigation.

This is why rune feels instant after the first load: the only thing crossing the network on navigation is a JavaScript chunk you usually already have.

Why the markup matches (and why that matters)

A subtle but crucial property: the server composes the React element tree as rootLayout(page) and so does the client. Because the same tree is rendered on both sides, hydration is a clean attach, not a re-render, and you never see the "hydration mismatch" warnings that plague hand-rolled SSR. The ssr()-seeded atoms are the mechanism that keeps data in sync to match the markup.

What does not get SSR'd

Dynamic routes (:param, *catch-all) are not pre-rendered into the cache, they serve a shell (the layout with hydration data) and let the client render the page, because their content depends on the URL. You can still give them an ssr() for the title and head. Static routes with an ssr() are the ones that get the full cached render. See Strategies.

Next: the handoff in detail, Hydration.