Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview
On this page

Client-only SPA with Oxide actions

Serve a static SPA shell, render frames on demand, and drop SSR guards you do not need.

Ship one HTML shell and mount the router in the browser. Add the Oxide SSR middleware only when server islands or actions need a frame renderer.

Mount without SSR

import { pageRouter } from "ilha:pages/client";

pageRouter.mount("#app");

Serve index.html for every route (a static host fallback). Deep links load the shell, then the router paints the page. Reach for hash mode when the host is file:// or cannot serve a fallback:

import { setHistoryMode } from "@ilha/router";

setHistoryMode("hash");

The logical path then lives in location.hash, which the server never sees.

Add Oxide actions

Actions still need a server. The client proxy posts to POST /__ilha/frame, and that endpoint only exists behind @ilha/router/ssr:

import pages from "@ilha/router/vite";
import oxide from "oxidejs/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [oxide({ middleware: ["@ilha/router/ssr"] }), pages()],
});

On the client you keep the same mount() call. Frames render on demand and morph into the SPA tree. See Server islands.

Drop guards you do not need

Frame guards (setFrameGuard, setFrameAuth) protect server island state. A pure static SPA has no frame endpoint, so there is nothing to guard — skip them. Add them the moment you serve frames: production denies unguarded frame POSTs with 403. Demos over world-readable state open the endpoint explicitly:

import { setFrameAuth } from "@ilha/router/ssr";

setFrameAuth({ defaultAction: "open" });

Keep "open" to demos. Gate real user state with setFrameGuard(). See Middleware and security.

Was this page helpful?