Custom SSR
Void supports framework-agnostic SSR via explicit server and client entries. This is for advanced use cases where you want full control over rendering and hydration.
TIP
For most apps, Pages Routing handles SSR automatically. You do not need entry files or hydration code.
Required entries
SSR mode is enabled when both of these exist:
src/main.ssr.tsorsrc/main.ssr.tsxsrc/main.client.tsorsrc/main.client.tsx
Only one server entry and one client entry may exist. If only one side is present, build/deploy fails with a clear error.
Render API
src/main.ssr.ts(x) must export either:
render(c: CloudContext, assetTags: RenderAssetTags): Response | Promise<Response>or:
export default defineRender((c, assetTags) => Response | Promise<Response>);The recommended form is defineRender(...) for inferred types.
assetTags contains the HTML tags for your client assets:
{
head: string; // styles/modulepreload/Vite client+preamble (dev)
body: string; // main client entry script tag
}If no render export is found, build/deploy fails with a clear error.
Example:
import { renderToString } from 'react-dom/server';
import { defineRender } from 'void';
import App from './App';
export default defineRender(async (c, assetTags) => {
const url = new URL(c.req.raw.url);
const html = renderToString(<App url={url.pathname} />);
return new Response(
`<!doctype html>
<html>
<head>${assetTags.css}${assetTags.preloads}</head>
<body>
<div id="root">${html}</div>
${assetTags.body}
</body>
</html>`,
{ headers: { 'content-type': 'text/html; charset=utf-8' } },
);
});src/main.client.ts(x) should hydrate/mount your app:
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root')!, <App url={window.location.pathname} />);Client Asset Injection
Void no longer mutates your rendered HTML automatically. You decide whether and where to inject client asset tags.
The assetTags values are computed by Void:
- In production: from
dist/client/.vite/manifest.json(entry script, CSS, modulepreload) - In dev: includes Vite HMR client and React refresh preamble (when React plugin is active), plus the client entry script
Caching
See Revalidation for stale-while-revalidate caching of SSR pages.
Request flow
With SSR enabled:
/api/*requests go to worker API routes- static asset hits are served from R2
- unmatched non-API requests fall back to
render(c, assetTags)
Without SSR entries, non-API requests keep SPA static fallback behavior.