SSR framework for Svelte 5 + Bun with islands-based selective hydration
transformPage
Pass transformPage to resolve(event, { transformPage }) inside a Handle to rewrite the rendered HTML before it ships. It runs once per response, only on text/html bodies, with the full HTML string and done: true.
// file: src/hooks.ts
import type { Handle } from 'mochi-framework/hooks';
const greeting: Handle = async ({ event, resolve }) => {
return resolve(event, {
transformPage({ html }) {
return html.replace('{{app.greeting}}', 'Welcome to Mochi!');
},
});
};<!-- file: src/shell.html -->
<body>
<header>{{app.greeting}}</header>
{{mochi.body}}
</body>The callback receives { html, done } and returns string | undefined | Promise<string | undefined>. Returning undefined replaces the body with an empty string.
const banner: Handle = async ({ event, resolve }) => {
return resolve(event, {
async transformPage({ html }) {
const message = await fetchBannerMessage();
return html.replace('{{app.banner}}', message);
},
});
};Use it for per-request mutations the shell template can’t express on its own — request-aware <html lang>, nonce injection, A/B placeholder swaps:
const lang: Handle = async ({ event, resolve }) => {
const locale = event.request.headers.get('accept-language')?.slice(0, 2) ?? 'en';
return resolve(event, {
transformPage({ html }) {
return html.replace('<html', `<html lang="${locale}"`);
},
});
};The callback is invoked once with the full HTML, not streamed chunk-by-chunk; done is always true. Treat it as a whole-document transform.
When sequence() chains multiple handlers, transforms run inner-most first and outer-most last — the handler closest to the route sees the original HTML, the outermost wraps the final result.