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, component contract version, hydration policy, module metadata, and inert state payload. Write it into an ordinary HTML response from the runtime you already use.
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 the contract version 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.