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)

Navigation

There is exactly one rule that, if you follow it, makes a rune app feel instant and keeps your state intact: use the router for in-app navigation. This page is that rule, with the why.

The Link component renders an <a> but intercepts the click to navigate on the client:

1
2
3
4
5
6
7
8
9
10
11
import { Link } from "ekko:rune/router";
 
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/docs">Docs</Link>
<Link href="/blog/hello">A post</Link>
</nav>
);
}

A Link still produces a real <a href>, so it is crawlable, middle-clickable (opens a new tab), and works without JavaScript (it falls back to a normal navigation). When JS is present, the click is handled by the client router: no reload, no flash, atoms preserved.

useRouter().navigate / navigate

For navigation triggered by logic rather than a click:

1
2
3
4
5
6
7
import { useRouter } from "ekko:rune/router";
 
const { navigate } = useRouter();
navigate("/dashboard");
// or the standalone:
import { navigate } from "ekko:rune/router";
navigate("/login");

The cardinal rule

Never use window.location.href = "..." or a plain <a href> for in-app links.

A plain <a> (without the router) or window.location triggers a full page reload:

  • the document re-downloads and re-parses,
  • the page re-renders from the server,
  • and all client state is destroyed, every Mimir atom that was not persisted resets to its default, the

React tree is rebuilt, and the user sees a flash.

This is the single most common cause of "my state disappears when I click a link". The fix is always: use Link or navigate.

1
2
3
<a href="/docs">Docs</a> // ✗ full reload, loses state
<Link href="/docs">Docs</Link> // ✓ client navigation
<a href="https://github.com">GitHub</a> // ✓ fine: this is genuinely external

External links (different origin) should be plain <a>, the router only handles in-app routes.

Back, forward, and history

useRouter() gives you back() and forward(), which call the History API. The router also honours the browser's own back/forward buttons: it re-matches the route and renders, so navigating back is as instant as navigating forward, and atoms survive both.

1
2
const { back } = useRouter();
<button onClick={back}> Back</button>

Side-effects on navigation

Because the page component swaps but the layout persists, run per-navigation effects keyed on the path:

1
2
3
4
5
const { path } = useRouter();
useEffect(() => {
window.scrollTo(0, 0); // reset scroll on navigation
analytics.pageview(path);
}, [path]);

Prefetching

The page chunks are listed in __routes and modulepreload-ed for the current page; the first time you navigate to a route its chunk is fetched and then cached, subsequent visits are instant. For most apps the preloads plus HTTP caching make navigation feel immediate without manual prefetch logic.

Put state in atoms and navigate with the router, and you get behaviour that is otherwise fiddly to build: a theme toggle, an open sidebar, a multi-step form draft, or a scroll position survive moving between pages, because the pages change but the atoms (and the layout holding them) do not. That is the whole reason the rule exists. See Mimir.

Next: redirects and protecting routes, Guards & redirects.