> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# TanStack Start

> Use Commerce Engine SEO request middleware, route head data, and WebMCP tools in TanStack Start.

TanStack Start is a server-mode Commerce Engine SEO integration in the production starters: use one request middleware for discovery surfaces, build route head data from loader results, and register WebMCP from one root client initializer.

## 1. Share site and route configuration

Keep public site identity and routes in a storefront-free module so both server SEO code and browser agent registration can import it safely.

```typescript src/lib/commerce-seo.config.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { defineCommerceSeoConfig } from "@commercengine/seo/config";

export const commerceSeo = defineCommerceSeoConfig({
  site: {
    name: "Acme",
    url: "https://acme.example",
    brandName: "Acme",
    locale: "en_US",
  },
  routes: {
    // The package default is /products/:slug. Configure /product only when
    // that is the storefront's actual public route convention.
    productBase: "/product",
    categoryBase: "/category",
  },
});

export const { site, routes } = commerceSeo;
```

## 2. Create the SEO instance

```typescript src/lib/seo.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createCommerceSeo } from "@commercengine/seo";
import { commerceSeo } from "@/lib/commerce-seo.config";
import { storefront } from "@/lib/storefront";

export const seo = createCommerceSeo({ ...commerceSeo, storefront });
```

<Warning>
  Browser code should import `site` and `routes` from `commerce-seo.config.ts`, not from the module exporting `seo`. The SEO instance holds the storefront and is used by loaders, head generation, and request middleware.
</Warning>

## 3. Mount one request middleware

TanStack file-route params must be valid JavaScript identifiers, so `.md` mirror paths are a poor fit for route files. Mount the Commerce Engine request middleware once instead:

```typescript src/start.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createTanStackStartSeoMiddleware } from "@commercengine/seo/tanstack-start/server";
import { createStart } from "@tanstack/react-start";
import { seo } from "@/lib/seo";

export const startInstance = createStart(() => ({
  requestMiddleware: [
    createTanStackStartSeoMiddleware(seo, {
      robots: true,
      sitemap: true,
    }),
  ],
}));
```

The request adapter always handles:

* product/category/search Markdown negotiation
* explicit `.md` mirrors
* `/llms.txt`
* `/sitemap.md`

`robots.txt` and the XML sitemap are deliberately opt-in in server mode; the example enables both.

## 4. Build route head data from loader data

`head` itself is synchronous, so resolve catalog data and asynchronous SEO routes in the loader and return the completed head model through `loaderData`.

```tsx src/routes/product/$slug.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { serializeJsonLd } from "@commercengine/seo";
import { createTanStackStartProductHead } from "@commercengine/seo/tanstack-start";
import type { ProductDetail } from "@commercengine/storefront";
import { createFileRoute } from "@tanstack/react-router";
import { seo } from "@/lib/seo";
import { storefront } from "@/lib/storefront";

async function productHeadWithBreadcrumb(product: ProductDetail) {
  const category = product.categories?.[0];
  const home = seo.config.site.url;

  const [head, productUrl, categoryUrl] = await Promise.all([
    createTanStackStartProductHead(seo, product),
    seo.productUrl(product),
    category ? seo.categoryUrl(category) : Promise.resolve(null),
  ]);

  const breadcrumb = seo.breadcrumbJsonLd([
    { name: "Home", url: home },
    ...(category && categoryUrl
      ? [{ name: category.name, url: categoryUrl }]
      : []),
    { name: product.name, url: productUrl ?? home },
  ]);

  return {
    ...head,
    scripts: [
      ...head.scripts,
      {
        type: "application/ld+json",
        children: serializeJsonLd(breadcrumb),
      },
    ],
  };
}

export const Route = createFileRoute("/product/$slug")({
  loader: async ({ params }) => {
    const { data, error, response } = await storefront
      .publicStorefront()
      .catalog.getProductDetail({ product_id: params.slug });

    if (error && response.status !== 404) {
      throw new Error(error.message);
    }

    const product = data?.product ?? null;
    return {
      product,
      seoHead: product ? await productHeadWithBreadcrumb(product) : undefined,
    };
  },
  head: ({ loaderData }) => loaderData?.seoHead ?? {},
  component: ProductPage,
});
```

