Documentation
Docs
Introduction
Getting Started
Tutorial: build an app
Core Concepts
Routing
Server-Side Rendering
Mimir, state management
Pages & Layouts
API Routes
Styling & Theming
Building & Deploying
API Reference
Guides
Recipes
FAQ (use cases)
Quick start
A five-minute tour: a page, a layout, some state, and client navigation. The Tutorial builds a complete app step by step; this page is the shortest path to "it works".
Scaffold it (the recommended start)
The fastest, correct way to start a rune app is ekko init rune. It writes the right ekko.json
("type": "rune", entry server.tsx, the React imports map) and a working starter, so you never
hand-write the manifest:
The rest of this page shows what those files contain, so you can edit them with confidence or build one by hand.
1. A page
Create pages/index.tsx. A page is a module with a default export (the React component) and an optional
ssr() function that runs on the server.
2. A layout
pages/layout.tsx is the shell wrapped around every page. It receives children.
3. Wire it up in server.tsx
server.tsx is the entry point. It registers the React renderer, builds the app, maps routes to page
modules, and starts listening.
scanRoutes("pages") discovers your files and returns { pattern, pageKey, ... } for each. You map each
pattern to the imported module and call app.page. (Larger apps automate this; the tutorial shows the full
pattern, including docs route generation.)
Why extensionless? Ekko owns module resolution. It sees a bare specifier, detects the source kind, transpiles
.ts/.tsx, caches the converted JS, then resolves it, uniformly, decoupled from any bundler. Writing the extension would tie your code to one on-disk form and bypass that pipeline, soekko runrejects it. Always import without an extension:./pages/layout, not./pages/layout.tsx.
4. Run it
Visit http://localhost:3000. View source, you will see real HTML for the page, not an empty <div id="root">.
5. Add client-side navigation
Use the router's <Link> (or useRouter().navigate) for in-app links so navigation stays on the client:
Never use
window.location.hrefor a plain<a href>for in-app navigation, that triggers a full reload and discards client state. External links are fine as plain<a>. See Navigation.
6. Add some state
Define an atom once, use it anywhere. It persists across navigation.
Navigate away and back, the count is still there. That is the point of Mimir. Read the full story in Mimir.
That is the whole loop: pages with ssr(), a layout, server.tsx to wire them, the router for navigation,
and atoms for state. Next, understand the folders: Project structure.