Custom SSR
Use custom SSR when you want to choose your own router, data loading, HTML shell and hydration. If you'd rather the SDK handle these, use Pages routing.
On this pageCreate a custom SSR app · Add the required entry files · Deploy it
Create the app
This step creates a root component. The React example renders a page from the request path.
// src/App.tsx
export default function App({ url }: { url: string }) {
return url === '/about' ? <main>About</main> : <main>Home</main>;
}Required entries
This step adds the two entry files.
src/main.ssr.tsorsrc/main.ssr.tsxsrc/main.client.tsorsrc/main.client.tsx
You need exactly one server entry and one client entry.
Render API
This step renders HTML on the server. Wrap the renderer in defineRender() and return an HTML response. Put assetTags.css and assetTags.preloads in <head>. Put assetTags.body before </body>.
// src/main.ssr.tsx
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' } },
);
});A named render(c, assetTags) export with the same signature also works. See defineRender for the types.
Hydrate in the browser
This step hydrates the same component in the client entry.
// src/main.client.tsx
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root')!, <App url={window.location.pathname} />);Caching
See Revalidation for stale-while-revalidate caching of SSR pages.
API routes and static files are served first. Other non-API requests go to render(c, assetTags).