Docs
QuickstartsTanStack Start

TanStack Start (React)

Wire Kizlo into a TanStack Start (React) app and make your first call.

This quickstart covers the app side. Make sure WordPress is ready first; see Installation. Either connect your own WordPress with the plugin and an Application Password, or let Kizlo run one locally in Docker.

Set up Kizlo in a new project

Starting fresh? Scaffold a TanStack Start app with Kizlo already wired:

npx kizlo@latest create tanstack-start-react my-app

It scaffolds a fully wired project and walks you through your WordPress credentials. When it finishes, cd my-app. Building with Solid instead? See the TanStack Start (Solid) quickstart.

Set up Kizlo in an existing project

Add Kizlo to an app you already have. Use the CLI (recommended) or wire the files by hand. Both produce the same result.

TanStack Start ships SSR by default, so there's nothing extra to enable. Kizlo renders on demand, and the server routes below run on that same server.

Run the initializer in your project root:

npx kizlo@latest init

It detects your framework, src dir (if present), package manager, import alias, etc. and scaffolds:

$.ts
__root.tsx
robots[.]txt.ts
sitemap[.]xml.ts
site[.]webmanifest.ts
$.ts
index.ts
client.ts
kizlo.config.ts
.env

Along the way it asks whether to run WordPress locally in Docker or connect your own, writes the matching credentials to .env, and generates the contract. When init finishes, you're ready to make your first call.

Kizlo's brand tags (icons, web manifest, theme color) render through the root route's head. init can't edit a __root.tsx you own, so it prints one manual step at the end: wire resolveRootHead into your root route's loader and spread its result into head (see the step below).

Feel free to customize this folder structure however you like. Point the dir option in kizlo.config.ts at it, so kizlo knows where your server files and kizlo instance live and can generate the server contract.

Manual setup

Prefer to wire it yourself? Create the same files the CLI would.

Install the package

npm install kizlo@latest

Add kizlo.config.ts

Add this at your project root. It tells the CLI where Kizlo lives and which import alias to use:

kizlo.config.ts
import { defineConfig } from "kizlo/config"

export default defineConfig({
  dir: "src/lib/kizlo",
  alias: "@",
})

Create the server

At src/lib/kizlo/server/index.ts, register the TanStack Start integration so it maps the framework environment onto Kizlo's values:

src/lib/kizlo/server/index.ts
import { createKizlo } from "kizlo"
import { tanstackStart } from "kizlo/tanstack-start/server"

export const { procedures, client, context, handler } = createKizlo({
  integrations: [tanstackStart()],
})

Mount the route handler

At src/routes/api/kizlo/$.ts, createApiHandlers fans Kizlo's single handler out to every HTTP method, and Kizlo's handler dispatches on the request internally:

src/routes/api/kizlo/$.ts
import { createFileRoute } from "@tanstack/react-router"
import { createApiHandlers } from "kizlo/tanstack-start/server"
import { handler } from "@/lib/kizlo/server"

export const Route = createFileRoute("/api/kizlo/$")({
  server: {
    handlers: createApiHandlers(handler),
  },
})

Generate the contract

So server/generated/ exists before the client imports it:

npx kizlo generate

Create the client

Create src/lib/kizlo/client.ts from the generated contract. It is typed automatically. Pass the public API URL at the call site so Vite inlines it into the browser bundle:

src/lib/kizlo/client.ts
import { createKizloClient } from "kizlo/tanstack-start"
import { contract } from "./server/generated"

export const client = createKizloClient(contract, { url: import.meta.env.VITE_KIZLO_BASE_URL })

Create the root route head

The root route feeds brand and site data (icons, web manifest, theme color) from WordPress into the document <head>. resolveRootHead holds the server-to-server client, so call it through a server function in the loader and spread the result into head. Child routes layer their per-page SEO on top:

src/routes/__root.tsx
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"
import { createServerFn } from "@tanstack/react-start"
import { resolveRootHead } from "kizlo/tanstack-start/server"
import type { ReactNode } from "react"
import { client } from "@/lib/kizlo/server"

const getRootHead = createServerFn({ method: "GET" }).handler(() => resolveRootHead(client))

