preact render to string SSR: working versions (tested 2026-10-17)
Server-render preact components with preact-render-to-string, covering props, useState initial values, conditional output, keyed lists and dangerouslySetInnerHTML while asserting exact escaped HTML offline.
passed6 passed · 0 failed2026-10-17 19:19 UTC- Status
- passed
- Last passed
- 2026-10-17 19:19 UTC
- Tests
- 6 / 6
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
preact@11.0.1preact-render-to-string@6.8.1@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 39061946720
The code
Exactly the files that passed. Your agent can pull them with get_recipe("preact-render-to-string-ssr") (install), or see them on GitHub.
package.json
{
"name": "preact-render-to-string-ssr",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"preact": "11.0.1",
"preact-render-to-string": "6.8.1"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/app.ts
import { h, type FunctionComponent } from "preact";
import { useState } from "preact/hooks";
import { renderToString } from "preact-render-to-string";
export type User = {
id: string;
name: string;
};
export const Greeting: FunctionComponent<{ name: string }> = ({ name }) =>
h("h1", null, `Hello, ${name}!`);
export const Counter: FunctionComponent<{ start?: number }> = ({ start = 0 }) => {
const [count] = useState(start);
return h("p", { class: "count" }, `Count: ${count}`);
};
export const UserList: FunctionComponent<{ users: User[] }> = ({ users }) =>
h(
"ul",
null,
users.map((user) => h("li", { key: user.id }, user.name)),
);
export const Banner: FunctionComponent<{ html: string }> = ({ html }) =>
h("div", { class: "banner", dangerouslySetInnerHTML: { __html: html } });
export const Page: FunctionComponent<{ user: User | null; users: User[] }> = ({
user,
users,
}) =>
h(
"main",
null,
user ? h(Greeting, { name: user.name }) : h("p", null, "Please sign in"),
h(Banner, { html: "<strong>Welcome</strong>" }),
h(UserList, { users }),
);
export function renderPage(user: User | null, users: User[]): string {
return renderToString(h(Page, { user, users }));
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { h } from "preact";
import { renderToString } from "preact-render-to-string";
import {
Banner,
Counter,
Greeting,
renderPage,
UserList,
type User,
} from "../src/app.ts";
test("renders a component with props", () => {
assert.equal(renderToString(h(Greeting, { name: "Ada" })), "<h1>Hello, Ada!</h1>");
});
test("renders useState initial value", () => {
assert.equal(
renderToString(h(Counter, { start: 5 })),
'<p class="count">Count: 5</p>',
);
assert.equal(renderToString(h(Counter, {})), '<p class="count">Count: 0</p>');
});
test("escapes text content", () => {
assert.equal(
renderToString(h(Greeting, { name: "A & B and 1 < 2" })),
"<h1>Hello, A & B and 1 < 2!</h1>",
);
});
test("renders keyed lists", () => {
const users: User[] = [
{ id: "1", name: "Ada" },
{ id: "2", name: "Bob" },
];
assert.equal(renderToString(h(UserList, { users })), "<ul><li>Ada</li><li>Bob</li></ul>");
assert.equal(renderToString(h(UserList, { users: [] })), "<ul></ul>");
});
test("renders dangerouslySetInnerHTML without escaping", () => {
assert.equal(
renderToString(h(Banner, { html: "<em>hi</em> & <b>bye</b>" })),
'<div class="banner"><em>hi</em> & <b>bye</b></div>',
);
});
test("renders conditional branches", () => {
assert.equal(
renderPage(null, []),
'<main><p>Please sign in</p><div class="banner"><strong>Welcome</strong></div><ul></ul></main>',
);
assert.equal(
renderPage(
{ id: "1", name: "Ada" },
[
{ id: "1", name: "Ada" },
{ id: "2", name: "Bob" },
],
),
'<main><h1>Hello, Ada!</h1><div class="banner"><strong>Welcome</strong></div><ul><li>Ada</li><li>Bob</li></ul></main>',
);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.