# Routing

> File-based routing, layouts, template syntax, and client reactivity

Package: Koze
Canonical: https://kuratchi.dev/docs/koze/routing
Markdown: https://kuratchi.dev/docs/koze/routing.md

## Route conventions

Place `.koze` files inside `src/routes/`. File paths become URL patterns.

```text
src/app.koze                       -> document shell (<!DOCTYPE>, <html>, <head>)
src/routes/layout.koze             -> shared fragment wrapping every page
src/routes/index.koze              -> /
src/routes/about/index.koze        -> /about
src/routes/items/index.koze        -> /items
src/routes/blog/[slug]/index.koze  -> /blog/:slug
```

API endpoints use a separate convention: place `.ts` or `.js` modules under
`src/routes/api/`. For example, `src/routes/api/v1/status.ts` maps to
`/api/v1/status`, and `src/routes/api/v1/health/index.ts` maps to
`/api/v1/health`. See [API Routes](/docs/koze/api-routes) for handler exports,
dynamic segments, and middleware boundaries.

Three file roles, one template syntax:

- **`app.koze`** — the document shell. Lives at `src/app.koze` (not inside `routes/`). Optional; the framework provides a default when absent.
- **`layout.koze`** — a fragment that wraps every page. Nest `<segment>/layout.koze` for section-specific chrome.
- **`index.koze`** — the page for a segment. Dynamic segments use `[brackets]`.

All three use the same shape: top `<script>` + template + `<slot></slot>` (where applicable). See [Project structure](/docs/koze/project-structure) for the full reference.

> **Note:**
**File extension:** Route files MUST end in `.koze`. The compiler discovers routes by scanning for this extension — plain `.html` files are ignored (and should not be used for Koze pages). The `.koze` extension signals "this is a Koze template with mixed server / client script regions" so editor tooling, the compiler, and agents can treat it correctly. Use `.html` only for true static HTML served from `src/assets/`.

## Route execution model

Koze pages are **server-rendered by default**, but the authored top
`<script>` model is **client-first**.

Write the top script as the place where page state, helper functions,
reactive `$:` code, and `$server/*` calls live. The framework then:

- runs the server-needed parts during SSR
- hydrates awaited SSR values into the browser copy of the script
- rewrites `$server/*` imports to browser RPC stubs on the client side
- preserves non-awaited `.pending` / `.error` / `.success` async-value behavior

```html
<script>
  import { getItems } from '$server/items';
  const items = await getItems();

  function onDelete(id) {
    // Runs in the browser when a button is clicked.
  }
</script>

<ul>
  for (const item of items) {
    <li>
      {item.title}
      <button onclick={onDelete(item.id)}>Delete</button>
    </li>
  }
</ul>
```

What runs where:

- `await $server/*` functions execute on the server at render time. The
  resolved values are serialized into the HTML response and hydrated
  into the browser copy of the script — no second round-trip for SSR
  data.
- Non-awaited `$server/*` calls return async values exposing
  `.pending`, `.error`, and `.success`. On the server they participate
  in streaming boundaries; in the browser the same shape is preserved by
  RPC stubs.
