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

# Astro

> Use Commerce Engine SEO head helpers, server middleware or static prebuild assets, and WebMCP tools with Astro View Transitions.

Astro can be a server application or a static site. Choose the SEO serving mechanism from the deployment mode, not from the Astro logo:

* `output: "server"` or on-demand SEO routes → mount one middleware
* default/static output → write SEO assets into `public/` before `astro build`

Both modes use the same head helpers and the same client-side AI registration.

## 1. Shared config and SEO instance

```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;
```

```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 });
```

## 2. Render Astro-native head data

```astro src/pages/product/[slug].astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
---
import { createAstroProductHead } from "@commercengine/seo/astro";
import { seo } from "../../lib/seo";
import { publicSdk } from "../../lib/storefront";

const { slug } = Astro.params;
const { data } = await publicSdk.catalog.getProductDetail({ product_id: slug! });
const product = data?.product;
const seoHead = product ? await createAstroProductHead(seo, product) : undefined;
---

<Layout seoHead={seoHead} noindex={!product}>
  <!-- page -->
</Layout>
```

Your layout should render `seoHead.title`, `seoHead.meta`, `seoHead.links`, and `seoHead.scripts`. Do not also render generic product/page Open Graph tags when `seoHead` is present.

### Add the breadcrumb separately

```astro theme={"theme":{"light":"github-light","dark":"github-dark"}}
---
import { serializeJsonLd } from "@commercengine/seo";

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

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

{breadcrumb && (
  <script is:inline type="application/ld+json" set:html={serializeJsonLd(breadcrumb)} />
)}
```

<Warning>
  Use `seo.productUrl()` and `seo.categoryUrl()` for breadcrumbs. Hard-coded `/product/` or `/category/` concatenation breaks as soon as a CMS or route resolver changes the public URL.
</Warning>

## 3A. Server deployment: mount middleware

```typescript src/middleware.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createAstroSeoMiddleware } from "@commercengine/seo/astro/server";
import { seo } from "./lib/seo";

export const onRequest = createAstroSeoMiddleware(seo, {
  robots: true,
  sitemap: true,
});
```

The middleware handles Markdown mirrors, content negotiation, `llms.txt`, `sitemap.md`, robots, and XML sitemap routes. It delegates unrelated requests without adding unnecessary `Vary: Accept` cache keys.

Use this only when those requests actually reach an Astro server in production.

## 3B. Static deployment: generate physical files

For Astro's default static output, write the assets into `public/` before the build:

Use the same pure SEO config from step 1 rather than copying site identity or route bases into the build script. Run the prebuild as TypeScript and load local env files explicitly because this command runs before Astro starts:

```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";

const envFiles = [
  ".env.production.local",
  ".env.local",
  ".env.production",
  ".env",
].filter(existsSync);
if (envFiles.length) loadEnvFiles({ path: envFiles });

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

const seo = createCommerceSeo({
  ...commerceSeo,
  storefront: createStorefront({
    storeId,
    apiKey,
    environment:
      process.env.PUBLIC_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 && astro build"
  }
}
```

Do not keep `indexable: true` in a shared build script. Leaving it unset preserves the package's deployment detection, so preview CI builds stay non-indexable while a proven production deployment becomes indexable automatically.

The generated files bypass Astro's file-router priority entirely.

<Tip>
  Do not implement static `.md` mirrors as a catch-all Astro route. A concrete `/product/[slug]` route can claim `/product/shoe.md` before the catch-all gets a chance to serve it.
</Tip>

## 4. Register agent tools with the client runtime

With View Transitions, the document changes without a normal full-page reload. Register on `astro:page-load` and clean up registrations across transitions.

```typescript src/lib/client-runtime.ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
let agentTools: AbortController | null = null;

async function registerAgentTools() {
  const [
    { registerCommerceWebMcp },
    { createHostedCheckoutBridge },
    { getCheckout },
    { storefront },
    { routes, site },
  ] = await Promise.all([
    import("@commercengine/ai/webmcp"),
    import("@commercengine/ai/checkout"),
    import("@commercengine/checkout"),
    import("./storefront-client"),
    import("./commerce-seo.config"),
  ]);

  agentTools?.abort();
  agentTools = await registerCommerceWebMcp({
    storefront,
    siteUrl: site.url,
    routes,
    checkout: createHostedCheckoutBridge({ getState: () => getCheckout() }),
    navigation: { navigate: (url) => { window.location.href = url; } },
    diagnostics: import.meta.env.DEV
      ? (event) => console.info("[commerce-ai]", event.code, event.message ?? "")
      : undefined,
  });
}

document.addEventListener("astro:page-load", () => void registerAgentTools());
document.addEventListener("astro:before-swap", () => agentTools?.abort());
```

If your app has no View Transitions, registering once from a client island/runtime bootstrap is sufficient.

## Production checklist

* Astro output mode matches the SEO serving mechanism
* static deployments generate files into `public/`
* server deployments mount `createAstroSeoMiddleware`
* page layout yields generic social defaults whenever page-specific `seoHead` exists
* breadcrumb URLs come from route resolvers
* View Transition navigation does not accumulate duplicate WebMCP registrations
* development diagnostics fire on every starter page load where registration is expected

<Card title="Reference starter" icon="github" href="https://github.com/tark-ai/ce-starter-projects/tree/main/apps/soja-astro">
  Static Astro production pattern with page head data, prebuilt discovery assets, and WebMCP registration.
</Card>
