Chapter 19: SSR, Hydration, and Deployment
⚠ Planned — not in Loup 1.25. This chapter describes the design in the Language Reference (§2-4.7 for server-side rendering, hydration and state serialization, §1-16.6.7–§1-16.6.8 for
SafeHtml, §1-20.6 for the browser build and bootstrap). The toolchain does not implement it yet, so its listings are design sketches and are not verified.
The Reference describes a path to production: render the first page on the server, send HTML the browser can show at once, then hydrate it so the existing DOM becomes reactive without being rebuilt. This chapter starts with what you can build for the browser today.
Building for the browser today
Loup 1.25 compiles a web project to a WebAssembly module, and nothing more. Unlike the rest of this chapter, this section's listing runs today and is checked.
Create a project with loup-bootstrap init shop, set entry = "src/main.lpweb" under [build] in shop/shop.lppkg, and write src/main.lpweb:
@mode(web.compiled)
func productCard(name: string, stock: int): string {
let card = ```(html):(none)
<article class="product">
<h2>{$ name $}</h2>
<p>{$ stock $} in stock</p>
</article>
```;
return card.render({});
}
func main(): void {
println(productCard("Desk lamp", 3));
}
Listing 19-1: A web.compiled entry that renders an html fence
$ loup-bootstrap build --target browser
Resolving dependencies...
Compiling 1 module(s)...
Generating code...
Build complete: …/shop/build/shop.wasm (20311 bytes)
…/shop/build/shop.wasm
$ ls build
shop.wasm
$ loup-launcher --run-bare build/shop.wasm
<article class="product">
<h2>Desk lamp</h2>
<p>3 in stock</p>
</article>
That bare module is the whole output:
--deliveryis the current spelling of--target; both work, and the shape isbrowser(--target webis an unknown delivery shape).--release/--debugpick the profile;-onames the output file.- There is no
dist/,index.html,loup-rt.jsor source map (not even with--debug), noloup serve, and--inspectis an unknown flag. No browser can load the module yet;loup-launcher --run-bareruns it and callsmain()(ruling ENTRY-1). - Compiled Wasm is experimental and not audited; for example, an
i"…"string currently traps under the launcher (Chapter 1). - On Linux x86-64
loupis the self-hosted driver, which refuses Wasm builds; useloup-bootstrap build. On macOS and Windows they are the same program.
The rest of the chapter is the planned design.
Server-side rendering
ui.web.renderToString(view, options) renders a component tree to HTML on the service target and returns SafeHtml (§2-4.7), which an outer html fence inserts without escaping it again (§1-16.6.8):
@mode(web.compiled)
from ui import (View, signal);
from ui.web import (renderToString, RenderToStringOptions);
pub func ProductPage(name: string, stock: int): View {
let count = signal(stock);
return ```(html):(none)
<main>
<h1>{$ name $}</h1>
<p>In stock: {$ count.get() $}</p>
</main>
```;
}
pub func renderProductPage(name: string, stock: int): SafeHtml {
let options = RenderToStringOptions(stateIntegrity = true);
return renderToString(ProductPage(name, stock), options);
}
Listing 19-2: Rendering a component on the server (design sketch)
SafeHtml itself does not exist yet (Chapter 14). The Reference also does not settle which @mode a server module declares: ui.web is listed for the browser and service targets, but service belongs to the host modes, whose UI backend is ui.desktop (§1-2.5.2, §1-2.6).
State serialization and hydration
The server writes the initial signal values into a JSON block for the client:
<script id="__loup_state__" type="application/json" data-integrity="9f86d081884c7d65…">
{"count": 3, "note": "a </script> inside a string is harmless"}
</script>
Every < and > is written as < and >, so the block never contains </ and a string cannot close the <script> element. With stateIntegrity = true the block carries the hex SHA-256 of its content, which hydrate checks before using the state.
On the client, hydrate(view, selector) attaches handlers and signals to the server-rendered DOM instead of creating nodes, and returns a MountHandle. If the markup does not match, the runtime warns and falls back to full client rendering.
@mode(web.scripted)
from ui.web import (hydrate);
from product import (ProductPage);
let handle = hydrate(ProductPage("Desk lamp", 3), "#root");
Listing 19-3: The client entry hydrates the page (design sketch)
Content Security Policy
The minimum policy for a Loup browser app (§2-4.7) is:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
connect-src 'self';
'wasm-unsafe-eval' lets the page compile Wasm without allowing JavaScript eval(). 'unsafe-inline' is unnecessary because event handlers become data-loup-ev attributes (Chapter 16) and state travels as JSON. Set the header in your server or CDN.
The planned build output
The Reference's non-normative description of loup build --target browser (§1-20.6) is a dist/ directory with index.html, a versioned loup-rt.js, app.wasm with .br and .gz copies, app.wasm.map under --debug, and assets/. On page load loup-rt.js would run: instantiate → initialize → register custom elements → wire events → hydrate → mount.
To deploy it, serve .wasm as application/wasm (or streaming compilation is lost), serve the compressed variants, preload app.wasm, cache the versioned files as immutable, and serve source maps as application/json with Access-Control-Allow-Origin: *. loup serve would rebuild on change and, with --inspect, start the browser debug bridge, which must never reach production.
Summary
- Today
loup-bootstrap build --target browser(alias--delivery) writes onebuild/<name>.wasmthat runs underloup-launcher --run-bare; there is nodist/, HTML shell,loup-rt.js, source map,loup serveor--inspect. - Planned:
renderToStringturns aViewintoSafeHtmlon theservicetarget;hydrateattaches reactivity to that HTML on the client. - Server state travels in a
__loup_state__JSON block with<and>escaped, optionally guarded by a SHA-256data-integrityhash. - The minimum CSP needs
'wasm-unsafe-eval'and no'unsafe-inline'. - The planned
dist/layout, bootstrap order and serving rules are in Language Reference §1-20.6.