- Helper functions (`function onDelete(id)`) and synchronous top-level
  code stay in the browser copy of the script. The framework wraps DOM
  listeners automatically via the
  [`on<event>={fn(args)}` directive](/docs/koze/client-interactivity#event-handler-directive).
- Top-level statements whose bodies contain `await` are stripped from
  the browser bundle after hydration wiring is in place — they already
  ran server-side.

Use `src/server/*` for private modules and backend logic. `$server/*`
functions called from the browser become RPC stubs that POST to a
framework-managed endpoint.

For the full hydration model, dispatch rules, and `__kozeReadData`
escape hatch, see [Client interactivity](/docs/koze/client-interactivity).

## Browser-only code

Routes allow one `<script>` block: the top script. Keep DOM-only work
inside functions that run in the browser, and move reusable code to
`$lib/*` modules that are safe to import during SSR.

```html
<script>
  import { getItems } from '$server/items';
  import { mountCopyButtons } from '$lib/copy-buttons';

  const items = await getItems();

  function enableCopyButtons() {
    mountCopyButtons(document.querySelectorAll('[data-copy]'));
  }
</script>

<ul>
  for (const item of items) {
    <li data-copy={item.secret}>{item.title}</li>
  }
</ul>

<button onclick={enableCopyButtons()}>Enable copy buttons</button>
```

If a third-party module touches `window` during import, wrap the import
in a lazy `$lib/*` helper and call that helper from a browser event.

## Layouts

`src/routes/layout.koze` is a **fragment** that wraps every page. Not a document — no `<!DOCTYPE>`, no `<html>`, no `<body>`. Those live in `app.koze`.

```html
<script>
  import { url } from 'koze:request';
  const current = url.pathname;
</script>

<header>
  <a href="/" class={current === '/' ? 'active' : ''}>Home</a>
  <a href="/items" class={current.startsWith('/items') ? 'active' : ''}>Items</a>
</header>

<main>
  <slot></slot>
</main>
```

### Nested layouts

Drop a `layout.koze` at any directory depth. It wraps every
route under that directory, composed with the root layout outside
and the page inside.

```text
src/routes/
├─ layout.koze             ← wraps every page
├─ index.koze              ← /
└─ admin/
   ├─ layout.koze          ← wraps /admin/*
   ├─ users/
   │  └─ index.koze        ← /admin/users (root + admin layouts)
   └─ settings/
      ├─ layout.koze       ← wraps /admin/settings/*
      └─ index.koze        ← /admin/settings (root + admin + settings)
```

Composition at render time goes **innermost first**: the route's HTML
is wrapped by its nearest layout, the result is wrapped by the next
layout up, and so on until the root layout hands its output to the
app shell. That means an author writing `<slot></slot>` in a nested
layout receives the already-wrapped child markup — the inner layout
is closer to the content it's decorating than the outer one.

Each layout is a self-contained fragment: its top `<script>` runs
independently, it has its own SSR data, and it can import its own
`$server/*` modules. Two layouts in the same chain can import
`koze:request` and read the same `searchParams` / `params` /
`pathname`; the framework plumbs request state into every layout and
page uniformly.

Use nested layouts for chrome that's scoped to a section — a
sidebar in `/admin`, a ticket-drawer overlay on `/board/*`, tab
navigation in `/settings` — without duplicating it across every
page underneath.

### App shell

The document shell (doctype, `<html>`, `<head>`, `<body>`) belongs in `src/app.koze` — alongside `worker.ts` and `middleware.ts`, not inside `routes/`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>My App</title>
</head>
<body>
  <slot></slot>
</body>
</html>
```

Koze auto-injects a `<link rel="stylesheet">` for `src/app.css` here — don't author it yourself. See [Styling](/docs/koze/styling).

Optional. Koze synthesizes a minimal default shell when the file is absent.

## Template syntax

### Interpolation

```html
<p>{title}</p>
<p>{@html bodyHtml}</p>
<p>{@raw trustedHtml}</p>
```

### Conditionals

```html
if (items.length === 0) {
  <p>Nothing here yet.</p>
} else {
  <p>{items.length} items</p>
}
```

If the condition reads reactive state, the block updates in the browser
after the initial server render.

### Loops

```html
for (const item of items) {
  <li>{item.title}</li>
}
```

If the iterable reads reactive state, the block updates in the browser
after the initial server render. This is the preferred pattern for
filtered selects, dependent lists, and other derived UI:

```html
<script>
  let selectedLocationId = '';

  $: selectOptions = allCells.filter(
    cell => cell.locationId === selectedLocationId,
  );
</script>

<select bind:value={selectedLocationId}>
  for (const location of locations) {
    <option value={location.id}>{location.name}</option>
  }
</select>

<select>
  for (const cell of selectOptions) {
    <option value={cell.id}>{cell.name}</option>
  }
</select>
```

Template text and attributes that read reactive state are also live:

```html
<p>{selectOptions.length} available cells</p>
<button disabled={selectOptions.length === 0}>Continue</button>
```

Loop locals stay available to client bindings:

```html
for (const item of rows) {
  <select bind:value={forms[item.id].selected}></select>
}
```

### Components

```html
<script>
  import Card from '$lib/card.koze';
</script>

<Card title="Stack">
  <p>Live</p>
</Card>
```

Package components like `kuzan/*` are optional. Local `.koze`
components and package components are first-class.

### Component Props

Components receive props via `koze:component`. Destructure in the
component's `<script>` block:

```html
<!-- src/lib/card.koze -->
<script>
  import { props } from 'koze:component';

  const { title, class: className = '', variant = 'default' } = props<{
    title?: string;
    class?: string;
    variant?: string;
  }>();
</script>

<div class="card {className}" data-variant={variant}>
  if (title) {
    <h2>{title}</h2>
  }
  <slot></slot>
</div>
```

**Props patterns:**
- Import and destructure with defaults: `const { title, size = 'md' } = props<{ title?: string; size?: string }>();`
- Access directly in template: `{props.title}` or `data-variant={props.variant}`
- Use `class:` for className (reserved word): `const { class: className = '' } = props<{ class?: string }>();`
- Children go in `<slot></slot>`

## Client reactivity

Use `$:` inside the top `<script>` when you need reactive updates driven
by client-side state:

```html
<script>
  let users = ['Alice'];

  $: console.log(`Users: ${users.length}`);

  function addUser() {
    users.push('Bob');
  }
</script>
```

Rules:

- The top `<script>` is authored as client-first code, with SSR-only
  work extracted and hydrated back into the browser copy.
- `$server/*` imports are the server/RPC escape hatch. Awaited on the
  server, RPC-stubbed in the browser.
- You do not need to author `type="module"` yourself — the framework
  adds it when the script has ES module imports.
- `$:` is available in the top script and runs in the browser copy of
  that script.
- `$:` assignment targets are defined by the assignment. You can write
  `$: filteredUsers = users.filter(...)` without first declaring
  `let filteredUsers`.
- Top-level `let` bindings that participate in `$:`, `bind:value`, or
  live template expressions become mutable reactive state. Top-level
  `const` bindings stay readonly, but can be read by reactive state.
- Proxy-backed array and object mutation works when `$:` is active.
- You should not need `typeof document !== 'undefined'` guards in the
  top script. Keep DOM-only work inside browser-called functions, not in
  top-level module execution.
