Server islands
Render selected components on the server with Oxide, frames, and oxidejs actions.
Use Oxide when a component must close over server data. You keep the SPA authoring model. Files named *.server.tsx never ship their render function to the browser.
import const pages: (options?: IlhaPagesOptions) => PluginVite plugin — use via `@ilha/router/vite`.pages from "@ilha/router/vite";
import const oxide: (options?: OxidejsOptions | undefined) => import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide from "oxidejs/vite";
import { function defineConfig(config: UserConfig): UserConfig (+5 overloads)Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.defineConfig } from "vite";
export default function defineConfig(config: UserConfig): UserConfig (+5 overloads)Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.defineConfig({
UserConfig.plugins?: PluginOption[] | undefinedArray of vite plugins to use.plugins: [function oxide(options?: OxidejsOptions | undefined): import("vite").Plugin<any>[] | import("vite").Plugin<any>oxide({ OxidejsOptions.middleware?: (string | {
module: string;
imports?: string[];
})[] | undefined
Module specifiers whose default export is `(request, ctx) => Response | undefined | Promise<Response | undefined>`.
Tried in order at the top of the production fetch handler; a Response short-circuits.
Dev servers use connect middleware instead.middleware: ["@ilha/router/ssr"] }), function pages(options?: IlhaPagesOptions): PluginVite plugin — use via `@ilha/router/vite`.pages()],
});
The middleware owns:
| Endpoint | Purpose |
|---|---|
POST /__ilha/frame |
Render a server island again |
There is no page loader endpoint. Fetch inside an async component, or yield from a streaming action.
One island on the server
Put the component in *.server.ts(x):
import * as Stream from "effect/Stream";
import { action } from "oxidejs";
export const deleteTask = action(async (id: string) => {
await db.tasks.delete(id);
});
export const getTasks = action(async function* () {
yield await db.tasks.list();
});
export const TaskList = async function TaskList() {
return Stream.map(
Stream.fromAsyncIterable(getTasks(), (error: unknown) =>
error instanceof Error ? error : new Error(String(error))
),
(list: { id: string; text: string }[]) => (
<ul>
{list.map((task) => (
<li key={task.id}>
{task.text}
<button type="button" onclick={deleteTask.with(task.id)}>
Remove
</button>
</li>
))}
</ul>
)
);
};
Import it from a regular page:
import { TaskList } from "$lib/tasks.server";
export default function Home() {
return <TaskList />;
}
The plugin replaces the server module with a client proxy. The proxy posts /__ilha/frame, morphs the HTML, and reconnects events.
Mark RPC functions with action from oxidejs. Bind them with .with(...) on event props so SSR can emit frame sentinels without running your handler during render.
Subscribe with Stream.fromAsyncIterable(getTasks()) so the scanner wires a live stream. Each yield refetches the frame.
Whole route on the server
Name the page foo.server.tsx. Export a default component. The client still mounts a proxy and pulls the first frame.
Auth
Frames are deny-by-default in production. For a public demo:
import { const setFrameAuth: (policy: FrameAuthPolicy) => voidInstall the frame-authorization policy consumed by the production
`@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by
the dev middleware (via `IlhaPagesOptions`).setFrameAuth } from "@ilha/router/ssr";
function setFrameAuth(policy: FrameAuthPolicy): voidInstall the frame-authorization policy consumed by the production
`@ilha/router/ssr` handler. `trustedOrigins` and `csrf` are also applied by
the dev middleware (via `IlhaPagesOptions`).setFrameAuth({ FrameAuthPolicy.defaultAction?: "open" | "deny" | undefinedAction taken when no frame guard is registered. `"deny"` (default in the
production handler) rejects every `/__ilha/frame` request with 403;
`"open"` preserves the legacy unauthenticated behavior. The dev
middleware stays permissive unless a guard is registered.defaultAction: "open" });
Install setFrameGuard() when islands serve private data. See Middleware and security.