Rendering & JSX
camelon renders with @kitajs/html. JSX compiles to a string (or Promise<string>). There is no virtual DOM and no client runtime. The server produces HTML.
export type GetOutput = { name: string };
export default function Page({ name }: GetOutput) {
return (
<section>
<h1 safe>{name}</h1>
<p>Static text needs no escaping.</p>
</section>
);
}
No data handler, no client JS — camelon renders the JSX and returns HTML:
export default function Hello() {
return (
<div class="d-card">
<strong>Hello from camelon</strong>
<p>A route with just a <code>default</code> export renders straight to HTML.</p>
</div>
);
}The safe attribute
Dynamic values are not escaped by default. That is what lets rendered markdown and trusted HTML pass through. Add safe to escape anything that could hold user input.
<p safe>{userValue}</p> // escaped — do this for untrusted input
<p>{trustedHtml}</p> // raw — only for HTML you produced
The @kitajs/ts-html-plugin flags a missing safe in your editor. Type some HTML below and render it — safe brings it back as text, so the markup never executes:
export default function Safe() {
return (
<div class="d-card" signals='{"html": ""}'>
<div class="d-row">
<input class="d-input" bind:html="" placeholder="try <b>bold</b>" />
<button class="d-btn d-wide" on:click="@post('/safe/render')">Render</button>
</div>
<p id="safe-out">…</p>
</div>
);
}import type { PostFunction } from 'camelon';
export type PostInput = { signals: { html: string } };
export const post: PostFunction<PostInput> = async ({ stream, signals }) => {
const html = signals.html ?? '';
stream.patchElements((<p id="safe-out" safe>{html || '…'}</p>) as unknown as string);
};The document
src/document.tsx wraps every page. The framework discovers it and prepends <!doctype html>. It receives the page as children and the merged head as head.
export const head = () => (
<>
<meta charset="utf-8" />
<title>My App</title>
<link rel="stylesheet" href="/styles.css" />
</>
);
export default function Document({ children, head }: DocumentProps) {
return (
<html lang="en">
<head>{head}</head>
<body>{children}</body>
</html>
);
}
Document data
document.tsx can also export an optional loader — a root-loader that runs once per full-document render with the same args as a route handler (session, db, …). What it returns arrives on the document as data. Type it by declaring data on Register, the same interface that types db, ctx, and session:
import type { DocumentLoaderFunction, DocumentProps } from 'camelon';
declare module 'camelon' {
interface Register {
data: { user: { name: string } | null };
}
}
export const loader: DocumentLoaderFunction = ({ session }) => ({
user: session.get('userId') ? { name: 'Ada' } : null,
});
export default function Document({ children, head, data }: DocumentProps) {
return (
<html lang="en">
<head>{head}</head>
<body>
{data?.user ? <p safe>Signed in as {data.user.name}</p> : ''}
{children}
</body>
</html>
);
}
Read data defensively — it is undefined when there is no root-loader, and on the error and 404 overlays that render without a request context. The root-loader is read-only by convention: it runs after the route has flushed its cookies, so session writes there won't persist.
Per-route head
A route can export head to override or extend the document head. The merge is tag-level: the route wins on a colliding <title>, <meta name>, or <script src>. Styles always append.
export const head: HeadFunction<GetOutput> = (data) => (
<>
<title safe>{data.name} | My App</title>
<style>{`body { background: #111 }`}</style>
</>
);
This route sets its own <title> and a background — watch it take effect:
import type { HeadFunction } from 'camelon';
export const head: HeadFunction = () => (
<>
<title>Head override — camelon</title>
<style>{`.d-head{background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff}.d-head p,.d-head code{color:#eef;background:rgba(255,255,255,.15)}`}</style>
</>
);
export default function Head() {
return (
<div class="d-card d-head">
<strong>This route set its own <title> + background</strong>
<p>
via a <code>head</code> export — camelon merged it into the document head. The gradient
comes from that head's <code><style></code>.
</p>
</div>
);
}Static assets
Files in public/ are served at the root: public/styles.css → /styles.css. On Node, mount them with express.static('public', { index: false }) next to the handler.