Glamour

Zero-flash SSR + claim for Lustre/Gleam on the BEAM.

Glamour renders your Lustre view on the server, serialises the exact model, fingerprints the markup, and claims the live DOM on the client without wiping it. If the fingerprint changes, the client falls back to hydrate and logs helpful errors. The goal is a two-line upgrade: one call on the server, one on the client, no flash between the first paint and the claimed DOM.

Project home: https://github.com/thirdreplicator/glamour


Why Glamour


Installation

Incubator workflow (inside this repo)

  1. Add Glamour as a path dependency (already configured here):
    # gleam.toml
    [dependencies]
    glamour = { path = "../lib/glamour" }
    
  2. Fetch dependencies and build:
    gleam deps download
    gleam build --target erlang
    

Future Hex package (roadmap)

Once published, replace the path dependency with a semantic version:

[dependencies]
glamour = "~> 0.1"

Usage

1. Describe your Lustre app

Create an application spec that tells Glamour how to render, serialise, and decode your model. The spec assumes the Lustre app’s start_args type matches the model.

import glamour/app
import lustre
import lustre/element/html as html
import gleam/json
import gleam/dynamic/decode as decode

pub type Model {
  Model(count: Int)
}

fn init(model: Model) -> Model { model }
fn update(model: Model, _msg) -> Model { model }
fn view(Model(count:)) -> html.Node { html.text(int.to_string(count)) }

fn encode_model(Model(count:)) -> json.Json {
  json.int(count)
}

fn decode_model() -> decode.Decoder(Model) {
  decode.map(decode.int, Model)
}

pub fn spec() -> app.Spec(Model, msg) {
  let lustre_app = lustre.simple(init, update, view)
  app.new(lustre_app, view, encode_model, decode_model())
  |> app.with_selector("#app")            // optional, default is "#app"
  |> app.with_state_script_id("glamour-state") // optional, default is "glamour-state"
}

2. Render on the server

Call glamour/server.render/3 inside your HTTP handler and return the HTML string it produces. The helper embeds the SSR fragment, serialised state, fingerprint, and client script tags.

import glamour/server
import gleam/option

pub fn render_page(model) -> server.Rendered {
  let spec = spec()
  let options =
    server.default_options()
    |> with_glamour_scripts()
    |> server.Options(..)
    |> option.Some

  server.render(spec, model, options)
}

fn with_glamour_scripts(options: server.Options) -> server.Options {
  server.Options(
    ..options,
    title: option.Some("Dashboard"),
    client_scripts: ["/assets/glamour/main.mjs"],
    head: [
      ..options.head,
      "    <link rel=\"stylesheet\" href=\"/assets/app.css\">\n",
    ],
  )
}

server.Options fields:

The returned Rendered(html) contains a complete HTML document. Render failures bubble up as normal BEAM errors.

3. Claim on the client

Bundle a small entry point that calls glamour/client.claim/2 (or /3 with options) for each page that should reuse the SSR DOM.

import glamour/client
import glam_app/dashboard
import gleam/option

@target(javascript)
pub fn main() -> Nil {
  case client.claim(dashboard.spec(), option.None) {
    Ok(_) -> Nil
    Error(error) -> handle_error(error)
  }
}

client.Options currently supports strict mode. When strict: True, a fingerprint mismatch logs an error and skips hydration; otherwise the client logs a warning and hydrates.

4. Ship the JavaScript bundle

  1. Compile the JS target:
    gleam build --target javascript
    
  2. Copy the generated module(s) into your static assets directory. For example:
    mkdir -p priv/static/assets/glamour
    cp build/dev/javascript/myapp/myapp/glamour/client_bundle.mjs priv/static/assets/glamour/main.mjs
    rsync -a build/dev/javascript/glamour priv/static/assets/glamour/
    
    Adjust the paths to match your app name and deployment pipeline.
  3. Reference the bundle in the client_scripts list so the browser loads it:
    <script type="module" src="/assets/glamour/main.mjs"></script>
    

Source Layout


Contracts & Assumptions


Error Handling

Use the error constructors in tests to assert the correct behaviour.


Conventions


Example: MyApp Admin

A typical integration renders admin routes with Glamour:

Run the developer server with:

gleam run --target erlang --module main -- server

Testing

Server-side tests:

gleam test --target erlang

Client-side behaviour can be smoke-tested by loading the bundled app in a browser; upcoming work will add harness tests around client.claim.


Roadmap


Released under the MIT licence.

Search Document