🍡 mochi
An experimental SSR framework for Svelte 5 and Bun.
Render everything on the server; ship JavaScript only where it earns its place.
Quick start
Documentation
Setup, hydration modes, routes, hooks, forms, cookies — everything in one place.
Start reading →Demos
Each demo lives on its own page. Pick one below to see the feature in isolation.
Basic
Hello World
The simplest possible Mochi page — pure server-rendered Svelte.Hydration Modes
The same component rendered five ways — eager, lazy, visible, rootMargin-tuned, and deferred server island.View Transitions
Drop <ViewTransitions /> into a shared layout to animate full-page navigations with zero JavaScript.Custom Transitions
Bring your own @keyframes to <ViewTransitions /> via custom={{ in, out }} — here, a funky 3D spin.Shared State
Two separate islands sharing the same reactive $state.Server Islands
Components marked mochi:defer render server-side on demand after the initial page is delivered.Crossing the server-client boundary with props
How props travel from a server-rendered parent into a hydrated island — Date, Map, Set, BigInt, URL, typed arrays, and even cyclic refs survive devalue’s round-trip.Lazy Islands
Islands marked mochi:hydrate:visible hydrate and load their CSS only when scrolled into view.Lazy Server Islands
Server islands marked mochi:defer:visible only fetch when the wrapper scrolls into view.Font loading
Ship fonts via @fontsource packages or standalone .woff2 files — automatically bundled and linked from the page head.MdSvex
A .md file compiled through mdsvex and rendered as a Svelte component, with an embedded <script> block.Nested Components
A five-level recursive tree — hydrating the root carries the whole subtree in one island.Shared Props
Nine islands, three unique payloads — each set serialized once and referenced via props-ref.Unique IDs
Svelte's native $props.id() inside islands — SSR-consistent, unique per instance, namespaced in server islands.Data & serialization
Server Props
Define serverProps on Mochi.page() to pass fresh data into a Svelte page on every request.Data Loading
Server-side fetch from PokéAPI cached via MochiCache and rendered at request time.Hydratable
Compute a value once on the server with hydratable(); the hydrated island reads it from <head> instead of re-running the async work.Cookies
Read and write cookies on the server and the client through one MochiCookieJar API.Isomorphic URL
One import for the current URL — reads from the request on the server, window.location on the client.Cache Events
Subscribe to MochiCache lifecycle events through mochiEvents and log them to the server console.Request ID
Every request gets a UUID v7 — read it server-side via getRequestContext().requestId; the same id rides every lifecycle event for correlation.Cookie Vary Test
A page that sets Vary: Cookie on its response — useful for testing cookie-partitioned cache keys.Endpoints & realtime
Real-time Chat
A hydrated island over a Mochi.ws() route, with pub/sub broadcast and in-memory history.API Endpoints
JSON routes defined with Mochi.api(), tested live against the running server.File Routes
Serve a file from disk with Mochi.file() — static path or a per-request resolver.Real-time Streams
WebSocket and SSE clocks, lazily hydrated via mochi:hydrate:visible.Forms
Form Actions
A login form rendered twice — plain HTML POST and intercepted with {@attach enhance(...)}.Using form return data
An action returns data via success({...}); {@attach enhance(...)} updates the UI in place, plain HTML re-renders the page.Form Errors
A thrown action error shown inline via {@attach enhance(...)}, or as the Mochi error page on plain submit.Form Redirects
redirect(303, …) intercepted as a JSON envelope by {@attach enhance(...)}, or followed natively by the browser.File Uploads via form actions
multipart/form-data submission, validated with fail() and success(), shown enhanced and plain.Reloading associated form data
After a successful submit, refetch the related list inside enhance() — or rely on the post-POST re-render.Cancelling form submissions
cancel() prevents the fetch from firing; controller.abort() stops one mid-flight.