Skip to main content
@commercengine/seo is the opinionated discovery layer for a Commerce Engine storefront. It consumes canonical Commerce Engine catalog entities and generates the surfaces search engines, answer engines, and agents need without requiring each storefront team to become a Schema.org or crawler-infrastructure expert.

Design goals

The package is built around four rules:
  1. Correct by default. Product variants, offers, canonical URLs, crawler policy, cache variation, and sitemap limits are package concerns rather than application-level toggles.
  2. One public route model. The same product/category URL resolvers feed head metadata, JSON-LD, Markdown, sitemaps, and @commercengine/ai.
  3. Deployment mode over framework. Runtime server → one request handler. Static output → one prebuild script.
  4. No parallel commerce schema. Product, variant, category, pricing, attribute, and image data come from Commerce Engine’s generated Storefront API types.

Configure site identity and routes

Keep the public definition in a browser-safe module:
src/lib/commerce-seo.config.ts
Then attach the storefront in server/build code:
src/lib/seo.ts
The framework-neutral core accepts either a first-party @commercengine/storefront wrapper or the canonical StorefrontFactory returned by createStorefront().
If you omit route configuration, the package defaults to /products/:slug, /category/:slug, and /search. The Commerce Engine starter projects explicitly set productBase: "/product" because that is their storefront URL convention. Base segments may also be / when a storefront intentionally serves an entity type at the site root; the route helpers normalize root bases to /slug rather than //slug.
Never import the module exporting seo into client code in Next.js, Astro SSR, SvelteKit SSR, or TanStack Start. Import the pure commerce-seo.config.ts instead. This prevents a server storefront from leaking into the browser bundle.

Product and category head data

Framework-neutral code can ask the instance for a complete head model:
Framework adapters convert that model to the native framework shape:
Product head data includes canonical URL, description, Markdown alternate, Open Graph, Twitter metadata, and product JSON-LD. Category helpers provide the equivalent collection metadata. The package cannot know your site hierarchy, so Product/ProductGroup schema and breadcrumbs are deliberately separate. Build the breadcrumb from resolved URLs, not string concatenation:

Structured product data

A non-variant product is emitted as Product with its Offer, price, currency, stock state, sale/strikethrough pricing, reviews when available, images, brand, and catalog attributes. A variant product is emitted as ProductGroup with hasVariant product entities and variant-specific offers. The package preserves every catalog variant axis:
  • recognized Schema.org properties such as color, size, material, pattern, and suggestedGender use their native product property
  • custom axes such as Metal, Finish, or Carat are kept in variesBy and emitted as PropertyValue entries through additionalProperty
  • the package does not invent nonexistent Schema.org URLs for merchant-specific option names
This lets a storefront describe its full merchandising model without losing custom axes or producing invalid product properties.

Custom routes and CMS-owned slugs

The simple case needs only productBase and categoryBase. Beyond that, the shape you need depends on one question: does one product have one page, or several?
Supply forward and inverse resolvers. The forward function answers “what is this product’s URL?”; the inverse maps an inbound public slug back to a Commerce Engine identifier, without which /products/editorial-speaker.md is looked up in the catalog by the literal string editorial-speaker and 404s.
Returning null from a public route resolver intentionally removes that entity from public discovery surfaces.
You can also enrich title, description, canonical URL, images, or brand without replacing the underlying Commerce Engine entity:
Returning null from a public route resolver intentionally removes that entity from public discovery surfaces.

Machine-readable storefront surfaces

The package can serve or generate:
Defaults are productBase = "/products", categoryBase = "/category", and searchPath = "/search". In the shared routes config the search key is routes.search; searchPath is the optional per-handler override. Product/category handler overrides are productPath and categoryPath regular expressions. On a server deployment, the canonical product/category URL can also return Markdown when the client prefers text/markdown. The response varies on Accept so a CDN cannot mix HTML and Markdown representations.

Server deployment

/llms.txt, /sitemap.md, Markdown mirrors, and Accept: text/markdown negotiation are always handled by the request adapter. robots.txt and XML sitemaps are deliberately opt-in so an integration cannot silently shadow routes the storefront already owns. Enable both when the Commerce Engine adapter should own them:
The framework guides show the native wrapper for Next.js, Astro, SvelteKit, and TanStack Start. Static generation is the opposite: robots and XML sitemap files are emitted by default unless you disable them explicitly.

Static deployment

Write real files before the framework build:
scripts/generate-seo-assets.mjs
Use static instead of public for SvelteKit adapter-static.

Deployment and indexability

Server versus static recipes, robots behavior, preview deployments, generated files, and deployment checks.

Indexability and preview deployments

indexable is an override, not a flag that defaults to false:
Recognized production deployments are indexable without setting the flag. Preview, development, and unknown environments resolve non-indexable. This matters because Vercel, Netlify, and Cloudflare preview builds commonly still have NODE_ENV=production. A non-indexable deployment deliberately remains crawlable: generated robots.txt allows / and advertises no sitemap, while HTML metadata and served responses carry noindex, nofollow. Blocking the crawler with Disallow: / would prevent it from reading the noindex directive. For a local static prebuild with no deployment-provider signal, explicitly setting indexable: true is appropriate only when you are intentionally generating the production artifact. For static output, the generator can also produce host-level noindex headers where supported because .md and .txt files have no HTML meta-tag equivalent.
Do not let a preview URL become your configured canonical origin. site.url is authoritative, and one wrong host poisons canonical tags, sitemaps, breadcrumbs, Markdown links, and agent navigation at once.

Remove overlapping hand-written SEO first

Before wiring the package, remove or reconcile:
  • committed/static robots.txt, sitemap.xml, llms.txt, or Markdown routes that would shadow generated ones
  • hand-authored Product JSON-LD on PDPs
  • layout-level Open Graph defaults that render before product/category-specific tags
  • product/category URL construction that bypasses seo.productUrl() or seo.categoryUrl()
These problems often fail silently: the app builds, but a crawler reads the wrong first tag or a static file shadows the package route.

Key exports

Next steps

Deployment & indexability

Choose server or static serving correctly.

AI agent tools

Add actionable WebMCP capabilities using the same routes.