# routes and pages

> How a component becomes a page: it declares a route with `[Page("/catalog/{slug}")]`, and navigating to a matching path serves it. Covers route templates and param capture, server- vs client-rendering, layouts and retained pages, in-app navigation, and the secure-by-default rule that a routed page requires sign-in.

<!-- id: ui-routing · area: ui · stability: stable · html: https://osysharp.com/reference/ui/routing/ -->

## Summary        {#summary}
A component becomes a **page** when it declares a route: `[Page("/catalog/{slug}")]`. When a browser navigates to a
matching path, the platform serves that page, binding the captured route segments to the component's props. Your app
ships no JavaScript of its own — you write components, and the platform runs them.

## Signature      {#signature}
```osy syntax
[Page("/catalog/{slug}")]        // the route template; {slug} captures a segment
component Catalog(slug) { … }    // …and binds to the same-named prop

[Route("/catalog/{slug}")]       // `[Route]` is the same attribute under a second name
component Catalog(slug) { … }
```

`[Page]` and `[Route]` mean exactly the same thing. Both spellings are accepted everywhere a routed component is
declared, and neither is preferred — pick one and stay with it in a codebase.

## Description    {#description}

### Putting a parameter in a URL — `{name}` segments   {#route-templates}
The route is the string in `[Page(...)]`. Segments are matched literally except `{name}` segments, which capture one
path segment and bind to the same-named component prop.

| Template | Path | Result |
|---|---|---|
| `/login` | `/login` | `Login`, no params |
| `/` | `/` | the root page |
| `/catalog/{slug}` | `/catalog/shoes` | `Catalog`, `slug = "shoes"` |
| `/o/{order}/line/{line}` | `/o/A1/line/7` | `order = "A1"`, `line = "7"` |
| `/plants/{id}` | `/plants/3f2a…` | `Edit`, `Guid id` — see below |

**A captured segment is converted to the PROP'S DECLARED TYPE.** The examples above all bind `string` because that
is what those pages declare, but the type is yours to choose — `Guid`, `int`, `long`, `decimal`, `bool` and
`DateTime` all bind, and a value that will not convert is a 404 rather than a page that throws:

```osy syntax
[Page("/plants/{id}")]
component EditPlant(Guid id) { … }        // `id` arrives as a Guid — no parsing, no Guid.Parse
```

⚑ Worth stating because the absence read as an answer: every example on this page binds a string, `line = "7"`
included, and a generated app duly took `string id` and called `Guid.Parse` on it by hand rather than "risk
relying on automatic type conversion from the route" (measured, eval run 263). The conversion is not a risk; it is
the contract.

Matching rules:
- Segment counts must be equal — `/catalog/{slug}` does **not** match `/catalog` or `/catalog/a/b`.
- Static segments match case-insensitively; captured values are URL-unescaped (`red%20shoes` → `red shoes`).
- **The most specific template wins**: a fully static template beats one with params, so `/catalog/new`
  resolves to the literal `NewItem` page, never to `/catalog/{slug}` with `slug = "new"`.
- A trailing slash is ignored (`/notes` and `/notes/` resolve alike).
- No path matches → the browser gets a **404**.

Only routed **components** are candidates — a component without a `[Page]` is never a destination.

### Render mode — SSR vs CSR {#render-mode}
A page declares how it is delivered with `[Render(...)]`. The mode decides whether the first response already contains
the page's content or the browser renders it after loading:

- **`[Render(CSR)]`** (client-side rendering) — the first response is empty of page content; the browser then renders
  the page. Simplest, but the first visible content waits on the browser to render.
- **`[Render(SSR)]`** (server-side rendering) — the page's content is in the first response, so it is visible
  immediately. The page then **hydrates** in the browser: it becomes interactive (state, events, and `live var`
  updates) without re-fetching or re-drawing what was already shown.

Server-side and client-side rendering produce the **same page** from the same definition — a prop, an interpolated
value, or an `if` condition evaluates identically either way, and an `Input` bound to state shows its current value in
the initial content. SSR is purely an optimization for first paint; choosing it never changes what the page does, only
how quickly its content appears.

**Server rendering is for public pages.** Only a page marked `[AllowAnonymous]` is pre-rendered. A hard browser
navigation carries no session, so the server has no identity to render a private page under — and pre-rendering it as
"nobody" would put a protected page's structure into content anyone can request. A page that requires sign-in is
therefore delivered client-side and renders once the browser has a session. It still works exactly the same; it just
isn't in the first response.

