16k

@json-render/tanstack-start

TanStack Start renderer for JSON-defined applications with routes, layouts, head metadata, SSR loaders, prerender paths, and client navigation.

Installation#

npm install @json-render/core @json-render/react @json-render/tanstack-start

schema#

Use the Start application schema to generate full multi-page specs.

import { defineCatalog } from "@json-render/core";
import {
  schema,
  startComponentDefinitions,
} from "@json-render/tanstack-start/server";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    ...startComponentDefinitions,
    Card: {
      props: z.object({ title: z.string() }),
      description: "Card container",
    },
    NavBar: {
      props: z.object({}),
      slots: ["default"],
      description: "Application navigation",
    },
  },
  actions: {},
});

The generation prompt teaches TanStack Router's $param and $ splat route syntax, reusable layouts, escaped JSON Patch route keys, and the built-in Slot, Link, and navigate capabilities.

Include startComponentDefinitions in the catalog so generated Slot and Link elements pass validation. PageRenderer supplies their React implementations automatically.

createStartApp#

Create helpers for a TanStack Start splat route.

import { createStartApp } from "@json-render/tanstack-start/server";

export const { getPageData, getHead, getStaticPaths } = createStartApp({
  spec,
  loaders: {
    post: async ({ slug }) => ({
      post: await getPost(slug as string),
    }),
  },
});

Options#

OptionTypeDescription
specStartAppSpec | (() => StartAppSpec | Promise<StartAppSpec>)A static application spec or an async spec factory
loadersRecord<string, LoaderFn>Named data loaders referenced by route specs

Returns#

HelperDescription
getPageData

Matches a pathname, runs its loader, and returns serializable page and layout data

getHead

Returns TanStack Router meta and links descriptors

getStaticPathsReturns concrete paths for TanStack Start prerendering

State is merged in this order: application state, layout state, page state, then loader data. Later sources override earlier values.

StartAppSpec#

interface StartAppSpec {
  metadata?: StartMetadata;
  routes: Record<string, StartRouteSpec>;
  layouts?: Record<string, Spec>;
  state?: Record<string, unknown>;
}

Each route requires a page spec and can select a layout, metadata, a named loader, loading/error/not-found specs, and static parameters.

Route Patterns#

PatternExampleParams
//{}
/about/about{}
/blog/$slug/blog/hello{ slug: "hello" }
/docs/$/docs/guides/intro{ _splat: "guides/intro" }

Loader parameters are URL-decoded before they reach named loaders. Splat content is a slash-delimited string under _splat. Parameter values supplied through staticParams are URL-encoded in the paths returned by getStaticPaths().

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.

For prerendered dynamic routes, provide staticParams:

routes: {
  '/blog/$slug': {
    page,
    staticParams: [{ slug: 'hello' }, { slug: 'world' }],
  },
  '/docs/$': {
    page: docsPage,
    staticParams: [{ _splat: 'guides/intro' }],
  },
}

Map getStaticPaths() into TanStack Start's top-level pages configuration:

const pages = (await getStaticPaths()).map((path) => ({ path }));

TanStack Route Setup#

Wire the helpers to a file-based $ 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. When a spec factory or named loader uses database clients, credentials, or server-only imports, invoke getPageData and getHead inside a TanStack Start createServerFn and call that server function from the route loader.

StartAppProvider#

Provide component implementations and action handlers around the root Outlet. Render HeadContent for route metadata.

import {
  createRootRoute,
  HeadContent,
  Outlet,
  Scripts,
} from "@tanstack/react-router";
import { StartAppProvider } from "@json-render/tanstack-start";
import { spec } from "@/lib/spec";

export const Route = createRootRoute({
  component: () => (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <StartAppProvider
          registry={registry}
          handlers={handlers}
          spec={spec}
        >
          <Outlet />
        </StartAppProvider>
        <Scripts />
      </body>
    </html>
  ),
});

Passing spec lets StartLoading, StartErrorBoundary, and StartNotFound automatically select the matched route's fallback specs. Their explicit loadingSpec, errorSpec, and notFoundSpec props take precedence. For a server-only application spec, omit spec and pass client-safe fallback specs explicitly.

Pass named functions through functions when props use $computed:

<StartAppProvider
  registry={registry}
  spec={spec}
  functions={{ uppercase: ({ value }) => String(value).toUpperCase() }}
>
  <Outlet />
</StartAppProvider>

Built-ins#

  • Slot inserts page content into a JSON-defined layout.
  • Link wraps TanStack Router's Link; generated specs use an href prop.
  • navigate performs client-side navigation from action bindings.
  • StartLoading, StartErrorBoundary, and StartNotFound resolve the matched route's fallback specs when they are used as TanStack Router boundary components and the provider receives spec.

The default StartErrorBoundary fallback invalidates the router and reruns the failed loader when the user selects Try again.

Slot and Link are automatically added to the page registry.

Server Utilities#

import {
  collectStaticPaths,
  matchRoute,
  metadataToHead,
  resolveMetadata,
  splatToPath,
} from "@json-render/tanstack-start/server";

Entry Points#

ImportContents
@json-render/tanstack-startProvider, page renderer, Link, and route fallback components
@json-render/tanstack-start/serverApp factory, schema, matcher, metadata, and prerender helpers
@json-render/tanstack-start/catalogServer-safe definitions for built-in Slot and Link components