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)

Dynamic routes

Dynamic routes match a shape of URL and expose the variable parts as params. They are how you build /blog/:slug, /users/:id, or a docs catch-all like /docs/*path.

Single dynamic segment , [param]

pages/blog/[slug].tsx   ->   /blog/:slug
1
2
3
4
5
6
7
8
9
// pages/blog/[slug].tsx
import { useParams } from "ekko:rune/router";
 
export function ssr() { return { title: "Post" }; }
 
export default function Post() {
const { slug } = useParams(); // { slug: "hello-world" } for /blog/hello-world
return <article><h1>{slug}</h1></article>;
}

A [param] matches exactly one path segment. /blog/hello-world matches; /blog/2024/hello does not (use a catch-all for that).

Nested routes under a [param] , /items/:id and /items/:id/edit

Because :id matches exactly one segment, /items/:id does not match /items/123/edit. They are two separate routes , each is its own page file (and gets its own client chunk). This is the way to build a detail page plus an edit page:

pages/items/[id].tsx        ->   /items/:id        (detail)
pages/items/[id]/edit.tsx   ->   /items/:id/edit   (edit)

The flat file [id].tsx and the directory [id]/ coexist in the same folder. Both pages read the same param:

1
2
3
4
5
6
7
// pages/items/[id]/edit.tsx
import { useParams } from "ekko:rune/router";
 
export default function EditItem() {
const { id } = useParams(); // { id: "123" } for /items/123/edit
return <EditForm id={id} />;
}

Do not try to serve /items/:id/edit from the /items/:id page by inspecting the path , :id will never swallow the trailing edit segment, so the match simply fails and you get a 404. Add the edit.tsx file and let the router match it.

Always ekko build --client after adding a page. A page is wired to its route through the build manifest. If a route has no built chunk it logs a [rune] route … has NO client chunk … warning at startup and renders blank (or full-reloads on navigation) , a rebuild fixes it. A path with no matching page file at all is a normal 404.

Catch-all , [...param]

pages/docs/[...path].tsx   ->   /docs/*path

A catch-all matches the rest of the path, one or more segments, and exposes them as a string (or array, depending on how you read it). The docs site you are reading uses exactly this shape: every /docs/... URL is served by one page module that picks the document from the current path.

1
2
3
4
5
6
7
import { useRouter } from "ekko:rune/router";
 
export default function DocPage() {
const { path } = useRouter(); // "/docs/guides/styling"
const doc = lookupDoc(path); // your own resolution
return <Doc {...doc} />;
}

Reading params, query, and path

The router gives you everything about the current URL:

1
2
3
4
5
6
7
8
9
import { useRouter, useParams, useSearchParams } from "ekko:rune/router";
 
const router = useRouter();
router.path // "/blog/hello?ref=hn" -> pathname "/blog/hello"
router.params // { slug: "hello" }
router.query // { ref: "hn" }
 
const params = useParams(); // same as router.params
const search = useSearchParams(); // same as router.query

See The router for the full hook surface.

Dynamic routes are rendered as a shell, then hydrated

Static routes with an ssr() are fully server-rendered and cached. Dynamic routes (:param, *catch) are not pre-cached, their content depends on the URL, so the server returns a shell (the layout + hydration data with props.params, props.query, props.path) and the client renders the page using those props. You can still export an ssr() to set the document title and head for the shell.

Practically:

  • Title/SEO for a dynamic page: return it from ssr() (it runs per request for the shell).
  • The body of a dynamic page renders on the client from params/query, or you fetch data after

hydration (see Data fetching).

If you have a fixed, known set of dynamic values (e.g. a finite list of blog slugs), you can register one static route per value and get full cached SSR for each, this is exactly how the docs generate a route per page. See Programmatic routes.

Specificity recap

Given these files:

pages/blog/new.tsx        ->  /blog/new        (static, priority 0)
pages/blog/[slug].tsx     ->  /blog/:slug      (dynamic, priority 1)
pages/blog/[...rest].tsx  ->  /blog/*rest      (catch-all, priority 2)

/blog/new hits the static route; /blog/hello hits :slug; /blog/2024/03/post hits *rest. rune sorts by priority so this "just works".

Next: the client router API, The router.