Server components and islands
Files in routes/ are pages rendered to HTML on the server. .tsx pages use the same JSX runtime, kit and SDK as the browser; .md pages are Markdown. The browser only downloads JavaScript for islands.
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
---
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, an index of all pages for LLMs grouped by section, and /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.
"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:
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.
isolatedrenders 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 on this site work this way.new Sluurp().collection("notes")works identically on both sides, as the visitor.