---
title: Server components and islands
description: routes/*.tsx and routes/*.md rendered on the server; islands where something moves.
section: Server
order: 6
---

# Server components and islands

<p class="lead">Files in <code>routes/</code> are pages rendered to HTML on the server. <code>.tsx</code> pages use the same JSX runtime, kit and SDK as the browser; <code>.md</code> pages are Markdown. The browser only downloads JavaScript for islands.</p>

```tsx title="routes/notes.tsx"
import { Badge } from "sluurp/kit/badge.js";
import { Card } from "sluurp/kit/card.js";
import { Sluurp } from "sluurp";
import AddNote, { total } from "../islands/add-note.tsx";

// components can be async
async function Newest() {
  const page = await new Sluurp().collection("notes").list({ sort: "title" });
  return <ul>{page.items.map((n) => <li>{n.title}</li>)}</ul>;
}

export default async function Page() {
  // a "use server" function, called directly
  const count = await total();
  return (
    <main>
      <h1>Notes</h1>
      <Card><Badge>{count} notes</Badge><Newest /></Card>
      <AddNote start={count} since={new Date()}><em>Written on the server.</em></AddNote>
    </main>
  );
}
```

## Routes

| File | URL |
|---|---|
| `routes/index.tsx` | `/` |
| `routes/docs/sync.md` | `/docs/sync` |
| `routes/posts/[slug].tsx` | `/posts/anything` (`params.slug`) |
| `routes/files/[...rest].tsx` | `/files/a/b/c` |
| `routes/_layout.js` | root layout wrapping every page |
| `routes/docs/_layout.js` | nested layout for pages in `docs/` |

Pages need no config. They're public unless they export a `rule` (`export const rule = "@request.auth.id != null"`), and the title is the first `<h1>` unless `load` returns one. Data is read as the visitor, so collection rules apply.

## Markdown pages

```md title="routes/docs/sync.md"
---
title: Sync
section: Data
order: 3
---

# Sync

A shape is a collection narrowed by a filter…

<sluurp-island src="chart-playground"></sluurp-island>
```

Frontmatter is the page's data. A folder's `_layout.js` receives all sibling Markdown pages with their URLs and frontmatter, which is how this site's sidebar is generated. Code fences accept `title="…"`. Islands are just their HTML tag.

Each Markdown page's source is also served at its URL plus `.md` (`/docs/sync.md`). The site also gets [`/llms.txt`](/llms.txt), an index of all pages for LLMs grouped by `section`, and [`/llms-full.txt`](/llms-full.txt) with every page's text in one file. `routes/_llms.md` is prepended (title, summary, usage notes) and its `sections` sets section order. A static `llms.txt` file takes precedence.

## A whole page as an island

A route starting with `"use client"` is rendered on the server and then hydrated as a whole in the browser. The file itself is sent to the browser (minus `"use server"` bodies), so don't put secrets anywhere else in it. The default export gets `{ data, query, params }` on both sides.

```tsx title="routes/index.tsx"
"use client";

export const schema = { todos: { fields: { title: "text!", done: "bool" }, rules: "" } } as const;

export async function load() { /* on the server */ }
export default function Page({ data }) { /* on the server, then in the browser */ }
```

`export const schema` works in any route. It's parsed as a literal (never executed) and applied along with `schema.json` at startup. Each field is a type (`!` = required) or a full `schema.json` field definition; `rules` is either one rule for all five actions or an object.

Row types are derived from it, so fields are declared once. Add `as const` and import the type in the island:

```tsx title="islands/todos.tsx"
import type { RowOf } from "sluurp/sync";
import type { schema } from "../routes/index.tsx";

type Todo = RowOf<typeof schema.todos>; // { id: string; title: string; done: boolean }
```

`import type` is erased, so the island never loads the route. Required fields (`"text!"`) are non-null; others may be `null`, except `bool`, which is always true or false.

## Islands

Components imported from `islands/` are server-rendered with the page, then hydrated in the browser. Props must be serialisable (no functions); they're encoded with devalue, so a `Date` stays a `Date`. JSX children and markup props are sent as templates.

- `client="visible"` hydrates when scrolled into view, `"idle"` when the browser is idle, `"media:(min-width: 768px)"` when the query matches. Default (`"load"`) hydrates immediately.
- Hydration adopts the server DOM: text typed, focus and scroll position before hydration are preserved.
- `isolated` renders the island in its own shadow root with only Sluurp's styles, so it looks the same wherever it's embedded. The [Todos Collab and Sheet](/examples) examples on this site work this way.
- `new Sluurp().collection("notes")` works identically on both sides, as the visitor.