Only a confirmed missing product should become a not-found state. Treat transport/5xx catalog failures as recoverable failures rather than permanent 404s.

<Info>
  Breadcrumb URLs come from `seo.productUrl()` and `seo.categoryUrl()`. Do not rebuild `/product/...` or `/category/...` strings independently; custom route bases and CMS-owned slugs must remain consistent across canonicals, sitemaps, Markdown, breadcrumbs, and agent navigation.
</Info>

## 5. Register WebMCP independently of session bootstrap

Tool registration only declares capabilities. Public catalog tools should not wait for anonymous-session or Hosted Checkout initialization, and session/cart tools already return a retryable not-ready result until the session is usable.

```tsx src/components/StorefrontInitializer.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createHostedCheckoutBridge } from "@commercengine/ai/checkout";
import { registerCommerceWebMcp } from "@commercengine/ai/webmcp";
import { getCheckout, initCheckout } from "@commercengine/checkout";
import { destroyCheckout } from "@commercengine/checkout/react";
import { useRouter } from "@tanstack/react-router";
import { useEffect } from "react";
import { routes, site } from "@/lib/commerce-seo.config";
import { ensureClientSessionBootstrapped } from "@/lib/session-bootstrap";
import { storefront, storefrontConfig } from "@/lib/storefront";

export function StorefrontInitializer() {
  const router = useRouter();

  useEffect(() => {
    let agentTools: AbortController | null = null;
    let active = true;

    void registerCommerceWebMcp({
      storefront,
      siteUrl: site.url,
      routes,
      checkout: createHostedCheckoutBridge({ getState: () => getCheckout() }),
      navigation: { navigate: (url) => router.navigate({ href: url }) },
      diagnostics: import.meta.env.DEV
        ? (event) => console.info("[commerce-ai]", event.code, event.message ?? "")
        : undefined,
    })
      .then((controller) => {
        if (!active) controller?.abort();
        else agentTools = controller;
      })
      .catch((error) => {
        console.error("Failed to register Commerce Engine agent tools", error);
      });

    void (async () => {
      await ensureClientSessionBootstrapped();
      if (!active) return;

      const sdk = storefront.clientStorefront();
      const accessToken = await sdk.getAccessToken();
      const refreshToken = await sdk.session.peekRefreshToken();
      if (!active) return;

      initCheckout({
        storeId: storefrontConfig.storeId,
        apiKey: storefrontConfig.apiKey,
        environment: storefrontConfig.environment,
        authMode: "provided",
        accessToken: accessToken ?? undefined,
        refreshToken: refreshToken ?? undefined,
        onTokensUpdated: ({ accessToken, refreshToken }) => {
          void sdk.setTokens(accessToken, refreshToken);
        },
      });
    })().catch((error) => {
      console.error("Failed to initialize hosted checkout", error);
    });

    return () => {
      active = false;
      agentTools?.abort();
      destroyCheckout();
    };
  }, [router]);

  return null;
}
```

Mount this initializer once near the router root. Registration and checkout bootstrap run in parallel, and teardown cannot leave a late WebMCP registration alive after unmount.

## Production checklist

* `src/start.ts` mounts `createTanStackStartSeoMiddleware()` once
* server mode enables `robots: true` and `sitemap: true` when Commerce Engine owns those routes
* loaders use `publicStorefront()` for public catalog reads
* asynchronous SEO head data is resolved in the loader and returned through `loaderData`
* breadcrumb URLs use SEO route resolvers
* WebMCP registration starts without waiting for session bootstrap
* late registration is aborted if the root initializer unmounts
* development diagnostics prove registration actually ran
* `site` and `routes` come from the pure shared config in browser code

<Card title="Reference starter" icon="github" href="https://github.com/tark-ai/ce-starter-projects/tree/main/apps/soja-tanstack">
  Full TanStack Start storefront using request middleware, route head data, and independent WebMCP registration.
</Card>
