ProvenPin Install

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 &amp; B and 1 &lt; 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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.