camelon

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 default function Page({ name }: { name: string }) {
  return (
    <section>
      <h1 safe>{name}</h1>
      <p>Static text needs no escaping.</p>
    </section>
  );
}

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 { RouteArgs } from 'camelon';

export async function action({ stream, signals }: RouteArgs) {
  const { html = '' } = signals as { html?: string };
  stream.patchElements((<p id="safe-out" safe>{html || '…'}</p>) as unknown as string);
}
service worker

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>
  );
}

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 = (data: LoaderOutput) => (
  <>
    <title safe>{data.name} | My App</title>
    <style>{`body { background: #111 }`}</style>
  </>
);

Static assets

Files in public/ are served at the root: public/styles.css/styles.css. On Node, mount them with express.static('public') next to the handler.