On a server
The renderer returns one string, synchronously. There is nothing to await and no framework integration to install — set the content type and send it.
import { Hono } from 'hono';
import { frame } from '@itsy/html/frame';
const app = new Hono();
app.get('/orders', (c) => {
const view = frame({ lang: 'en', title: 'Orders', content: Orders(data) });
return c.html(view.markup);
});import Fastify from 'fastify';
import { frame } from '@itsy/html/frame';
const app = Fastify();
app.get('/orders', (_req, reply) => {
const view = frame({ lang: 'en', title: 'Orders', content: Orders(data) });
reply.type('text/html; charset=utf-8').send(view.markup);
});import { createServer } from 'node:http';
import { frame } from '@itsy/html/frame';
createServer((_req, res) => {
const view = frame({ lang: 'en', title: 'Orders', content: Orders(data) });
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(view.markup);
}).listen(3000);Send view.markup
Html is an object. Express, Fastify and Koa send an object as JSON, and their types accept it, so nothing flags it before a request does. What each one does.
The page is one string
There is no streaming and no partial flush. frame() builds the whole document and hands it over. For most pages that is the simpler trade: no suspense boundaries, no out-of-order chunks, and the Content-Length is known.
To send a shell before the data is ready, send two responses — a fast page and a fetch — rather than trying to split a template.
A nonce per request
frame({ nonce }) adds the nonce to every script and style entry that has none of its own.
app.get('/orders', (c) => {
const nonce = crypto.randomUUID();
c.header('content-security-policy', `script-src 'nonce-${nonce}'; object-src 'none'`);
return c.html(frame({ lang: 'en', title: 'Orders', content, scripts, nonce }).markup);
});The library never writes an inline event handler, so a policy with no unsafe-inline for scripts works as-is. See what it does not do.
Check the output in development
import { check } from '@itsy/html/check';
if (process.env.NODE_ENV !== 'production') {
for (const p of check(view)) console.warn(`[${'rule' in p ? p.rule : `html ${p.code}`}] ${p.message}`, p.near);
}In the production build check() returns [], so the guard above is about skipping the loop, not about safety. Leaving the call in unguarded is fine.