# Server-Side Rendering

LiveSvelte supports server-side rendering (SSR) of Svelte components, which provides meaningful HTML on the first paint before the JavaScript bundle loads.

## How SSR Works

When SSR is active, the initial (dead) render calls Node.js to execute Svelte's `render()` function server-side. The result is embedded directly in the HTML response:

1. LiveSvelte calls `SSR.render(component_name, props, slots)`
2. `render()` returns `%{"head" => "<style>...</style>", "html" => "<div>...</div>", "css" => %{"code" => "..."}}`
3. The `head` and CSS styles are included in the page; `html` is placed inside the `data-svelte-target` div
4. When JavaScript loads, the `SvelteHook` hydrates the existing DOM instead of mounting fresh

## SSR Modes

LiveSvelte has two SSR modules for different environments:

### NodeJS Mode (Production)

Uses [`elixir-nodejs`](https://github.com/revelrylabs/elixir-nodejs) to run a pool of Node.js workers that execute the SSR bundle.

```elixir
# config/prod.exs
config :live_svelte,
  ssr_module: LiveSvelte.SSR.NodeJS,
  ssr: true
```

The SSR bundle is built by:
```bash
mix assets.build  # runs phoenix_vite.npm vite build (client + SSR)
```

This produces `priv/svelte/server.js`, which the NodeJS supervisor loads on application start.

### ViteJS Mode (Development)

Forwards SSR requests to the Vite dev server over HTTP. This provides instant HMR without rebuilding the SSR bundle on every change.

```elixir
# config/dev.exs
config :live_svelte,
  ssr_module: LiveSvelte.SSR.ViteJS,
  vite_host: "http://localhost:5173"
```

> #### ViteJS Mode Requires Vite Dev Server {: .warning}
>
> `LiveSvelte.SSR.ViteJS` only works when `mix phx.server` is running alongside the Vite dev server started by Phoenix's watchers. If the Vite server is not running, SSR will silently fall back to client-only rendering.

## Configuration

Enable/disable SSR globally:

```elixir
# config/config.exs
config :live_svelte, ssr: true   # enabled (default)
config :live_svelte, ssr: false  # disabled
```

Select SSR module:

```elixir
config :live_svelte, ssr_module: LiveSvelte.SSR.NodeJS   # production (default)
config :live_svelte, ssr_module: LiveSvelte.SSR.ViteJS   # development
```

## Per-Component SSR Opt-Out

Disable SSR for a specific component:

```heex
<.svelte name="HeavyChart" props={%{data: @data}} socket={@socket} ssr={false} />
```

Components with `ssr={false}` render a loading slot or nothing on the first paint, then mount client-side normally.

## HMR in Development

When running with `LiveSvelte.SSR.ViteJS`, changes to Svelte files trigger automatic hot module replacement. The `SvelteHook` re-mounts affected components without a full page reload.

**With phoenix_vite (recommended):** The layout uses `PhoenixVite.Components.assets`. The Igniter installer adds to your endpoint in `config/dev.exs` a `:vite` watcher and `static_url: [host: "localhost", port: 5173]`, so `mix phx.server` starts the Vite dev server and asset URLs point at it — Svelte and CSS then hot-reload with no extra terminal. See [Installation](installation.md) for the exact config; if you added phoenix_vite or LiveSvelte manually, add that endpoint config yourself.

**Without phoenix_vite:** Use `LiveSvelte.Reload.vite_assets/1` in your layout to include Vite's HMR client and production fallback:

```heex
<LiveSvelte.Reload.vite_assets assets={["/js/app.js"]}>
  <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
  <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
</LiveSvelte.Reload.vite_assets>
```

## Loading Slot

Show content while a component is loading (only when `ssr={false}`):

```heex
<.svelte name="SlowChart" props={%{data: @data}} socket={@socket} ssr={false}>
  <:loading>
    <div class="spinner">Loading chart...</div>
  </:loading>
</.svelte>
```

> #### Loading Slot + SSR Incompatible {: .warning}
>
> The `:loading` slot is mutually exclusive with SSR. Using both together produces a compile warning. If SSR is active, the loading slot is ignored.

## Telemetry

LiveSvelte emits telemetry events for SSR operations:

| Event | When |
|-------|------|
| `[:live_svelte, :ssr, :start]` | SSR render begins |
| `[:live_svelte, :ssr, :stop]` | SSR render completes; includes `duration_microseconds` measurement |
| `[:live_svelte, :ssr, :exception]` | SSR render throws an exception |

Attach handlers for observability:

```elixir
:telemetry.attach(
  "live-svelte-ssr",
  [:live_svelte, :ssr, :stop],
  fn _event, measurements, _metadata, _config ->
    Logger.debug("SSR render took #{measurements.duration_microseconds}µs")
  end,
  nil
)
```

## Testing with SSR

SSR is disabled in the test environment by default. To write tests that verify SSR output, enable it per test suite:

```elixir
defmodule MyAppWeb.SsrTest do
  use MyAppWeb.ConnCase, async: false  # must be async: false

  setup do
    Application.put_env(:live_svelte, :ssr, true)
    on_exit(fn -> Application.put_env(:live_svelte, :ssr, false) end)
    :ok
  end

  test "renders SSR HTML", %{conn: conn} do
    html = conn |> get("/counter") |> html_response(200)
    assert html =~ "data-ssr=\"true\""
  end
end
```

Use `get/2` + `html_response/2` for initial HTML checks — `visit/2` from PhoenixTest connects the socket and transitions past the dead render.

## Production Deployment

See [Deployment](deployment.md) for complete Node.js setup instructions for production.
