--- name: sfnext-seo description: >- Storefront Next SEO and AEO/GEO: the SeoMeta component (title, description, noIndex, Open Graph, X cards), JsonLd structured data, canonical URLs and the query-parameter allowlist, hreflang, configurable product/category URLs via url.seoRoutes, semantic URL builders (createProductUrl, createCategoryUrl, useSeoUrlContext), redirectToCanonicalPath, crawler rendering and pagination, and how multi-domain and base path affect generated URLs. Use when adding meta tags or JSON-LD to a route, enabling or changing seoRoutes, fixing duplicate or wrong canonical or og:url URLs, or when a build fails for a missing site in seoRoutes. Do not use for route file naming (use `storefront-next:sfnext-routing`), site/locale config in general (use `storefront-next:sfnext-configuration`), or translations (use `storefront-next:sfnext-i18n`). --- # Storefront Next SEO In-project references: `docs/README-SEO.md`, `docs/README-AEO-GEO.md`, `docs/README-MULTI-SITE.md` (URL Config), `docs/README-MULTI-DOMAIN.md`, `docs/README-BASE-PATH.md`, `docs/migrations/seo-url-rules/README.md`. Read them for depth; this skill is the task map. ## Page metadata: SeoMeta ```tsx import { SeoMeta } from '@/components/seo-meta'; {/* private/transactional pages */} {/* no " | Site Name" suffix */} ``` Props (verified in `src/components/seo-meta/index.tsx`): `title`, `rawTitle`, `description`, `noIndex`, `siteName`, `twitter`, `openGraph`. Open Graph input auto-derives X card tags unless `twitter` is set. Use `noIndex` for cart, checkout, account, and other non-public pages. Root-level canonical and hreflang descriptors come from `src/utils/seo.ts` and `src/utils/canonical-url.ts`; routes do not add them. ## Structured data: JsonLd ```tsx import { JsonLd } from '@/components/json-ld'; ``` Pass the CSP nonce so the inline script is allowed. Builders live in `src/utils/product-schema.ts`, `category-schema.ts`, `schema-url.ts`. PDP emits `Product`; PLP emits `CollectionPage` + `ItemList` (see `docs/README-AEO-GEO.md`). ## One URL everywhere Canonical ``, `og:url` and JSON-LD `url` must agree. In loaders: ```ts import { buildSeoPageUrl } from '@/lib/seo/page-url.server'; import { redirectToCanonicalPath } from '@/lib/seo/canonical-redirect.server'; redirectToCanonicalPath(requestUrl); // 301 for trailing slash const pageUrl = buildSeoPageUrl(context, requestUrl); // origin + canonical path/query ``` Only allowlisted query params survive in canonicals (`CONTENT_PARAMS` in `src/utils/canonical-url.ts`: `q`, `offset`, `sort`, `refine`, `pid` by default). Add a param only if it changes main page content, and add a test next to `canonical-url.test.ts`. ## Configurable URLs: url.seoRoutes ```ts // config.server.ts url: { prefix: '/:siteId/:localeId', seoRoutes: { RefArchGlobal: { product: { prefix: 'p' }, category: { prefix: 'c', mode: 'id-suffix' } }, }, } ``` Key rules (full playbook in [references/SEO-ROUTES.md](references/SEO-ROUTES.md)): - Build-time: changing `seoRoutes` requires a rebuild and redeploy (restart the dev server locally). - Every active site needs an entry, or URL generation throws / the build fails. - Business Manager URL settings are the source of truth; `seoRoutes` is a manual copy that must be kept in sync. - Do not rename route files. Routes are already generic; the config maps prefixes. - Build links with the semantic builders, never hardcoded `/product/...` strings: ```tsx import { createProductUrl, createCategoryUrl } from '@/route-paths'; import { useSeoUrlContext } from '@/hooks/use-seo-url-context'; const ctx = useSeoUrlContext(); ``` `seoFallback.sites` in config supplies per-site fallback behavior when a SEO URL cannot be resolved; see `docs/README-MULTI-SITE.md`. ## Multi-domain and base path - Origin for canonicals comes from `getAppOrigin` (honors forwarded host, falls back to `EXTERNAL_DOMAIN_NAME`). Each custom domain must be attached to the Managed Runtime environment. See [references/MULTI-DOMAIN-BASE-PATH.md](references/MULTI-DOMAIN-BASE-PATH.md). - Mapping domains to sites uses an `X-Site-Id` header set by your CDN. - A base path (`MRT_ENV_BASE_PATH`, set from the push config) prefixes asset and route URLs; use `getBasePath()` rather than hardcoding. ## Crawlers Crawlers receive full HTML rendering; category pages stay crawlable through a `?page=N` parameter with rel prev/next links, while the canonical stays the base category URL. Verify with `curl` and a crawler user agent. Check `docs/README-SEO.md` "Crawler Rendering and Pagination" before changing streaming behavior. ## Related Skills - `storefront-next:sfnext-routing` - route modules and URL patterns - `storefront-next:sfnext-configuration` - config.server.ts and env overrides - `storefront-next:sfnext-i18n` - locales, hreflang inputs - `storefront-next:sfnext-hybrid-storefronts` - URLs owned by another storefront - `storefront-next:sfnext-security` - CSP nonce for JSON-LD - `storefront-next:sfnext-performance` - streaming vs crawler rendering