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)

Programmatic routes

app.page(path, component, meta) is just a function call, so you can register routes in a loop. This is how you get fully server-rendered, individually-cached pages for a known set of data, blog posts, docs pages, product pages, without writing one file per item.

The pattern

Given data with a stable list of entries, register one route per entry, all sharing a single page component:

1
2
3
4
5
6
7
8
9
import DocPage from "./pages/docs";
import { docsData } from "./content/docs/docs.data";
 
for (const entry of docsData.nav.flat) {
const routePath = "/docs" + (entry.slug ? "/" + entry.slug : "");
const Comp: any = function DocRoute() { return <DocPage />; };
Comp.ssr = function () { return { title: `${entry.title}, My Docs` }; };
app.page(routePath, Comp, { title: entry.title, page: "docs.tsx", head });
}

This is the exact technique the docs site you are reading uses. Each /docs/... URL is a static route (no :param), so each gets a full cached SSR render on first hit (or eagerly at startup), great for first paint and SEO, while the shared DocPage component, selected by useRouter().path, makes navigation between docs instant on the client.

Why not one catch-all [...path]?

A catch-all (/docs/*path) would serve every doc with one shell route. That works, but the page renders on the client (the server returns a shell), so you lose the full server render and the SSR cache for each document. Registering one static route per known slug gives you:

  • a cached, fully-rendered HTML page per URL (better first paint, better SEO),
  • correct per-page <title> and head tags from each route's ssr(),
  • the same instant client navigation afterwards (the shared component reads the path).

Use a catch-all when the set is unbounded or unknown (arbitrary user paths); use a per-entry loop when the set is known at build/startup (a docs index, a CMS export, a product list loaded at boot).

Sharing one component, selecting by path

The shared page reads the current path and renders the matching content from the in-memory data:

1
2
3
4
5
6
7
8
9
10
// pages/docs.tsx
import { useRouter } from "ekko:rune/router";
import { docsData } from "../content/docs/docs.data";
 
export default function DocPage() {
const { path } = useRouter();
const slug = path.replace(/^\/docs\/?/, ""); // "" for /docs
const doc = docsData.docs[slug] || docsData.docs[""];
return <DocShell doc={doc} nav={docsData.nav} />;
}

Because docsData is embedded in the bundle (imported at module load), every navigation is a local lookup, no network request to fetch the next doc.

Generating the page key

All looped routes share one client chunk (page: "docs.tsx"), since they render the same component. If you generate routes for different components, give each its own page key matching its manifest entry.

When data changes at runtime

If your route set can change while the server runs (a new post is published), you have two options:

  1. Re-register on a restart , simplest; the route set is fixed per process.
  2. Use a catch-all for the dynamic tail and resolve content per request, accepting the shell render.

For content that changes rarely (docs, marketing, a product catalogue refreshed on deploy), the startup loop is the sweet spot: static, cached, SEO-perfect routes with zero per-file boilerplate.

That completes Routing. Next: the SSR model in depth, SSR overview.