Does an application server receive requests after deployment?That determines whether Commerce Engine should generate discovery responses on demand or write them as files at build time.
Decision matrix
Both modes produce the same discovery model. The difference is only where it is materialized.
Server mode
Use one framework handler and enable robots + sitemap:createCommerceSeoRequestHandler() from @commercengine/seo/server provides the same contract for other runtimes that use standard Web Request and Response objects.
A request handler always owns:
- HTML/Markdown content negotiation for product/category/search routes
- explicit
.mdmirrors /llms.txt/sitemap.md
/robots.txt only when robots: true is enabled, and /sitemap.xml plus /sitemap/{id}.xml shards only when sitemap: true is enabled. Both server-mode options default to false so the package cannot silently shadow existing application routes.
Why Vary: Accept matters
When the same URL can return HTML or Markdown, the HTML response must vary on Accept; otherwise a shared cache can serve the wrong representation to the next client. Commerce Engine adapters scope this header only to URLs that actually negotiate Markdown so unrelated CDN cache keys do not fragment.
Explicit .md URLs remain the primary discovery links and work even when an AI client cannot set custom Accept headers.
Static mode
A static host cannot inspectAccept, run catalog queries, or build arbitrary search results after deployment. Generate real files before the framework build:
Keep site identity and public routes in the same pure commerceSeo config the application and AI tools already import. A TypeScript prebuild can reuse that module directly:
scripts/generate-seo-assets.ts
indexable unset in the shared build script. The package then uses Vercel, Netlify, Cloudflare, or generic Node deployment signals: preview builds remain non-indexable, while proven production builds become indexable automatically.
Run that script before the framework build, for example:
package.json
&& (astro build, next build, and so on).
Output directory
Generated files are artifacts and should generally be gitignored.
Why physical files instead of catch-all routes
Framework routers prioritize concrete product routes. A/product/[slug] route can claim /product/shoe.md before a catch-all Markdown route sees it. Files placed in the publish directory bypass the application router entirely.
Static assets generated
By default the static generator can produce:/products and /category; the examples in these docs configure /product explicitly to match the Commerce Engine starter route convention.
Optional variant mirrors can be generated when variant public URLs use distinct paths. Query-string variants usually reuse the base product .md file rather than multiplying the file count.
The static generator builds sitemap.md and XML sitemap output from the assets it actually emits, so intentionally unroutable catalog entities are not advertised as dead links.
Production versus preview indexability
createCommerceSeo() resolves one indexable value and applies it consistently across head metadata, robots behavior, dynamic responses, and static output.
indexable is an explicit override:
Static noindex protection
HTML pages can carry robots metadata. Markdown and text files cannot. On a non-indexable static build, the package can emit a_headers file with a site-wide X-Robots-Tag: noindex, nofollow rule for hosts such as Netlify and Cloudflare Pages.
If your host does not understand _headers, apply equivalent response headers in that host’s configuration.
Robots policy
Production robots should make public storefront pages crawlable while keeping operational/session URLs out of discovery. Use package policy unless your application has a specific reason to override it. A non-indexable deployment intentionally emits an allow-all robots policy with no sitemap while its pages/responses carrynoindex, nofollow. Do not replace that with Disallow: /: a crawler blocked from fetching the page cannot read the noindex directive.
Never use robots.txt as the only preview protection. robots.txt controls crawling; page/header directives control indexability.
Sitemap limits
XML sitemaps are limited to 50,000 URLs per file. Commerce Engine automatically shards larger catalogs and produces a sitemap index. You can reduce the maximum during testing:Build credentials
Static SEO generation runs before the app build and needs public catalog credentials. Local development may use.env.local; CI usually injects environment variables directly and has no local env file.
A robust prebuild script:
- reads the local file only when it exists
- overlays/accepts CI environment variables
- fails explicitly if Store ID or API key is missing
- never emits an empty “valid-looking” sitemap after an authentication failure
Existing-file conflicts
Before adding the package, remove overlapping files/routes:- static
robots.txt - static or hand-built
sitemap.xml - custom
llms.txt - old Markdown route handlers
- duplicate JSON-LD generators
public/robots.txt can shadow a perfectly correct dynamic robots.txt with no warning.
Deep-link hosting checks
SEO creates links to real pages, so production host routing must resolve them.SPA
Configure a catch-all rewrite toindex.html after static-file matching. Hard-refresh a product/category URL as part of deployment validation.
SvelteKit adapter-static on Vercel
Use:vercel.json
/privacy-policy.html works but /privacy-policy 404s, the problem is host routing rather than the Svelte build.
Validate the deployed site
Check the deployed production origin, not only localhost:- canonical URLs use the real production domain
- sitemap URLs hard-load successfully
- preview builds return noindex protection
- product/category Markdown links resolve
- only one product structured-data entity is present per page
- social preview tags use product/category values rather than generic site defaults
SEO package
Structured data and discovery API.
Framework guides
Framework-specific wiring.