The Loup Web Book — contents
  1. Chapter 1: Getting Started
  2. Chapter 2: The Web Modes
  3. Chapter 3: Variables and Data Types
  4. Chapter 4: Functions
  5. Chapter 5: Control Flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and Pattern Matching
  9. Chapter 9: Classes and Objects
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error Handling
  13. Chapter 13: Modules and Packages
  14. Chapter 14: Template Fences — HTML and CSS
  15. Chapter 15: Async and Concurrency
  16. Chapter 16: Reactive UI
  17. Chapter 17: Web Components
  18. Chapter 18: DOM, Storage, Workers, and JavaScript Interop
  19. Chapter 19: SSR, Hydration, and Deployment
  20. Chapter 20: Memory Safety in Web Apps

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:

  • --delivery is the current spelling of --target; both work, and the shape is browser (--target web is an unknown delivery shape). --release/--debug pick the profile; -o names the output file.
  • There is no dist/, index.html, loup-rt.js or source map (not even with --debug), no loup serve, and --inspect is an unknown flag. No browser can load the module yet; loup-launcher --run-bare runs it and calls main() (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 loup is the self-hosted driver, which refuses Wasm builds; use loup-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):

Fragment — not a complete program
@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.

Fragment — not a complete program
@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 one build/<name>.wasm that runs under loup-launcher --run-bare; there is no dist/, HTML shell, loup-rt.js, source map, loup serve or --inspect.
  • Planned: renderToString turns a View into SafeHtml on the service target; hydrate attaches reactivity to that HTML on the client.
  • Server state travels in a __loup_state__ JSON block with < and > escaped, optionally guarded by a SHA-256 data-integrity hash.
  • 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.