Advanced: move the first render to the server.
Take the Counter from the basic path unchanged. The server produces its initial HTML; the browser
loads the approved component module and attaches bindings and events to those exact nodes.
Render the island
import { renderIsland } from "@taipa/ui/server";
import { Counter } from "./counter";
const island = await renderIsland(
Counter,
{},
{
id: "counter",
hydrate: "load",
module: "/assets/counter.js",
exportName: "Counter",
state: { count: 3 },
},
);The island string contains its host, hydration policy, module metadata, and inert state payload. Write it into an ordinary HTML response from the runtime you already use.
Keep payloads bounded
Each props or state script is limited to 64 KiB, measured as post-escape JSON characters. renderIsland()
rejects an oversized payload before it returns markup, matching the browser’s preflight limit. In development,
payloads at 75% of that limit emit a warning with the component name, payload kind, character count, and UTF-8
byte count. The warning never includes serialized data.
Keep props and state focused on the data an island needs to attach. Island payloads are visible in the page source and should never contain credentials or other secrets.
Return the page from your runtime
The component and island renderer are runtime-neutral ESM. Only the few lines that create the HTTP response differ between Node, Deno, and Bun.
pnpm add hono @hono/node-serverimport { Hono } from "hono";
import { serve } from "@hono/node-server";
import { renderIsland } from "@taipa/ui/server";
import { Counter } from "./counter.js";
const app = new Hono();
app.get("/", async (context) => {
const island = await renderIsland(Counter, {}, {
id: "counter",
hydrate: "load",
module: "/assets/counter.js",
exportName: "Counter",
state: { count: 3 },
});
return context.html(
[
"<!doctype html>",
"<main>" + island + "</main>",
"<script type=\"module\" src=\"/assets/bootstrap.js\"></script>",
].join(""),
);
});
serve(app);import { renderIsland } from "@taipa/ui/server";
import { Counter } from "./counter.ts";
Deno.serve(async () => {
const island = await renderIsland(Counter, {}, {
id: "counter",
hydrate: "load",
module: "/assets/counter.js",
exportName: "Counter",
state: { count: 3 },
});
return new Response(
[
"<!doctype html>",
"<main>" + island + "</main>",
"<script type=\"module\" src=\"/assets/bootstrap.js\"></script>",
].join(""),
{
headers: { "content-type": "text/html; charset=utf-8" },
},
);
});import { renderIsland } from "@taipa/ui/server";
import { Counter } from "./counter.ts";
Bun.serve({
async fetch() {
const island = await renderIsland(Counter, {}, {
id: "counter",
hydrate: "load",
module: "/assets/counter.js",
exportName: "Counter",
state: { count: 3 },
});
return new Response(
[
"<!doctype html>",
"<main>" + island + "</main>",
"<script type=\"module\" src=\"/assets/bootstrap.js\"></script>",
].join(""),
{
headers: { "content-type": "text/html; charset=utf-8" },
},
);
},
});Register the browser module
import { bootstrap } from "@taipa/ui/client";
import { Counter } from "./counter";
bootstrap({
registry: {
Counter: { load: async () => ({ Counter }), exportName: "Counter" },
},
});bootstrap() discovers eligible island hosts, resolves only registry-approved modules, and follows
the load policy from the server output. The three server examples above can all serve this same
browser module.
Keep the server markup authoritative
Hydration preflights payload shapes and every singular declared ref before it adds listeners or starts
effects. A missing or duplicated data-taipa-ref leaves the server HTML intact and reports an error
rather than replacing nodes.