| outline | deep |
|---|
Use custom SSR to choose your own router, data loading, HTML shell, and hydration code. If you want Void to handle these for you, use Pages Routing.
Start with a root component. This React example renders a page based on the request path:
// src/App.tsx
export default function App({ url }: { url: string }) {
return url === '/about' ? <main>About</main> : <main>Home</main>;
}Create both entry files:
src/main.ssr.tsorsrc/main.ssr.tsxsrc/main.client.tsorsrc/main.client.tsx
Use one server entry and one client entry. Both are required.
Wrap your server renderer with defineRender() and return an HTML response. Insert assetTags.css and assetTags.preloads in <head>, and 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' } },
);
});You can also export a named render(c, assetTags) function with the same signature. See defineRender for the types.
In the client entry, hydrate the same component:
// src/main.client.tsx
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root')!, <App url={window.location.pathname} />);See Revalidation for stale-while-revalidate caching of SSR pages.
API routes and static files are served before custom rendering. Unmatched non-API requests use render(c, assetTags).