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)

Tutorial , 5. SSR and data

Our list page already server-renders (it is a static route with ssr()). In this step we make the render correct on the first paint: seed the theme so there is no flash, and fix the no-FOUC class. We also make the note list a fully cached SSR page.

The theme flash, and why

When the page first paints, the server rendered with the theme atom's default. If the user previously chose "light", the client will switch to light after hydration, a brief flash of dark. Two fixes work together:

  1. Seed the atom in ssr() so the React tree is right.
  2. Set the .dark class before paint with a tiny inline script reading the same persisted preference.

Seed the theme atom

1
2
3
4
5
6
7
// pages/index.tsx
export function ssr() {
return {
title: "Notes",
__atoms: { "site-theme": "dark" }, // a sensible default the server knows
};
}

For a per-user choice you would read it from a cookie/header here. But our list is a cached static page (shared across users), so we only seed a neutral default and let the client's persisted session take over after hydration, exactly the caveat from Mimir → SSR & hydration.

The no-FOUC script

Add an inline script to the document head that sets .dark from the persisted theme before the browser paints. The client persisted the theme under the Mimir IndexedDB store, but for the very first paint we want a synchronous read, the simplest robust approach is to also mirror the choice to localStorage in the bridge and read it here:

1
2
// in ThemeBridge's effect, after computing the new value:
localStorage.setItem("notes-theme", mimir.get(themeAtom)); // mirror for the no-FOUC script
1
2
3
// server.tsx , build the head with a no-FOUC script
const noFouc = `<script>try{if(localStorage.getItem('notes-theme')!=='light')document.documentElement.classList.add('dark');}catch(e){}</script>`;
const head = `<style>${compileSass(readText("styles/global.scss"))}</style>${noFouc}`;

Now the first paint matches the user's choice (the class is set before render), the seeded atom matches the React tree, and the persisted session keeps it across reloads. No flash. This is the standard rune theming recipe, see No-FOUC.

The note list is cached SSR

Because / is static and exports ssr(), rune renders it into the cache, eagerly at startup with our ssr: "eager" setting. The first visitor gets a warm, fully-rendered page (good for first paint and SEO).

When we add notes via the API in the next step, that cache would go stale. We will call app.invalidate("/") (or a tag) after a write so the list re-renders. For now, since notes live only in the client atom, the list page renders the seed notes on the server and the client atom takes over after hydration.

Per-request data on the detail page

The detail route /notes/:id is dynamic, so it renders on the client from params. Its ssr() can still set a useful title. If you wanted the title to include the note's name on first load (for shared links), you would either:

  • look the note up in ssr() if your data is available server-side (it is, notesAtom's default is

seedNotes), or

  • register one static route per known note id (so each gets a cached render and a precise title), the

programmatic routes technique.

For a notes app whose ids are user-created at runtime, the dynamic route + client render is the right call; for a fixed content set (docs, products), prefer per-entry static routes.

Verify

1
ekko build --client && ekko run server.tsx --allow=fs,net,env
  • View source on /, the list HTML is there, server-rendered.
  • Set the theme to light, reload, no flash to dark; it stays light from the first paint.
  • The server log shows SSR cache: N pages rendered (eager).

Next: persist notes for real with an API, 6. API routes.