@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:- Correct by default. Product variants, offers, canonical URLs, crawler policy, cache variation, and sitemap limits are package concerns rather than application-level toggles.
- One public route model. The same product/category URL resolvers feed head metadata, JSON-LD, Markdown, sitemaps, and
@commercengine/ai. - Deployment mode over framework. Runtime server → one request handler. Static output → one prebuild script.
- 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
src/lib/seo.ts
@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.Product and category head data
Framework-neutral code can ask the instance for a complete head model:Breadcrumbs stay page-aware
The package cannot know your site hierarchy, soProduct/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 asProduct 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
suggestedGenderuse their native product property - custom axes such as Metal, Finish, or Carat are kept in
variesByand emitted asPropertyValueentries throughadditionalProperty - the package does not invent nonexistent Schema.org URLs for merchant-specific option names
Custom routes and CMS-owned slugs
The simple case needs onlyproductBase and categoryBase. Beyond that, the shape you need depends on one question: does one product have one page, or several?
- One page per product
- Several pages per product
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 Returning
/products/editorial-speaker.md is looked up in the catalog by the literal string editorial-speaker and 404s.null from a public route resolver intentionally removes that entity from public discovery surfaces.null from a public route resolver intentionally removes that entity from public discovery surfaces.
Machine-readable storefront surfaces
The package can serve or generate: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:
Static deployment
Write real files before the framework build:scripts/generate-seo-assets.mjs
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:
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.
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
ProductJSON-LD on PDPs - layout-level Open Graph defaults that render before product/category-specific tags
- product/category URL construction that bypasses
seo.productUrl()orseo.categoryUrl()
Key exports
Next steps
Deployment & indexability
Choose server or static serving correctly.
AI agent tools
Add actionable WebMCP capabilities using the same routes.