A Svelte island, not a second app
Add a Svelte character counter to the notebook's native form. Submission and validation still work without it. This recipe uses Svelte 5.57.2 and Vite 6.4.4.
Connect the server
import { fileURLToPath } from "node:url";
import { html, staticAssets } from "@portablesheep/brevity-web";
import { createIslands, islandManifest } from "@portablesheep/brevity-islands";
import { createNotebook } from "../tutorial/app.js";
import type { NoteStore } from "../tutorial/store.js";
import { nativeRoutes } from "../native/server.js";
export function islandApp(store: NoteStore) {
const island = createIslands({ manifest: "/assets/islands.json", loader: "/assets/loader.js" });
const app = createNotebook(store, (form) =>
island(
"counter",
{ initialText: form.value("text") },
{
fallback: html`<div id="note-counter">Character counter available with JavaScript.</div>`,
},
),
);
app.router.get("/assets/islands.json", (_req, res) =>
res.json(islandManifest({ counter: "/assets/counter.js" })),
);
app.use(
staticAssets({
"/assets/counter.js": {
file: fileURLToPath(new URL("./dist/counter.js", import.meta.url)),
},
"/assets/loader.js": {
file: fileURLToPath(import.meta.resolve("@portablesheep/brevity-islands/loader.js")),
},
}),
);
nativeRoutes(app);
return app;
}createIslands emits safely serialized props in an escaped data attribute, an external loader, and a no-JS fallback. It reuses web's JSON escaping, including retained values containing </script>. The JSON manifest maps component names to module URLs. Never put state into executable inline JS. staticAssets serves only explicitly mapped paths; it is not a directory server.
Mount the component
import { defineIsland } from "@portablesheep/brevity-islands/client";
import { svelteIsland } from "@portablesheep/brevity-islands-svelte";
import NoteCounter from "./NoteCounter.svelte";
const mountCounter = svelteIsland(NoteCounter);
export default defineIsland((element, data: unknown, context) => {
if (
!data ||
typeof data !== "object" ||
!("initialText" in data) ||
typeof data.initialText !== "string"
)
throw new Error("Invalid note island state");
return mountCounter(element, { initialText: data.initialText }, context);
});Export a defineIsland default mount, validate state, and delegate to the Svelte 5 adapter from @portablesheep/brevity-islands-svelte. The native loader imports it after the form exists.
<script lang="ts">
import { onMount, untrack } from "svelte";
let { initialText }: { initialText: string } = $props();
let length = $state(untrack(() => initialText.length));
onMount(() => {
const field = document.querySelector<HTMLTextAreaElement>('textarea[name="text"]');
if (!field) throw new Error("Missing native note field");
const update = () => { length = field.value.length; };
update();
field.addEventListener("input", update);
return () => field.removeEventListener("input", update);
});
</script>
<p id="note-counter" aria-live="polite">{length} / 200 characters</p>The component has no credentials or application state store. It observes the native field and removes its listener when unmounted. With JavaScript disabled or the bundle unavailable, the same form still submits and retains 422 errors.
Build independently
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { fileURLToPath } from "node:url";
export default defineConfig({
plugins: [svelte()],
build: {
outDir: fileURLToPath(new URL("./dist", import.meta.url)),
emptyOutDir: true,
lib: {
entry: fileURLToPath(new URL("./client/main.ts", import.meta.url)),
formats: ["es"],
fileName: () => "counter.js",
},
},
});npm run docs:check
npm run docs:e2eThe first command builds docs/samples/island/dist/counter.js; the server maps that precise asset. no-cache is appropriate for this fixed filename; for a production app, generate hashed assets and an explicit manifest/map before using immutable cache headers. Vite bundles the Svelte runtime; Brevity does not compile .svelte files.
For a local preview, use the checked example entry point:
import { openStore } from "../tutorial/store.js";
import { islandApp } from "./server.js";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const directory = await mkdtemp(join(tmpdir(), "brevity-island-"));
const store = await openStore(`file:${join(directory, "notes.db")}`);
const server = islandApp(store).listen(3107);
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.once(signal, () => {
server.close((error) => {
store.close();
if (error) {
console.error(error);
process.exitCode = 1;
}
void rm(directory, { recursive: true, force: true }).catch((cleanupError) => {
console.error(cleanupError);
process.exitCode = 1;
});
});
server.closeAllConnections();
});
}It listens on http://127.0.0.1:3107 with a temporary SQLite file. The Playwright recipe runs it automatically.
See native islands for deferred hydration, plain TypeScript components, enhanced forms, and CSP/asset contracts.