JSX / TSX on onvibe (no build step)
onvibe serves the files you deploy and runs them in an isolated runtime — there is no build
phase (no npm install, no bundler). You can still write views in JSX/TSX: Deno transpiles
them on the fly. This is the recommended way to build UIs with components — prefer it over Fresh,
SvelteKit, Vite or any framework that needs a local build, unless you specifically need one.
Start from it with create_project({ template: "jsx" }).
The rules (follow all of them)
-
Put a per-file pragma at the very top of every
.tsxfile:/** @jsxImportSource npm:preact */This tells Deno which JSX runtime to use. It is per file — the runtime does NOT read
deno.json, so ajsxImportSourceindeno.jsonis ignored. Every.tsxfile needs its own pragma line. -
Keep the entry
main.ts(plain TypeScript, no JSX). onvibe's entry file ismain.tsand it exports the handler. A.tsfile cannot contain JSX syntax, so put all JSX in.tsxfiles and import from them. -
Use only
npm:/jsr:specifiers and relative imports. e.g.npm:preact,npm:preact-render-to-string,./views.tsx. Do NOT rely on bare specifiers mapped throughdeno.jsonimports— the runtime does not load that import map, so bareimport "preact"fails. Always write the fullnpm:/jsr:specifier. -
Render server-side to a string with
preact-render-to-string; the handler serves that string as HTML.
Canonical structure
views.tsx — all JSX lives here:
/** @jsxImportSource npm:preact */
import { render } from "npm:preact-render-to-string";
function Layout({ title, children }: { title: string; children: unknown }) {
return (
<html lang="en">
<head><meta charset="utf-8" /><title>{title}</title></head>
<body>{children as any}</body>
</html>
);
}
export function renderHome(): string {
return "<!doctype html>" + render(
<Layout title="My app">
<h1>Hello</h1>
</Layout>,
);
}
main.ts — entry, no JSX:
import { withErrorReporting } from "./.onvibe/helpers.ts";
import { renderHome } from "./views.tsx";
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/") {
return new Response(renderHome(), {
headers: { "content-type": "text/html; charset=utf-8" },
});
}
return new Response("Not found", { status: 404 });
}
export default withErrorReporting(handler);
Notes
- This is server-side rendering (SSR). The browser receives plain HTML. For client-side
interactivity you must ship browser JavaScript separately (e.g. a small
<script>or a client module served from a route); the.tsxcomponents above render on the server only. - Preact
classandclassNameboth work;classis fine. - Split views into multiple
.tsxfiles freely — just remember the pragma on each one. - Combine with
create_database+npm:pgexactly as any other onvibe app; nothing about the database pattern changes.
Why not Fresh / SvelteKit / Vite?
Those frameworks require a local build, and their bundlers emit bare import "pg"-style
specifiers that depend on an import map the runtime does not load — so they need extra setup and
can break. Native JSX/TSX as above needs no build and no import map, so it "just works". Reach for
a framework only when you truly need its features.