JSX/HTML Utility - Full Reference
Exhaustive reference for htmlContent(), htmlResponse(), and BaseRestController.defineJSXRoute(). For a readable introduction and the common tasks, start with the JSX/HTML overview.
Files:
packages/core/src/utilities/jsx.utility.ts-htmlContent,htmlResponsepackages/core/src/base/controllers/rest/base.ts-BaseRestController.defineJSXRoutepackages/core/src/base/controllers/rest/abstract.ts-AbstractRestController.getJSXRouteConfigspackages/helpers/src/common/types.ts-FC,PropsWithChildren,Child(re-exported fromhono/jsx)
htmlContent()
Creates a standard OpenAPI content object for text/html responses.
Source -> packages/core/src/utilities/jsx.utility.ts
const htmlContent = (opts: { description: string; required?: boolean }) => ({
description: opts.description,
content: {
'text/html': {
schema: z.string().openapi({
description: 'HTML content',
example: '<!DOCTYPE html><html><head><title>Page</title></head><body>...</body></html>',
}),
},
},
required: opts.required ?? false,
});Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | Yes | - | Description of the HTML content, shown in the generated OpenAPI document |
required | boolean | No | false | Whether the content is required |
Returns
An OpenAPI content configuration object: description, content['text/html'].schema (a z.string()), and required.
htmlResponse()
Creates a standard OpenAPI response object for HTML endpoints: a success (200) HTML response plus a JSON error response for 4xx | 5xx status codes using ErrorSchema.
Source -> packages/core/src/utilities/jsx.utility.ts
const htmlResponse = (opts: { description: string; required?: boolean }) => ({
[HTTP.ResultCodes.RS_2.Ok]: htmlContent({ description: opts.description, required: opts.required }),
['4xx | 5xx']: {
description: 'Error Response',
content: { 'application/json': { schema: ErrorSchema } },
},
});Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | Yes | - | Description of the successful HTML response |
required | boolean | No | false | Whether the content is required |
Returns
A responses object: 200 (via htmlContent()) plus 4xx | 5xx (JSON ErrorSchema).
import { htmlResponse } from '@venizia/ignis';
this.defineRoute({
configs: {
path: '/dashboard',
method: 'get',
responses: htmlResponse({ description: 'Dashboard HTML page' }),
},
handler: c => c.html(<h1>Dashboard</h1>),
});BaseRestController.defineJSXRoute()
Defines and registers a JSX/HTML route in a single call - the JSX counterpart of defineRoute().
Source -> packages/core/src/base/controllers/rest/base.ts
defineJSXRoute<RouteConfig extends IAuthRouteConfig, ResponseType = unknown>(opts: {
configs: RouteConfig;
handler: TRouteHandler<ResponseType, RouteEnv>;
hook?: Hook<any, RouteEnv, string, ValueOrPromise<any>>;
}): IDefineRouteOptions<RouteConfig, RouteEnv, RouteSchema, BasePath>| Parameter | Type | Required | Description |
|---|---|---|---|
configs | RouteConfig (extends IAuthRouteConfig) | Yes | path, method, responses, plus the same authenticate/authorize/request/middleware fields defineRoute accepts |
handler | TRouteHandler<ResponseType, RouteEnv> | Yes | Receives the route context (TRouteContext); return c.html(<Component />) |
hook | Hook<...> | No | Same validation hook defineRoute accepts |
Behavior
defineJSXRoute is defineRoute with one difference: it builds the route configuration through getJSXRouteConfigs instead of getRouteConfigs.
Source -> packages/core/src/base/controllers/rest/abstract.ts
getJSXRouteConfigs<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
const { restConfig, security, mws } = this.buildRouteMiddlewares(opts);
const { responses, tags = [] } = restConfig;
return createRoute<string, RouteConfig>(
Object.assign({}, restConfig, {
middleware: mws,
responses: Object.assign({}, htmlResponse({ description: 'HTML page' }), responses),
tags: [...tags, this.scope],
security,
}) as any,
);
}- Default response merged in first.
htmlResponse({ description: 'HTML page' })is the base object; your ownresponsesis merged over it withObject.assign, so any status code you declare (typically200) overrides the default entry with the same key. - Everything else matches
defineRoute. Auth middleware, tags (this.scopeis always appended), and OpenAPI security are built the same way as JSON routes viabuildRouteMiddlewares. - Handler contract is unchanged. The handler still returns whatever
c.html(...)produces (aResponse) -defineJSXRouteonly changes how the route's OpenAPI shape is computed, not how the handler runs.
JSX setup
tsconfig.json
.tsx files compile against Hono's JSX runtime, not React's:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}Verified in packages/core/tsconfig.json and the JSX example app's examples/rpc-api-server/tsconfig.json.
Component types
FC, PropsWithChildren, and Child are re-exported from @venizia/ignis-helpers (sourced from hono/jsx) - import them from there rather than reaching into hono/jsx directly.
Source -> packages/helpers/src/common/types.ts
export type { Child, FC, PropsWithChildren } from 'hono/jsx';No separate renderer
IGNIS registers no jsxRenderer middleware and no template engine. A handler builds a JSX tree and calls Hono's own c.html() on it - that call is what triggers rendering to an HTML string.
Component patterns
Layout composition
A layout component takes children (typed via PropsWithChildren) and wraps them in the surrounding document shell. Page components render into a layout the same way any JSX component nests another.
import type { FC, PropsWithChildren } from '@venizia/ignis-helpers';
interface MainLayoutProps {
title: string;
description?: string;
}
export const MainLayout: FC<PropsWithChildren<MainLayoutProps>> = ({ title, description, children }) => (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<title>{title}</title>
{description && <meta name="description" content={description} />}
</head>
<body>
<main>{children}</main>
</body>
</html>
);
interface HomePageProps {
timestamp?: string;
}
export const HomePage: FC<HomePageProps> = ({ timestamp }) => (
<MainLayout title="Home" description="Welcome to IGNIS">
<h1>Welcome to IGNIS!</h1>
{timestamp && <p>Page rendered at: {timestamp}</p>}
</MainLayout>
);Wiring pages into a controller
import { BaseRestController, controller, htmlContent, type IControllerOptions, type ValueOrPromise } from '@venizia/ignis';
import { HTTP } from '@venizia/ignis-helpers';
import { HomePage } from '@/views/pages/home.page';
@controller({ path: '/' })
export class ViewController extends BaseRestController {
constructor(opts: IControllerOptions) {
super({ ...opts, scope: ViewController.name, path: '/' });
}
override binding(): ValueOrPromise<void> {
this.defineJSXRoute({
configs: {
path: '/',
method: 'get',
description: 'Home page rendered with JSX',
tags: ['Views'],
responses: {
[HTTP.ResultCodes.RS_2.Ok]: htmlContent({ description: 'Home page HTML' }),
},
},
handler: c => {
const timestamp = new Date().toISOString();
return c.html(<HomePage timestamp={timestamp} />);
},
});
}
}Raw HTML with dangerouslySetInnerHTML
Hono JSX's intrinsic elements accept a dangerouslySetInnerHTML prop - an object with an __html string - matching React's escape hatch. Use it only for HTML you already trust (a stored template, sanitized markdown output) - never for unsanitized user input.
async previewTemplate(c: TRouteContext) {
const { templateId } = c.req.valid<{ templateId: string }>('param');
const template = await this.emailService.getTemplate(templateId);
return c.html(
<html>
<head>
<title>Email Preview: {template.subject}</title>
</head>
<body>
<div dangerouslySetInnerHTML={{ __html: template.html }} />
</body>
</html>,
);
}Comparison with JSON utilities
htmlContent vs jsonContent
| Aspect | htmlContent() | jsonContent() |
|---|---|---|
| Content type | text/html | application/json |
| Schema | z.string() | Caller-supplied Zod schema |
| Use case | HTML pages, JSX rendering | API responses, structured data |
htmlResponse vs jsonResponse
| Aspect | htmlResponse() | jsonResponse() |
|---|---|---|
| Success type | text/html (200) | application/json (200) |
| Error type | application/json (4xx | 5xx) | application/json (4xx | 5xx) |
| Use case | Server-rendered web pages | REST APIs |
See Schema Utility for jsonContent/jsonResponse.
Route definition choices
defineJSXRoute is the recommended way to register a JSX route because it fills in a default HTML 200 response automatically. Two equivalent alternatives exist when you need more control:
| Approach | When to use |
|---|---|
this.defineJSXRoute({ configs, handler }) | Default choice - HTML response shape is inferred, override responses[200] only if you need a custom description |
this.defineRoute({ configs: { responses: htmlResponse({ description }) }, handler }) | You want the full responses object built explicitly via htmlResponse(), without relying on the JSX default merge |
this.bindRoute({ configs }).to({ handler }) | Fluent two-step registration, same configs shape as either of the above |
Authentication and authorization on a JSX route use the same configs.authenticate / configs.authorize fields as JSON routes - see Controllers.
See also
- JSX/HTML overview - introduction and common tasks
- Schema Utility -
jsonContent,jsonResponse, and the wider response-helper family - Controllers -
defineRoute,bindRoute, route configuration, authentication - External: Hono JSX Documentation