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)

Error and not-found

Two convention pages handle the unhappy paths: not-found for unmatched URLs (404) and error for render failures (500). Both are plain components you provide to createApp.

The not-found page (404)

pages/not-found.tsx renders when no route matches. Wire it via createApp({ notFound }):

1
2
3
4
5
6
7
8
9
10
11
// pages/not-found.tsx
import { Link } from "ekko:rune/router";
export default function NotFound() {
return (
<main className="container">
<h1>404</h1>
<p>That page does not exist.</p>
<Link href="/">Go home</Link>
</main>
);
}
1
2
import NotFound from "./pages/not-found";
const app = createApp({ /* ... */ notFound: NotFound });

rune registers a catch-all (*) that renders NotFound with a 404 status and the request path as a prop, so you can show the attempted URL:

1
2
3
export default function NotFound({ path }: { path?: string }) {
return <main><h1>404</h1><p>No page at <code>{path}</code>.</p></main>;
}

The 404 is wrapped in an HTML shell (with your lang), so it is a complete page, not a bare fragment.

The error page (500)

pages/error.tsx renders when a page's server render throws. Wire it via createApp({ error }):

1
2
3
4
5
6
7
8
9
// pages/error.tsx
export default function ErrorPage() {
return (
<main className="container">
<h1>Something went wrong</h1>
<p>We hit an error rendering this page. Please try again.</p>
</main>
);
}
1
2
import ErrorPage from "./pages/error";
const app = createApp({ /* ... */ error: ErrorPage });

If an SSR render throws, rune logs the error server-side and returns a safe error response rather than leaking a stack trace to the client. Keep the error page generic and reassuring; do not render the actual error message to users.

Per-section error boundaries

The layout tree can carry an error handler per segment (layoutTree[seg].error), so a section can have its own error UI. rune looks for the nearest error handler up the path from the failing route. For most apps the single createApp({ error }) is enough; use per-segment handlers when a subtree (say /admin) should fail differently from the rest of the site.

404 vs a route's own "not found" branch

There are two distinct "not found"s:

  1. No route matches the URL , rune renders your notFound page (a real 404). Example: /totally/unknown.
  2. A route matches, but its data is missing , the page renders its own "not found" content (still a 200,

because the route exists). Example: /notes/:id matched, but there is no note with that id.

Use the notFound page for unknown URLs; handle missing-data-within-a-known-route inside the page:

1
2
3
4
5
6
export default function NotePage() {
const { id } = useParams();
const note = findNote(id);
if (!note) return <p>No note with id {id}.</p>; // route exists; data does not
return <Note note={note} />;
}

If you want a missing-data case to be a true 404 (for SEO/correctness), register a server redirect or render through a dynamic route that can set a 404 status, but for most UX, an in-page message is the right call.

Status codes

  • Unmatched URL → 404 via notFound.
  • SSR render throw → 500 via error (logged server-side).
  • Matched route, missing data → 200 with in-page messaging (unless you deliberately 404 it).

That completes Pages & Layouts. Next: the server surface, API routes.