export const Route = createRootRoute({
  loader: () => getRootHead(),
  head: ({ loaderData }) => ({
    meta: [{ charSet: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, ...(loaderData?.meta ?? [])],
    links: loaderData?.links ?? [],
  }),
  component: () => (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  ),
})

Wire robots, sitemaps, and the web manifest

Each is a server route that refetches live from WordPress on every request. Create the four files:

src/routes/robots[.]txt.ts
import { createFileRoute } from "@tanstack/react-router"
import { createRobotsRoute } from "kizlo/tanstack-start/server"
import { client } from "@/lib/kizlo/server"

export const Route = createFileRoute("/robots.txt")({
  server: { handlers: { GET: createRobotsRoute(client) } },
})
src/routes/sitemap[.]xml.ts
import { createFileRoute } from "@tanstack/react-router"
import { createSitemapRedirectRoute } from "kizlo/tanstack-start/server"

// 308-redirects the well-known /sitemap.xml to the generated index at /sitemaps/index.xml.
export const Route = createFileRoute("/sitemap.xml")({
  server: { handlers: { GET: createSitemapRedirectRoute() } },
})
src/routes/sitemaps/$.ts
import { createFileRoute } from "@tanstack/react-router"
import { createSitemapRoute } from "kizlo/tanstack-start/server"
import { client } from "@/lib/kizlo/server"

// Splat route so /sitemaps/index.xml and every /sitemaps/{key}.xml page resolve to one handler.
export const Route = createFileRoute("/sitemaps/$")({
  server: { handlers: { GET: createSitemapRoute(client) } },
})
src/routes/site[.]webmanifest.ts
import { createFileRoute } from "@tanstack/react-router"
import { createManifestRoute } from "kizlo/tanstack-start/server"
import { client } from "@/lib/kizlo/server"

export const Route = createFileRoute("/site.webmanifest")({
  server: { handlers: { GET: createManifestRoute(client) } },
})

Environment

Finally, connect WordPress. Use a local stack while you build and your hosted WordPress in production.

Local

Let Kizlo run WordPress for you. There is nothing to fill in:

npx kizlo dev

It starts WordPress in Docker, writes its dev credentials to .env for you, and watches your server files to keep the contract in sync. This gives you one command for the whole local loop.

Remote

For your hosted WordPress, add its credentials to .env:

.env
KIZLO_WP_URL=https://your-site.com
KIZLO_WP_USERNAME=admin
KIZLO_WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
KIZLO_WP_SECRET=                   # a long random string
VITE_KIZLO_BASE_URL=http://localhost:3000/api/kizlo

Generate KIZLO_WP_SECRET with openssl rand -hex 32. While developing, run npx kizlo dev to keep the generated contract in sync as you edit your server files.

Render a post

Here is a complete dynamic blog post route. A server function fetches the post by slug and returns a lean, serializable payload; the route's head maps its SEO straight into the document <head>:

  • resolvePageHead: maps the post's SEO head into the meta/links/scripts TanStack merges into the head: title, description, canonical, Open Graph, and Twitter tags, so you never hand-write them.
  • JSON-LD: renderJsonLd serializes the post's schema.org graph to a string in the loader (so the payload stays serializable); resolvePageHead emits it as a <script type="application/ld+json">.

Each route renders on demand for its slug, so there's no build-time list step.

src/routes/blog/$slug.tsx
import { createFileRoute, notFound } from "@tanstack/react-router"
import { createServerFn } from "@tanstack/react-start"
import { renderJsonLd, resolvePageHead } from "kizlo/tanstack-start"
import { client } from "@/lib/kizlo/server"

// Return only what the page renders (title, content, SEO). Keep the payload lean and serializable, not the
// whole WordPress post. JSON-LD is rendered to a string here so the loader payload stays serializable.
const getPost = createServerFn({ method: "GET" })
  .validator((slug: string) => slug)
  .handler(async ({ data: slug }) => {
    const { data } = await client.posts.get({ params: { identifier: slug } })
    if (!data) return null
    return {
      title: data.title,
      content: data.content,
      head: data.seo?.head ?? null,
      jsonLd: data.seo ? renderJsonLd(data.seo.schema) : null,
    }
  })

export const Route = createFileRoute("/blog/$slug")({
  loader: async ({ params }) => {
    const post = await getPost({ data: params.slug })
    if (!post) throw notFound()
    return post
  },
  // SEO tags and JSON-LD mapped from the post's SEO head. Missing/unpublished posts 404 in the loader.
  head: ({ loaderData }) => (loaderData?.head ? resolvePageHead(loaderData.head, loaderData.jsonLd) : {}),
  component: Post,
})

function Post() {
  const post = Route.useLoaderData()

  return (
    <article>
      <h1>{post.title ?? "Untitled"}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content ?? "" }} />
    </article>
  )
}

From here, head to Concepts to see how procedures, integrations, and adapters compose into something production-ready.

On this page