* feat(tanstack-start): add renderer - Add full-app routing, SSR data helpers, metadata, layouts, and navigation. - Validate with focused tests and the current TanStack Router type contract. - Document the package across the API reference, renderer guide, and agent skill. * fix(tanstack-start): address review findings * fix(tanstack-start): align runtime contracts - Merge layout state into page rendering with explicit precedence. - Match TanStack Router string splat parameters across runtime, types, and docs. - Preserve the client boundary and add regression coverage. * fix(tanstack-start): match empty splats - Align root and trailing-slash splat matching with TanStack Router. - Cover empty splat paths with regression tests. * fix(tanstack-start): harden route transitions - Scope page state to the rendered route during pending navigation. - Normalize route matching across empty splats and trailing slashes. - Preserve client boundaries in CommonJS and ESM builds. * fix(tanstack-start): handle route recovery edge cases - Retry failed loaders by invalidating the router. - Normalize trailing-slash and encoded route pathnames. - Add regression coverage and document corrected behavior. * fix(tanstack-start): preserve encoded percent params - Preserve encoded percent signs through pathname normalization. - Cover dynamic, splat, and static-path round trips.
@json-render/tanstack-start
TanStack Start renderer for @json-render/core. Define routes, layouts, head metadata, state, and loader-backed pages as JSON, then render them through TanStack Router and Start SSR.
Installation
npm install @json-render/core @json-render/react @json-render/tanstack-start
Quick Start
1. Define the catalog
Include the definitions for the built-in Slot and Link components. Their
React implementations are added automatically by PageRenderer.
import { defineCatalog } from "@json-render/core";
import {
schema,
startComponentDefinitions,
} from "@json-render/tanstack-start/server";
export const catalog = defineCatalog(schema, {
components: {
...startComponentDefinitions,
Card: cardDefinition,
Container: containerDefinition,
NavBar: navBarDefinition,
Post: postDefinition,
},
actions: {},
});
2. Define the application
import type { StartAppSpec } from "@json-render/tanstack-start";
export const spec: StartAppSpec = {
metadata: {
title: { default: "My App", template: "%s | My App" },
},
layouts: {
main: {
root: "shell",
elements: {
shell: {
type: "Container",
props: {},
children: ["nav", "slot"],
},
nav: { type: "NavBar", props: {}, children: [] },
slot: { type: "Slot", props: {}, children: [] },
},
},
},
routes: {
"/": {
layout: "main",
metadata: { title: "Home" },
page: {
root: "hero",
elements: {
hero: {
type: "Card",
props: { title: "Welcome" },
children: [],
},
},
},
},
"/blog/$slug": {
layout: "main",
loader: "post",
page: {
root: "post",
elements: {
post: {
type: "Post",
props: { post: { $state: "/post" } },
children: [],
},
},
},
},
},
};
3. Create the route helpers
// src/lib/json-app.ts
import { createStartApp } from "@json-render/tanstack-start/server";
import { spec } from "./spec";
export const { getPageData, getHead, getStaticPaths } = createStartApp({
spec,
loaders: {
post: async ({ slug }) => ({ post: await getPost(slug as string) }),
},
});
4. Wire a splat route
// src/routes/$.tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import {
PageRenderer,
StartErrorBoundary,
StartLoading,
StartNotFound,
} from "@json-render/tanstack-start";
import { getHead, getPageData } from "@/lib/json-app";
export const Route = createFileRoute("/$")({
loader: async ({ location }) => {
const data = await getPageData({ pathname: location.pathname });
if (!data) throw notFound();
return data;
},
head: ({ match }) => getHead({ pathname: match.pathname }),
component: Page,
pendingComponent: StartLoading,
errorComponent: StartErrorBoundary,
notFoundComponent: StartNotFound,
});
function Page() {
return <PageRenderer {...Route.useLoaderData()} />;
}
TanStack Router loaders are isomorphic. If your spec factory or named loaders
contain secrets or server-only imports, call getPageData and getHead from a
TanStack Start createServerFn and return the resulting data from the route
loader.
5. Provide the registry
// src/routes/__root.tsx
import {
createRootRoute,
HeadContent,
Outlet,
Scripts,
} from "@tanstack/react-router";
import { StartAppProvider } from "@json-render/tanstack-start";
import { registry, handlers } from "@/lib/registry";
import { spec } from "@/lib/spec";
export const Route = createRootRoute({ component: Root });
function Root() {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<StartAppProvider
registry={registry}
handlers={handlers}
spec={spec}
>
<Outlet />
</StartAppProvider>
<Scripts />
</body>
</html>
);
}
Passing spec lets the Router boundary components automatically render the
matched route's loading, error, and notFound specs. An explicit
loadingSpec, errorSpec, or notFoundSpec prop overrides this lookup. If the
application spec is server-only, omit spec from the provider and supply those
explicit props from client-safe fallback specs.
The default StartErrorBoundary fallback invalidates the router and reruns the
failed loader when the user selects Try again.
Pass named functions through the provider when generated props use
$computed:
<StartAppProvider
registry={registry}
spec={spec}
functions={{ uppercase: ({ value }) => String(value).toUpperCase() }}
>
<Outlet />
</StartAppProvider>
Route Patterns
| Pattern | Matches | Loader params |
|---|---|---|
/ |
/ |
{} |
/about |
/about |
{} |
/blog/$slug |
/blog/hello |
{ slug: "hello" } |
/docs/$ |
/docs/a/b |
{ _splat: "a/b" } |
Static routes are included in getStaticPaths(). Dynamic routes are included
when their route spec supplies staticParams. Loader parameters are URL-decoded,
and splat content is a slash-delimited string under _splat. Parameter values
emitted by getStaticPaths() are URL-encoded.
Route matching treats trailing slashes as optional and accepts both encoded and decoded pathname representations. This keeps loader data and route metadata in sync for static paths containing spaces or non-ASCII characters.
Initial state is merged in this order: application state, layout state, page state, then loader data. Later sources override earlier values.
Map the paths to TanStack Start's top-level pages option when prerendering:
const pages = (await getStaticPaths()).map((path) => ({ path }));
Entry Points
| Import | Description |
|---|---|
@json-render/tanstack-start |
Provider, page renderer, Link, and route fallback components |
@json-render/tanstack-start/server |
App factory, schema, matcher, metadata, and prerender helpers |
@json-render/tanstack-start/catalog |
Server-safe definitions for built-in Slot and Link components |
See the full API reference.