**Data on a server-rendered page.** A public page has its server-read data fetched and rendered on the server too, so a
list of records is already in the first response — ideal for pages that must load fast or be indexed.

**Chrome comes with it.** A server-rendered page inside a `[Layout]` arrives with its layout already painted around
it, so the shell is on screen before any JavaScript runs — see [layouts](#layouts) below.

If a page uses something server rendering doesn't support yet, that page falls back to client-side rendering
automatically — it still loads and works, it just isn't pre-rendered. You never get a broken page for choosing
`[Render(SSR)]`.

### Sharing a sidebar or header across pages — `[Layout]`   {#layouts}
Most apps wrap their pages in shared chrome: a sidebar, a header, a tab bar. Rebuilding that on every navigation is
both slow and visibly wrong — a sidebar shouldn't blink, and its scroll position and open sections shouldn't reset.

A **layout** is a component that wraps child pages. It marks itself with `[Layout]` and renders exactly one `Outlet;`
— the place its child page appears. A page opts in by naming the layout it renders inside:

```osy title="a layout, and the pages that render inside it" test app=ui-routing
[Composable] component Sidebar() { render { Text("nav"); } }

[Layout]
component AppShell() {
  render {
    Sidebar();
    Outlet;          // the matched page renders here
  }
}

[Page("/users")] [Layout(AppShell)] [Render(CSR)] component UsersPage() { render { Text("Users"); } }
[Page("/teams")] [Layout(AppShell)] [Render(CSR)] component TeamsPage() { render { Text("Teams"); } }

[Page("/login")] [AllowAnonymous] [Render(CSR)] component LoginPage() { render { Text("Sign in"); } }   // no layout — renders bare
```

Navigating from `/users` to `/teams` **keeps `AppShell` mounted**. Only the outlet's child is replaced, so the shell's
chrome, its state, its queries, and its scroll position all survive. Navigating to `/login` — which names no layout —
tears the shell down; coming back rebuilds it.

`[Layout(AppShell)]` is a **checked identifier**, not a string: naming something that isn't a layout is a compile
error, exactly like naming an undeclared policy in `[Authorize(...)]`. More mistakes are caught at compile time
rather than becoming a blank screen:

- an `Outlet;` in a component that isn't a `[Layout]`;
- a `[Layout]` that renders **no** outlet (its child would have nowhere to go) or **more than one**;
- a component using **itself** as its layout;
- a **public page inside a protected layout**. An anonymous visitor loads a page's chrome as well as its body, so an
  `[AllowAnonymous]` page can only render inside a layout that is itself `[AllowAnonymous]` (or `[Composable]`).
  Otherwise the page could never paint for the visitor it was made public for.

### Does the layout render on the server too?   {#layout-ssr}
When a `[Render(SSR)]` page declares a layout, the server renders the **layout around it** — the shell and the page
arrive together, in one response. The browser paints the whole thing before the app's JavaScript has loaded, and when
the client takes over it **adopts** what was painted rather than rebuilding it. Nothing flashes and nothing is drawn
twice. This is the reason a layout is compile-checked so heavily: the page the server paints and the page the client
builds have to be the same page.

### Keeping more than one page alive    {#retain}
By default an outlet holds **one page at a time**: navigating away disposes the page you left. That's what a shop, a
marketing site, or any ordinary web app wants, and it costs nothing.

Some apps want the opposite. An admin console where you keep several records open; a mobile shell with a back-stack; a
wizard whose steps remember what you typed. For those, tell the outlet to **retain** the routes you visit:

```osy syntax
Outlet;                  // one page at a time — disposed on navigate (the default)
Outlet(retain: true);    // every visited route stays alive, hidden, exactly one visible
Outlet(retain: 8);       // …up to 8; beyond that the least-recently-used one is dropped
```

A retained page is **mounted, just not visible**. Its state, its scroll position, and any **unsaved edits** survive —
so returning to it is instant and nothing you typed is lost. Its live queries go quiet while it's hidden and catch up
in a single read when you come back, so keeping pages around doesn't multiply your data traffic.

**The platform has no opinion about how you present this.** There is no tab component, no tab bar, no back-stack
widget. Retention is the mechanism; you decide whether it looks like tabs, a stack, a wizard, or nothing at all, and
you build that chrome from ordinary components in your layout. [Navigation](https://osysharp.com/reference/ui/navigation/) is what you read to build it.

Two rules the platform does enforce, because they protect the user's work:

- **A page with unsaved edits is never dropped to satisfy a `retain: N` cap.** The cap is exceeded instead. Losing
  someone's half-finished form to reclaim memory is never the right trade.
- **Closing a page with unsaved edits requires an explicit confirmation from your app.** The platform will refuse the
  close and tell you the page is dirty; showing the dialog (and what it says) is yours.

### Three rules about layouts   {#layout-notes}
- **A layout is not a route.** It has no `[Page]` of its own and never appears as a destination; it exists only to
  wrap pages.
- **A layout wrapping public pages must itself be public.** Page structure is fetched under the same secure-by-default
  rule as everything else, so a layout around `[AllowAnonymous]` pages needs `[AllowAnonymous]` too. A layout around
  protected pages simply stays protected.
- **Layouts don't nest yet.** A `[Layout]` that declares its own `[Layout(...)]` is a compile error rather than
  silently rendering without its parent.

### Why an in-app link does not reload the browser   {#navigation}
Once an app is running, moving between its pages **does not reload the browser**. A link to one of the app's own
routes swaps that page's content in place, leaving the rest of the app — and everything it has already loaded —
alive.

The address bar still holds a **real URL**, never a `#` fragment. So every page in your app is:

- **bookmarkable and shareable** — pasting the URL into a fresh tab opens that exact page;
- **navigable with Back and Forward**, which move between pages rather than out of the app;
- **refreshable** — reloading a deep URL re-serves that page, not the home page.

Route params keep working exactly as they do on a first load: `/catalog/{slug}` navigated to as `/catalog/shoes`
binds `shoes` to the page's `slug` parameter.

To navigate from code — and to read which routes are open, which is active, and which hold unsaved edits — use
[Navigation](https://osysharp.com/reference/ui/navigation/).

### What still performs a full browser navigation    {#hard-navigation}
Only in-app links are taken over. Everything a user expects the browser to handle, the browser still handles:

- a link to a path that **isn't one of your routes** (a file, an API path, a 404);
- an **external** link, or one marked `rel="external"`;
- a link with a **`target`** (e.g. opening in a new tab) or a **`download`**;
- a **modified click** — ⌘/Ctrl-click, Shift-click, Alt-click, or a middle-click.

That last one matters: "open in a new tab" keeps working on every link in your app.

### Signing in mid-navigation    {#navigation-auth}
Navigating to a page that requires authentication while signed out sends the visitor to your login page, carrying a
return address so they land back where they were headed. This is a convenience, not the security boundary — the
server independently refuses to serve a protected page's structure or data to a caller who isn't allowed it, so a
page can never leak by a client-side check being skipped.

### Auth model — secure by default {#auth}
**A routed component requires an authenticated principal BY DEFAULT.** A page opts out with `[AllowAnonymous]` — the
explicit, compiler-visible declaration that it is public. This is the platform's "safe by default" posture applied to
routing: the secure configuration is the zero-config one, and a page can never leak by a forgotten attribute (the
failure mode of the inverse "`[Authorize]` opts in" model).

- `component Home()` — protected: requires an authenticated principal.
- `[AllowAnonymous] component Home()` — public: any caller, including anonymous, may view it.
- `[Authorize(Managers)] component Home()` — protected *and* the principal must satisfy the named `policy Managers`
  (a compile-checked reference, not a string; see [page authorization (policies)](https://osysharp.com/reference/ui/authorize/)).

The **real authorization teeth are at the data boundary**, not at page delivery. A hard browser navigation carries no
credential (the platform authenticates with a Bearer token, not a cookie), so — exactly as with any single-page app —
the server cannot tell a logged-in user from an anonymous one at page-load. So delivery of the page *shell* is public
(a bootstrapper contains no app data), and the `[AllowAnonymous]`-or-not fact rides along only so the browser can
redirect an unauthenticated visitor to the login route before it tries to fetch a page it can't have. The server then
**independently refuses** to serve a protected page's structure or its data to a caller who isn't allowed it — so a
malicious client can load a public shell but can never obtain a protected page's tree or rows.

The login flow itself is `[AllowAnonymous]`, so it loads for a signed-out visitor. The user submits credentials, the
app's own auth surface issues them a session, and every subsequent page and data request is made under it.

## See also {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — the `[Page]` / `[Authorize]` / `[Render]` attributes on the component.
- [Navigation](https://osysharp.com/reference/ui/navigation/) — reading the open routes and navigating from code.
- [[ui-component#render-tree]] — the render tree the client walks once the component is mounted.
- <span class="planned" title="this page is planned and not written yet">ui-query-member</span> — how a mounted page's server-read members read data.
