A server-first full-stack framework built on Hono, Preact 11, and Vite.
Build fast, progressively enhanced web apps with server-side rendering, file-based routing, typed routes, server actions, middleware, and opt-in islands. Otok ships HTML first and sends browser JavaScript only where interactivity is needed.
Documentation · Quick start · Examples · Roadmap · Discussions
| Capability | What it gives you |
|---|---|
| Server-first rendering | HTML on the first response, with client JavaScript only for interactive islands |
| File-based routing | Pages, nested layouts, middleware, error boundaries, and typed URL generation |
| Progressive enhancement | Native forms and links work without JavaScript; soft navigation enhances them when enabled |
| Full-stack Hono foundation | Add APIs, authentication, uploads, and Hono middleware beside SSR routes |
| Preact 11 included | Build islands with Preact 11 and hydrate them on load, idle, visibility, or a media query |
| Deployable architecture | Node.js reference deployment plus Edge-safe and Cloudflare Workers foundations |
| Production-oriented tooling | Testing helpers, health checks, graceful shutdown, package checks, and reproducible builds |
Otok keeps its core focused: routing, rendering, islands, actions, middleware, and deployment primitives. UI libraries, databases, authentication, validation, and CSS remain explicit application choices.
Prerequisite: Node.js 20 or newer.
pnpm create otok@latest my-app
cd my-app
pnpm install
pnpm devThe generated project includes Preact 11 and is ready for server rendering and island hydration—no separate Preact setup is required.
For a full dashboard starter with Kamod UI, run:
pnpm create otok@latest my-app --template fullAn Otok app has four main areas:
src/server.ts Hono server entry
src/client.ts Island hydration entry
src/app/routes/ File-based pages, layouts, and special routes
src/app/islands/ Interactive Preact components
- The Vite plugin scans
src/app/routesand generates typed route definitions. - Hono handles requests, middleware, loaders, actions, and API endpoints.
- Preact 11 renders pages to HTML on the server.
<Island>marks only the interactive parts of a page.- The client hydrates those islands using the selected loading strategy.
- Optional soft navigation swaps page regions without remounting persistent layout chrome.
- Pages without islands omit the client module entirely.
flowchart LR
Routes["Routes"] --> Vite["Otok Vite plugin"]
Islands["Islands"] --> Vite
Vite --> Server["Hono + Preact SSR"]
Server --> HTML["HTML response"]
HTML --> Hydration["Selective hydration"]
Routes are files in src/app/routes:
routes/index.tsx /
routes/about.tsx /about
routes/users/[id].tsx /users/:id
routes/docs/[...slug].tsx /docs/:slug*
routes/[[lang]]/about.tsx /about and /:lang/about
routes/(marketing)/about.tsx /about
Special files colocate framework behavior with the routes they affect:
_layout.tsx Shared layout
_middleware.ts Route middleware
_not-found.tsx Convention-based 404 page
_error.tsx Convention-based error page
The generated virtual:otok-routes module also provides a typed URL builder:
import { route } from "virtual:otok-routes";
route("/users/[id]", { params: { id: "alice" } });
route("/docs/[...slug]", { params: { slug: ["routing", "catch-all"] } });Route modules can load data and handle mutations on the server. Native HTML forms work without JavaScript; optional progressive enhancement uses the same soft-navigation runtime.
import { fail, redirect, type OtokActionContext } from "@kamod-ch/otok/server";
export async function action({ formData }: OtokActionContext) {
const name = String(formData?.get("name") ?? "").trim();
if (!name) {
fail(400, {
message: "Validation failed",
fieldErrors: { name: ["Name is required"] },
});
}
await saveProject(name);
redirect("/projects", 303);
}
export default function ProjectForm() {
return (
<form method="post">
<input name="name" />
<button>Save</button>
</form>
);
}Otok also includes response helpers for JSON, redirects, validation failures, not-found responses, and typed route data.
Islands are Preact components rendered on the server and hydrated later in the browser:
import { Island } from "@kamod-ch/otok/client";
import Counter from "../islands/counter";
export default function Page() {
return <Island component={Counter} props={{ initial: 5 }} strategy="visible" />;
}Available hydration strategies:
| Strategy | Behavior |
|---|---|
load |
Hydrate immediately |
idle |
Hydrate during browser idle time |
visible |
Hydrate when the island enters the viewport |
media |
Hydrate when a media query matches |
client-only |
Skip SSR markup and render only in the browser |
Keep persistent shells mounted while replacing the current page and other named regions:
createOtokClient({
registry: islandModules,
softNav: { forms: true },
});<nav data-otok-swap="sidebar-nav">...</nav>
<header data-otok-swap="topbar">...</header>
<main>{children}</main>Otok synchronizes managed head metadata, swaps matching regions, hydrates new islands, restores navigation state, and falls back to a full navigation when enhancement is not possible.
Use createOtokHandler() when your application also needs API routes, authentication middleware, or uploads:
import { Hono } from "hono";
import { createOtokHandler } from "@kamod-ch/otok/server";
import { routes } from "virtual:otok-routes";
const app = new Hono();
app.get("/api/health", (c) => c.json({ ok: true }));
app.get("*", createOtokHandler({ routes }));The default template creates separate Vite builds for browser and server code:
pnpm build:client
pnpm build:server
pnpm startNode.js is the reference runtime. The repository also contains Cloudflare Workers smoke tests and Edge-safe runtime foundations. See the Node deployment guide for Docker, reverse-proxy, health-check, static-asset caching, and graceful-shutdown guidance.
Use @kamod-ch/otok-test for server-side route tests without a browser or Vite development server:
import { createTestApp, renderRoute } from "@kamod-ch/otok-test";
const app = createTestApp({
routes: [
{
path: "/users/:id",
component: ({ params }) => <p>User {params.id}</p>,
},
],
});
const { response, html } = await renderRoute(app, "/users/123");Use Playwright for hydration, progressive enhancement, and browser behavior.
The framework core in this repository is @kamod-ch/otok 2.0.0 — active monorepo development, not proof that npm already serves that version or that every extension is production-ready. Read the current status before upgrading apps:
- Otok 2.x repository status — supported tooling, verified gates, honest limits
- 2.x migration notes — names, imports, and doc map from 0.x-era material
- Release-candidate assessment (historical)
- Stabilization evidence log (historical)
- Changelog
- Documentation site
- Framework conventions
- Examples
- Contributing guide
- Security policy
- GitHub Discussions
Contributions are welcome. Read CONTRIBUTING.md, run the project checks locally, and include a changeset for user-visible package changes.
Otok is available under the MIT License.