Routing
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.
This page covers the route tree, page modules, multi-site membership, and intent preloading.
SSR uses React Router Data Mode for routing. Export a routes array from routesEntry (default src/routes.tsx). sku wires that tree into React Router on the server and in the browser.
Prerequisite
Install React Router in your app (react-router@^8). For route API details (layouts, loaders, error boundaries), see React Router Data Mode routing.
Add a page
Compose the route tree
Each route can set a path (or index), optional site membership, and a lazy page import. Put loader, action, Component, and ErrorBoundary on the lazily imported page module — not on the route object in routes.tsx. Use React Router’s lazy factory so each page is a separate chunk:
import type { SkuRouteObject } from 'sku/runtime';
import { RootLayout } from './App/RootLayout';
export const routes: SkuRouteObject[] = [
{
Component: RootLayout,
children: [
{ index: true, lazy: () => import('./pages/home/home') },
{ path: 'about', lazy: () => import('./pages/about/about') },
],
},
];import { Outlet } from 'react-router';
export const RootLayout = () => <Outlet />;export function Component() {
return <main>Home</main>;
}export function Component() {
return <main>About</main>;
}Lazy page modules must export a named Component (not export default).
Use a pathless root layout for shared UI and providers (see Providers).
You’re set up when:
- Pages load via
lazy: () => import(...)(not static imports intoroutes.tsx) - Each page module exports a named
Component - Shared UI lives on a pathless root layout
Keep pages lazy
Do not statically import page modules into routes.tsx, or you lose per-route chunking. Prefer the idiomatic form so sku can derive production modulepreload links automatically:
lazy: () => import('./pages/about/about');Automatic modulepreload
Idiomatic lazy: () => import(...) lets sku set handle.moduleId to the Vite client manifest key (for example src/pages/about/about.tsx). That is how production document responses emit modulepreload links for the matched route.
If you use another lazy shape, set handle.moduleId yourself to that same manifest key. sku warns in development when a lazy route is missing moduleId, and skips that route’s production preloads.
When to use loaders
For page content, prefer render-time data loading. Use loaders when you need document redirects, response headers, or to start work above a suspending tree. Export those loaders from the same page module as Component.
Multi-site routes
When different sites need different path sets, set optional sites on a route. sku only includes that route when the active site is in the list. If you omit sites, the route is available on every configured site.
Resolve the active site in the server entry with getSite (required when config has more than one site; omit on single-site apps):
import type { SkuRouteObject } from 'sku/runtime';
import { RootLayout } from './App/RootLayout';
export const routes: SkuRouteObject[] = [
{
Component: RootLayout,
children: [
{ index: true, lazy: () => import('./pages/home/home') },
{
path: 'au-only',
sites: ['au'],
lazy: () => import('./pages/au-only/au-only'),
},
{
path: 'nz-only',
sites: ['nz'],
lazy: () => import('./pages/nz-only/nz-only'),
},
],
},
];import { defineServerEntry } from 'sku/runtime';
const server = defineServerEntry({
getSite({ req }) {
return req.get('x-site') === 'nz' ? 'nz' : 'au';
},
});
export default server;Strictly typed sites in route objects
When defining sites in a route, you can narrow down the type of sites by defining SiteName in SkuRouteObject<SiteName>.
You can get the site name directly from the return of your getSite method using SkuRouteObject<SiteOf<typeof server>>.
A good practice is to export route types from the same file as createSkuContexts, then import them wherever you define routes. Do not import the server entry into those files.
import {
createSkuContexts,
type SiteOf,
type SkuRouteObject,
} from 'sku/runtime';
import type client from './client';
import type server from './server';
export const { useSite, useClientContext, useReactContext } = createSkuContexts<
typeof server,
typeof client
>();
export type AppRouteObject = SkuRouteObject<SiteOf<typeof server>>;import type { AppRouteObject } from './skuContext';
export const routes: AppRouteObject[] = [
{ path: 'au-only', sites: ['au'] },
{ path: 'nz-only', sites: ['nz'] },
];Multiple paths with mapRoutePath
When the same page should match more than one concrete path (for example /about and /fr/about, or / and /fr), export optional mapRoutePath from routesEntry. sku calls it while pre-building each site tree and clones the route for each returned path. Index homes are called with path: '' — return '' to keep index: true, or a non-empty string for a prefixed home without index.
import type { MapRoutePath, SkuRouteObject } from 'sku/runtime';
export const mapRoutePath: MapRoutePath = ({ path, site, parentSegments }) => {
if (parentSegments.length > 0) {
return [path];
}
if (path === '' && site === 'au') {
return ['', 'au'];
}
if (path === 'about' && site === 'au') {
return ['about', 'au/about'];
}
return [path];
};
export const routes: SkuRouteObject[] = [
{
Component: RootLayout,
children: [
{ index: true, lazy: () => import('./pages/home/home') },
{ path: 'about', lazy: () => import('./pages/about/about') },
],
},
];See Multi-language → Languages in the path for the localisation-prefix case.
Case-sensitive paths
By default, sku matches route paths case-sensitively. If a route omits React Router’s caseSensitive, sku sets caseSensitive: true while pre-building the site tree. So /about matches a route with path: 'about', and /About does not.
Opt out on a specific route when you need case-insensitive matching:
{ path: 'about', caseSensitive: false, lazy: () => import('./pages/about/about') },Intent preloading with usePreloadRoute
On the initial document, sku already emits modulepreload links for the matched route’s chunks. To warm chunks for a route the user is about to visit, use usePreloadRoute:
import { Link, type LinkProps } from 'react-router';
import { usePreloadRoute } from 'sku/runtime';
export function PreloadingLink({ to, ...rest }: LinkProps) {
const preload = usePreloadRoute(to);
return (
<Link
to={to}
onMouseEnter={preload}
onFocus={preload}
onTouchStart={preload}
{...rest}
/>
);
}Calling the returned function loads matched lazy route modules for the current site. It is fire-and-forget — a failed warm-up never throws; navigation reports the real error. Loader data is not prefetched — only route modules.
React Router route middleware
React Router Data Mode supports a middleware array on routes for isomorphic behaviour on matched routes. That is separate from Express middleware on the server entry: use Express for HTTP-level work, and route middleware for behaviour tied to the matched route tree. See Middleware for when to use each.
See also
- Providers — root layout and typed hooks
- Data loading — render-time fetch and loaders
- Middleware — Express vs React Router route
middleware - Multi-language — language chunks and path prefixes
- Request entries —
getSiteand other getters
