Server Rendering
Render pages on the server for instant loads and SEO.
renderToString
The simplest way to render on the server. Pass a virtual node tree and get back a complete HTML string synchronously. Components are called as functions, event handlers are stripped, and all attributes are properly escaped:
import { renderToString } from 'what-framework/server';
function Greeting({ name }) {
return (
<div class="greeting">
<h1>Hello, {name}!</h1>
<p>Welcome to the site.</p>
</div>
);
}
const html = renderToString(
<Greeting name="Alice" />
);
console.log(html);
// <div class="greeting"><h1>Hello, Alice!</h1><p>Welcome to the site.</p></div>
Use this in your server handler to return fully rendered HTML. It handles text nodes, arrays, nested components, void elements (like <img> and <br>), and properly escapes all attribute values and text content to prevent XSS.
renderToStream
For large pages, streaming sends HTML to the browser as it is generated instead of waiting for the entire page to finish. renderToStream returns an async generator that yields HTML chunks:
import { renderToStream } from 'what-framework/server';
// In your server handler (Node.js example)
app.get('/', async (req, res) => {
res.setHeader('Content-Type', 'text/html');
const stream = renderToStream(
<App url={req.url} />
);
for await (const chunk of stream) {
res.write(chunk);
}
res.end();
});
Streaming is especially powerful with async components. If a component returns a Promise, renderToStream awaits it and continues streaming once the data is ready. The browser can start painting the page header while the database query for the content is still running.
definePage
Use definePage to declare how a page should be rendered. It supports four modes:
static(default) -- Pre-render at build time. No JavaScript shipped. Best for content that rarely changes.server-- Render on every request. Use for personalized or dynamic pages that depend on cookies, headers, or real-time data.client-- Render entirely in the browser as a single-page app. The server sends a minimal HTML shell.hybrid-- Static HTML shell with interactive islands. The best of both worlds for content-heavy pages with a few interactive widgets.
import { definePage } from 'what-framework/server';
// Static blog post — built once at deploy time
export default definePage({
mode: 'static',
component: BlogPost,
title: 'My First Post',
meta: { description: 'An introduction to What Framework.' },
});
// Server-rendered dashboard — fresh data on each request
export default definePage({
mode: 'server',
component: Dashboard,
title: 'Dashboard',
});
// Hybrid product page — static shell, interactive cart island
export default definePage({
mode: 'hybrid',
component: ProductPage,
title: 'Widget Pro',
islands: ['AddToCart', 'ReviewSection'],
});
Server Components
Mark a component as server-only with server(). Server components render on the server and send pure HTML to the client. No JavaScript for these components is ever shipped to the browser:
import { server } from 'what-framework/server';
const UserProfile = server(function({ userId }) {
// This code only runs on the server
// Safe to access databases, env vars, secrets
const user = db.users.find(userId);
return (
<div class="profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
});
Server components are ideal for data-heavy content that does not need client-side interactivity: user profiles, product details, article bodies, admin tables.
Server Actions
Server actions let you define server-side functions that can be called directly from client code. Define an action on the server, and the client calls it via an automatic RPC mechanism over fetch:
import { action } from 'what-framework/server';
// Define a server action
const saveUser = action(async (data) => {
const user = await db.users.create(data);
return { success: true, id: user.id };
}, {
onSuccess: (result) => console.log('Saved:', result.id),
revalidate: ['/users'],
});
useAction
The useAction hook wraps a server action with reactive state for pending status, errors, and response data:
import { useAction } from 'what-framework/server';
function SaveButton() {
const { trigger, isPending, error, data } = useAction(saveUser);
return (
<div>
<button
onClick={() => trigger({ name: 'Alice', email: 'alice@example.com' })}
disabled={isPending()}
>
{isPending() ? 'Saving...' : 'Save User'}
</button>
{error() && <p class="error">{error().message}</p>}
{data() && <p class="success">Saved with ID: {data().id}</p>}
</div>
);
}
useMutation
For simpler cases where you just need pending/error/data tracking around any async function, use useMutation:
import { useMutation } from 'what-framework/server';
const { mutate, isPending, error, data, reset } = useMutation(
async (id) => {
const res = await fetch(`/api/items/${id}`, { method: 'DELETE' });
return res.json();
},
{
onSuccess: () => console.log('Deleted'),
onError: (err) => console.error(err),
onSettled: () => console.log('Done'),
}
);
// Call the mutation
mutate(42);
useOptimistic
Show the user an instant result while the server processes the real mutation. If the server call fails, the optimistic update is automatically rolled back:
import { useOptimistic } from 'what-framework/server';
function LikeButton({ postId, initialCount }) {
const likes = useOptimistic(
initialCount,
// Reducer: how to apply an optimistic action
(current, action) => action === 'like' ? current + 1 : current - 1
);
async function handleLike() {
// Shows +1 immediately, rolls back on error
await likes.withOptimistic('like', async () => {
const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
const data = await res.json();
return data.count; // Server's authoritative count
});
}
return (
<button onClick={handleLike}>
Like ({likes.value()})
</button>
);
}
The withOptimistic helper applies the optimistic action immediately, runs the async function, and either resolves with the server value or rolls back to the previous state on error.
CSRF Protection
Server actions include built-in CSRF protection. What provides three utilities for token management and a fail-closed default that prevents silent security vulnerabilities:
import {
generateCsrfToken,
validateCsrfToken,
csrfMetaTag,
handleActionRequest,
} from 'what-framework/server';
// Server: generate a token per session
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCsrfToken();
}
next();
});
// Server: inject the token into your HTML
const html = `
<head>
${csrfMetaTag(req.session.csrfToken)}
</head>
`;
// Renders: <meta name="what-csrf-token" content="abc-123...">
// Server: handle action requests with CSRF validation
app.post('/__what_action', async (req, res) => {
const { actionId, args } = req.body;
const result = await handleActionRequest(req, actionId, args, {
csrfToken: req.session.csrfToken,
});
res.status(result.status).json(result.body);
});
handleActionRequest uses fail-closed semantics: if you forget to pass a CSRF token, it returns a 500 error explaining the misconfiguration rather than silently accepting the request. Token comparison uses constant-time string matching to prevent timing attacks. The client reads the token from the <meta> tag automatically and sends it with every action request.
Server Runtime Required
Server actions need a server runtime to handle the /__what_action endpoint. They work with Node.js, Deno, and Bun. For purely static sites without a server, use client-side fetch calls to your API instead of server actions.
Static Site Generation
Use generateStaticPage to pre-render pages at build time. It calls renderToString internally and wraps the output in a full HTML document with the configured title, meta tags, styles, and island scripts:
import { generateStaticPage, definePage } from 'what-framework/server';
const page = definePage({
mode: 'hybrid',
component: BlogPost,
title: 'My Post',
meta: { description: 'A great article.' },
styles: ['/styles/blog.css'],
islands: ['CommentSection'],
});
const html = generateStaticPage(page, { slug: 'my-post' });
// Full HTML document with <!DOCTYPE html>, <head>, island hydration script
For hybrid pages with islands, the generated HTML includes an automatic <script type="module"> that imports and calls hydrateIslands(). For static pages, no script tags are included at all.