> ## 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.

# React / Vite

> Add Commerce Engine SEO/AEO and browser agent tools to a React or Vite storefront.

A React/Vite storefront normally has no application server at request time. Use `@commercengine/seo` in the browser for page head data, generate the discovery assets before the Vite build, and register `@commercengine/ai` from app bootstrap or a root effect.

## 1. Share site and route configuration

```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: {
    productBase: "/product",
    categoryBase: "/category",
  },
});

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

## 2. Create the SEO instance

A browser-only SPA can use the canonical Storefront factory directly:

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

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

Build head data asynchronously because route resolution and enrichment can be async:

```tsx src/lib/use-commerce-seo.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import type { Category, ProductDetail } from "@commercengine/storefront";
import type { CommerceSeoHead } from "@commercengine/seo";
import { useEffect, useState } from "react";
import { seo } from "./seo";

export function useCommerceSeoHead(
  entity: ProductDetail | Category | null | undefined,
  kind: "product" | "category",
) {
  const [head, setHead] = useState<CommerceSeoHead | null>(null);

  useEffect(() => {
    if (!entity) {
      setHead(null);
      return;
    }

    let active = true;
    const request = kind === "product"
      ? seo.productHead(entity as ProductDetail)
      : seo.categoryHead(entity as Category);

    void request.then((value) => {
      if (active) setHead(value);
    });

    return () => { active = false; };
  }, [entity, kind]);

  return head;
}
```

## 3. Render the head model

React 19 hoists `<title>`, `<meta>`, and `<link>` into `<head>`. JSON-LD can remain inline in the page tree.

```tsx src/components/Seo.tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import type { CommerceSeoHead } from "@commercengine/seo";

export function Seo({ head }: { head: CommerceSeoHead | null }) {
  if (!head) return null;

  return (
    <>
      <title>{head.title}</title>
      {head.meta.map((tag) =>
        tag.property ? (
          <meta
            key={`${tag.property}:${tag.content}`}
            property={tag.property}
            content={tag.content}
          />
        ) : (
          <meta
            key={`${tag.name}:${tag.content}`}
            name={tag.name}
            content={tag.content}
          />
        )
      )}
      {head.links.map((tag) => (
        <link
          key={`${tag.rel}:${tag.href}`}
          rel={tag.rel}
          href={tag.href}
          type={tag.type}
        />
      ))}
      {head.scripts.map((script) => (
        <script
          key={script.content.slice(0, 64)}
          type={script.type}
          dangerouslySetInnerHTML={{ __html: script.content }}
        />
      ))}
    </>
  );
}
```

<Warning>
  React does not deduplicate these tags against static metadata in `index.html`. Remove generic product/page Open Graph tags or mark and retire them when page-specific head data exists. Otherwise crawlers and social platforms can read the first, generic tag instead of the product tag.
</Warning>

The Commerce Engine starters demonstrate a safe `data-seo-default` pattern that removes only overridden defaults and restores them on unmount.

## 4. Generate SEO assets before Vite builds

A SPA cannot answer `Accept: text/markdown` or generate `robots.txt` on demand. Write physical files into `public/` before `vite build`.

Run the prebuild as TypeScript so it can import the exact same pure configuration used by the app and AI tools. Add a TypeScript executor and a small env loader:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pnpm add -D tsx dotenv
```

```typescript scripts/generate-seo-assets.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { existsSync } from "node:fs";
import { createCommerceSeo } from "@commercengine/seo";
import { writeCommerceSeoAssets } from "@commercengine/seo/build";
import { createStorefront, Environment } from "@commercengine/storefront";
import { config as loadEnvFiles } from "dotenv";
import { commerceSeo } from "../src/lib/commerce-seo.config";

// This script runs before Vite. Load the files Vite would use for a production
// build; dotenv leaves already-injected process.env values untouched, so CI wins.
const envFiles = [
  ".env.production.local",
  ".env.local",
  ".env.production",
  ".env",
].filter(existsSync);
if (envFiles.length) loadEnvFiles({ path: envFiles });

const storeId = process.env.VITE_STORE_ID;
const apiKey = process.env.VITE_API_KEY;
if (!storeId || !apiKey) {
  throw new Error("[seo] missing VITE_STORE_ID / VITE_API_KEY");
}

const seo = createCommerceSeo({
  ...commerceSeo,
  storefront: createStorefront({
    storeId,
    apiKey,
    environment:
      process.env.VITE_CE_ENV === "production"
        ? Environment.Production
        : Environment.Staging,
  }),
});

await writeCommerceSeoAssets(seo, { outDir: "public" });
```

```json package.json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "scripts": {
    "build": "tsx scripts/generate-seo-assets.ts && vite build"
  }
}
```

Do not hard-code `indexable: true` in this shared build script. With the option omitted, Vercel, Netlify, Cloudflare, and generic Node production/preview signals remain authoritative. A local build with no reliable deployment signal is intentionally generated as non-indexable.

Generated files are build artifacts. Ignore them instead of committing them:

```gitignore theme={"theme":{"light":"github-light","dark":"github-dark"}}
/public/robots.txt
/public/sitemap.xml
/public/sitemap.md
/public/llms.txt
/public/product/
/public/category/
```

If the app is deployed behind a static host, configure deep-link fallback to `index.html`. Static files such as `.md`, `robots.txt`, and `sitemap.xml` are normally matched before that rewrite.

## 5. Register AI tools

Registration does not need to wait for the session. Mount it from app startup or a root effect and abort it on teardown.

```typescript src/lib/register-agent-tools.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createHostedCheckoutBridge } from "@commercengine/ai/checkout";
import { registerCommerceWebMcp } from "@commercengine/ai/webmcp";
import { getCheckout } from "@commercengine/checkout";
import { routes, site } from "./commerce-seo.config";
import { storefront } from "./storefront";

let registration: AbortController | null = null;

export async function registerAgentTools(navigate: (url: string) => void) {
  registration?.abort();
  registration = await registerCommerceWebMcp({
    storefront,
    siteUrl: site.url,
    routes,
    checkout: createHostedCheckoutBridge({ getState: () => getCheckout() }),
    navigation: { navigate },
    diagnostics: import.meta.env.DEV
      ? (event) => console.info("[commerce-ai]", event.code, event.message ?? "")
      : undefined,
  });

  return registration;
}
```

The browser can remain fully functional without WebMCP. `registerCommerceWebMcp()` returns `null` when the capability is unavailable. The root effect/bootstrap that calls `registerAgentTools()` should keep the returned controller and call `.abort()` during cleanup so a late registration cannot survive unmount.

## Production checklist

* build outputs `/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/sitemap.md`, and product/category `.md` files
* product pages render one product schema, not a package schema plus a hand-written duplicate
* static Open Graph defaults do not override page-specific metadata
* deep product/category URLs work on a hard reload
* development console shows a Commerce AI registration diagnostic
* AI and SEO both import the same `site` and `routes` configuration

<Card title="Reference starter" icon="github" href="https://github.com/tark-ai/ce-starter-projects/tree/main/apps/soja">
  See the Soja React/Vite storefront for the complete production pattern.
</Card>
