Server components and islands
A file in routes/ is a page rendered to HTML on the server. A .tsx page uses the same JSX runtime, kit and SDK as the browser; a .md page is Markdown. The browser receives only the islands' JavaScript.
import { Card, Badge } from "sluurp/kit";
import { Sluurp } from "sluurp";
import AddNote, { total } from "../islands/add-note.tsx";
// a component may wait for its data
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 in place
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, as params.slug |
routes/files/[...rest].tsx | /files/a/b/c |
routes/_layout.js | the document every page is wrapped in |
routes/docs/_layout.js | wraps the pages in docs/, inside the one above |
A page needs no declarations. It is public unless it exports a rule (export const rule = "@request.auth.id != null"), and its title is its first <h1> unless load returns another. What a page shows, it reads as the visitor, so collection rules still decide what that is.
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>The frontmatter is the page’s data. A folder’s _layout.js receives every Markdown page beside it, with each page’s URL and frontmatter, so this site’s sidebar builds itself. Code fences can take a title="…". An island is just its HTML tag, which the browser wakes like any other island.
A whole page as an island
A route that begins with "use client" is drawn on the server and then taken over, whole, by the browser. The browser is sent the file itself, with its "use server" bodies left out, so nothing else in it should be secret. Its default export receives { data, query, params } on both sides.
"use client";
export const schema = { todos: { fields: { title: "text!", done: "bool" }, rules: "" } };
export async function load() { /* on the server */ }
export default function Page({ data }) { /* on the server, then in the browser */ }export const schema can go in any route. It is read as a literal, never run, and applied with the app’s schema.json when the server starts. Each field is a type (with ! for required) or a field as schema.json writes it, and rules is one rule for all five or an object with each.
Islands
A component imported from islands/ is rendered on the server with the page, then taken over by the browser. Its props must be data, not functions. They are written as devalue, so a Date prop is still a Date in the browser. JSX children and markup props travel as templates.
client="visible"wakes an island when it scrolls into view,"idle"when the browser is idle, and"media:(min-width: 768px)"when a media query matches. By default ("load"), it wakes at once.- The browser adopts what the server drew: text typed, the focus and scroll positions set before an island wakes are all still there after.
isolateddraws an island inside its own shadow root, with only Sluurp’s styles: it looks exactly the same wherever it is embedded, whatever the page around it does. The Todos Collab example on this site is one.new Sluurp().collection("notes")works the same on both sides, reading as the visitor on the server and in the browser.