Skip to content

Server-Side Rendering

@whisq/ssr lets you render Whisq components to HTML strings on the server. The browser receives pre-rendered HTML, then Whisq hydrates it into an interactive app on the client.

Install the SSR package:

Terminal window
npm install @whisq/ssr

Use renderToString() to convert a component tree to HTML:

import { renderToString } from "@whisq/ssr";
import { div, h1, p, ul, li } from "@whisq/core";
const Page = () =>
div(
h1("Welcome"),
p("This page was rendered on the server."),
ul(
li("Fast initial load"),
li("SEO friendly"),
li("Works without JavaScript"),
),
);
const html = renderToString(Page());
// <div><h1>Welcome</h1><p>This page was rendered on the server.</p>...</div>

Serve a Whisq app from an Express server:

import express from "express";
import { renderToString } from "@whisq/ssr";
import { component, div, h1, p } from "@whisq/core";
const App = component(() =>
div(
h1("My App"),
p("Server-rendered with Whisq"),
)
);
const app = express();
app.get("*", (req, res) => {
const content = renderToString(App({}));
res.send(`
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="app">${content}</div>
<script src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000);

On the client, call mount() on the same root element. Whisq attaches event handlers to the existing HTML:

client.ts
import { mount } from "@whisq/core";
import { App } from "./app";
mount(App({}), document.getElementById("app"));

The user sees the server-rendered content immediately, and interactivity kicks in once the client JavaScript loads.

  • Reactive values are evaluated once — signals are read at render time and their current value is used in the HTML output.
  • Event handlers are strippedonclick, oninput, etc. don’t appear in the server HTML. They’re attached during hydration.
  • HTML is properly escaped — attribute values and text content are escaped to prevent XSS.
Use SSR when…Use client-only when…
SEO matters (public pages, blogs)App is behind a login
Fast first paint is criticalContent is fully dynamic
Users have slow connectionsApp is a dashboard or tool
Content doesn’t change per-userReal-time data is the focus

Fetch data on the server before rendering:

app.get("/users", async (req, res) => {
const users = await fetch("https://api.example.com/users")
.then(r => r.json());
const Page = () =>
div(
h1("Users"),
ul(users.map(u => li(u.name))),
);
const html = renderToString(Page());
res.send(wrapInShell(html));
});
  • Routing — Server-render different pages based on URL
  • Data Fetching — Client-side data loading after hydration
  • Testing — Test server-rendered output