🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Custom HTML shell

htmlShell overrides the default <!doctype html> wrapper that Mochi renders pages into. Pass either a path to an .html file (detected by the .html suffix) or the template string directly:

// file: src/index.ts
import { Mochi } from 'mochi-framework';

await Mochi.serve({
  htmlShell: './src/shell.html',
  routes: {
    '/': Mochi.page('./src/Home.svelte'),
  },
});

The default shell lives at packages/mochi/src/templates/default-shell.html — copy it as a starting point.

Placeholders

A custom shell must contain four placeholders. Each is replaced once per request by Mochi.resolveHtmlShell():

PlaceholderReplaced with
{{mochi.head}}<svelte:head> output plus the warning-collector bootstrap script.
{{mochi.css}}Island-display rules, island-failure styles, and <link rel="stylesheet"> tags for compiled CSS.
{{mochi.body}}Rendered component HTML, debug-info script, dev toolbar mount, dev error overlay, error-report script.
{{mochi.script}}Hydration bootstrap <script type="module">, server-island loader, debug bar, and dev live-reload.

Example: minimal shell.

<!-- file: src/shell.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    {{mochi.head}} {{mochi.css}}
  </head>
  <body>
    {{mochi.body}} {{mochi.script}}
  </body>
</html>

Do NOT omit a placeholder — every required asset for that slot is silently dropped. Skip {{mochi.script}} and hydration, server islands, the debug bar, and dev live-reload all stop working; skip {{mochi.css}} and component styles never load; skip {{mochi.body}} and the page renders blank.

When to customize

  • Add <meta>, <link rel="icon">, analytics snippets, or third-party scripts site-wide.
  • Set lang, dir, or class names on <html> / <body> for theming.
  • Inline a fonts preload or a CSP <meta> tag.

For per-request HTML mutations (nonces, A/B class names, ENV-derived strings) use transformPage inside a handle:

// file: src/hooks.ts
import type { Handle } from 'mochi-framework';

export const handle: Handle = ({ event, resolve }) =>
  resolve(event, {
    transformPage: ({ html }) => html.replace('%nonce%', event.locals.nonce),
  });

Do NOT read request data inside htmlShell — it is loaded once at startup; instead, inject per-request values via transformPage.