Request entries
CAUTION
Experimental — not for production. Managed Data Mode SSR is available for evaluation and testing. Do not use it in production yet; the API and behaviour may change. In the meantime, continue using Webpack SSR.
SSR apps have three entry modules. Server and client each export default an object from defineServerEntry / defineClientEntry. Routes export a named routes array — see Routing.
| Entry | Default path | Role |
|---|---|---|
| Server | src/server.tsx | Server setup (onListen, middleware) |
| Client | src/client.tsx | Hydrate-time setup |
| Routes | src/routes.tsx | Route tree — see Routing |
Server entry
Start from the template shape — middleware only is enough for many apps:
// src/server.tsx
import { defineServerEntry } from 'sku/runtime';
const server = defineServerEntry({
middleware: [
(req, res, next) => {
if (req.path === '/api/health') {
res.status(200).type('text/plain').send('ok');
return;
}
next();
},
],
});
export default server;You’re set up when the file default-exports a defineServerEntry({ … }) object.
Add getters when you need them. They run after consumer Express middleware and before React Router handles the document request:
Which channel to use is documented under Data loading → Three value channels.
Early getters (getSite, getLanguage, getClientContext) receive { req } (the Express request). Later getters also receive already-resolved sibling values so you can project instead of re-deriving.
getSite
Resolves the active site name for this request. Required when config has more than one site; omit on single-site apps.
getSite?: (args: { req: ExpressRequest }) => Site;getSite({ req }) {
return req.get('x-site') === 'nz' ? 'nz' : 'au';
},Use useSite() in the app. See Routing → Multi-site.
getLanguage
Resolves the language for Document vocab chunk registration. Return a name from config languages (or en-PSEUDO).
getLanguage?: (args: { req: ExpressRequest }) => Language;getLanguage({ req }) {
return req.path.startsWith('/th') ? 'th-TH' : 'en';
},See Multi-language.
getClientContext
JSON-serialisable content sent to the client and available via useClientContext(). See Three value channels for serialisation and nested undefined rules.
getClientContext?: (args: { req: ExpressRequest }) => ClientContext | Promise<ClientContext>;getClientContext({ req }) {
return {
theme: 'dark',
userId: req.user?.id ?? null,
};
},getReactContext
Server-specific values for React via useReactContext() (for example API clients).
getReactContext?: (args: {
req: ExpressRequest;
site: Site;
clientContext: ClientContext | undefined;
}) => ReactContext | Promise<ReactContext>;Pair with a client getReactContext when values differ by environment — see Providers and Three value channels.
middleware
Express middleware run before SSR for each request. See Middleware.
middleware?: RequestHandler[];onListen
Called once after middleware and HTML are mounted and listen succeeds (both sku start and production). Use it for keep-alive timeouts, readiness logging with the bound port, or rare Express knobs. It is not re-fired on server-entry HMR. Omit if you do not need a post-listen hook.
For a single reverse-proxy hop, prefer config expressTrustProxy (hop count 1). Override other trust-proxy values here via app.set('trust proxy', …).
onListen?: (args: {
app: Express;
httpServer: http.Server | https.Server;
port: number;
}) => void | Promise<void>;defineServerEntry({
onListen({ app, httpServer, port }) {
httpServer.keepAliveTimeout = 20_000;
console.log(`listening on ${port}`);
// rare: app.set('trust proxy', 2)
},
});Server entry example
// src/server.tsx
import { defineServerEntry } from 'sku/runtime';
const server = defineServerEntry({
getSite({ req }) {
return req.get('x-site') === 'nz' ? 'nz' : 'au';
},
getLanguage({ req }) {
return req.path.startsWith('/th') ? 'th-TH' : 'en';
},
getClientContext({ req }) {
return {
theme: 'dark',
userId: req.user?.id ?? null,
};
},
middleware: [
(req, res, next) => {
if (req.path === '/api/health') {
res.status(200).type('text/plain').send('ok');
return;
}
next();
},
],
});
export default server;Client entry
// src/client.tsx
import { defineClientEntry } from 'sku/runtime';
import type server from './server';
const client = defineClientEntry<typeof server>()({
onHydrate() {
// Optional hydrate-time side effects (e.g. analytics)
},
});
export default client;Prefer defineClientEntry<typeof server>()({ … }) so client callbacks get Site / ClientContext from the server entry.
onHydrate
Side effects before client React hydrate. Receives { clientContext } from the server seed. Request values reach React via providers — no need to stash them in module state.
onHydrate?: (args: {
clientContext: ClientContext | undefined;
}) => void;getReactContext
Client-specific values for useReactContext() (same channel as the server; no Express).
getReactContext?: (args: {
site: Site;
clientContext: ClientContext | undefined;
}) => ReactContext | Promise<ReactContext>;Advanced: router context
Optional dual-entry getRouterContext seeds React Router’s RouterContextProvider for loader, action, and route-middleware dependency injection. Prefer projecting isomorphic values both server and client can supply — see Data loading → Router context and Three value channels.
getRouterContext
Server signature — receives Express req plus already-resolved sibling values:
getRouterContext?: (args: {
request: Request;
req: ExpressRequest;
site: Site;
clientContext: ClientContext | undefined;
reactContext: ReactContext | undefined;
}) => RouterContextProvider | Promise<RouterContextProvider>;On the client entry, the same name is called on every client navigation / fetcher — not once at hydrate. It must work without Express:
getRouterContext?: (args: {
site: Site;
clientContext: ClientContext | undefined;
reactContext: ReactContext | undefined;
}) => RouterContextProvider | Promise<RouterContextProvider>;Never put Express req in RouterContextProvider
Prefer values both sides can supply. Raw req is undefined on client navigations.
Router context example
// Shared key — src/userIdContext.ts
import { createContext } from 'react-router';
export const userIdContext = createContext<string | null>(null);import { RouterContextProvider } from 'react-router';
import { defineServerEntry } from 'sku/runtime';
import { userIdContext } from './userIdContext';
const server = defineServerEntry({
getClientContext({ req }) {
return { userId: req.user?.id ?? null };
},
getRouterContext({ clientContext }) {
const ctx = new RouterContextProvider();
ctx.set(userIdContext, clientContext?.userId ?? null);
return ctx;
},
});
export default server;import { RouterContextProvider } from 'react-router';
import { defineClientEntry } from 'sku/runtime';
import type server from './server';
import { userIdContext } from './userIdContext';
const client = defineClientEntry<typeof server>()({
getRouterContext({ clientContext }) {
const ctx = new RouterContextProvider();
ctx.set(userIdContext, clientContext?.userId ?? null);
return ctx;
},
});
export default client;Routes entry
Named export of a React Router route tree — see Routing.
import type { MapRoutePath, SkuRouteObject } from 'sku/runtime';
export const routes: SkuRouteObject[];
export const mapRoutePath?: MapRoutePath;SkuRouteObject is a React Router RouteObject plus optional sites for multi-site membership.
Optional mapRoutePath clones path-bearing and index routes for alternate paths (index homes use path: '') — see Multi-language.
See also
- Routing — compose the route tree
- Providers — typed hooks and root layout
- Middleware — Express and
devServerMiddleware - Data loading — render-time fetch and loaders
- Logging —
instrumentationson each entry - Runtime API —
sku/runtimehelpers
