Configure Multisite URLs
Storefront Next supports serving multiple B2C Commerce sites and locales from a single deployment. This configuration applies whether you operate a single site or multiple sites—in both cases, the multisite middleware handles site and locale resolution. Configure how site and locale identifiers appear in your URLs—in the path, as query parameters, or a combination of both. For single-site storefronts, you can omit identifiers from the URL entirely and let the middleware resolve everything from cookies and headers.
Multisite configuration controls how site and locale identifiers appear in your storefront URLs. The url config determines the URL pattern, and the detection config determines how the middleware reads site and locale back from incoming requests.
Configuration is in config.server.ts under three areas:
commerce.sitesdefines the available sites and their supported locales.defaultSiteIddetermines the fallback site when no site can be resolved from the URL, cookie, or header.urldefines the URL pattern, which includes the path prefix, query parameters, and excluded routes.
The Storefront Next template defaults to a convention where all URLs—including the homepage—use site and locale prefixes (for example, /global/en-GB/ for the homepage, /global/en-GB/product/123 for subpages). Requests to bare / are redirected server-side to the default site and locale prefix. This makes all URLs shareable and fully deterministic.
Homepage (/global/en-GB/) | Subpages | |
|---|---|---|
| URL | Prefixed with site/locale | Includes site/locale prefix or query params |
| Site resolution | From URL path or query params | From URL path or query params |
| Shareable? | Yes (deterministic from URL) | Yes (deterministic from URL) |
The bare / redirect is handled in the homepage loader (_app._index.tsx), so you can customize the redirect behavior as needed.
Sites and locales are defined under commerce.sites in config.server.ts. By default, sites and their locales are retrieved from the MRT Data Store. See MRT Data Store Sites. If the MRT Data Store sites option is turned off, sites and locales are retrieved from config.server.ts.
Each field serves a specific purpose:
id— The B2C Commerce site ID. This must match the site ID configured in Business Manager.defaultLocale— The locale used when no locale can be resolved from the URL, cookie, or header.defaultCurrency— The fallback currency when no currency preference exists.supportedLocales— An array of locales available for this site. Each entry includes anidand apreferredCurrencythat is applied when the user selects that locale.supportedCurrencies— The currencies available for manual selection on this site.defaultSiteId— Defined at theapplevel, this determines which site is used on a first visit when nosite_idcookie exists.
Keep the i18n.supportedLngs array in sync with the locale id values across all entries in commerce.sites[].supportedLocales. A mismatch causes the locale switcher to show unsupported locales or hide valid ones.
On by default. When commerce.sitesFromDal is on, live site data synced through the MRT Data Store replaces the static commerce.sites for site, locale, and currency resolution, resolved per request. defaultSiteId, siteAliasMap, and localeAliasMap stay static and derived from config, never from the MRT Data Store. Set the flag to false to keep the static commerce.sites authoritative.
Fallback behavior. When the middleware can’t get site data from the MRT Data Store, the storefront keeps serving the static commerce.sites and doesn’t fail the request. This fallback applies whenever commerce.sitesFromDal is off, the MRT Data Store entry is unavailable, the payload yields no usable sites, or the usable sites omit the site named by defaultSiteId. That last case logs a warning naming the missing default and the site IDs actually present, so the issue is visible in monitoring.
URL aliasing stays config-owned. The MRT Data Store supplies which sites exist and their locale and currency data, but not how their URLs are aliased. siteContextMiddleware runs after the data store rewrite and derives each resolved site’s routing alias from the config siteAliasMap, keyed by site id (the same key for the data store and static sites), so siteAliasMap and localeAliasMap stay the config-owned source for the :siteId and :localeId URL refs. A per-site alias on the data store payload would be overwritten before routing reads it, so the rewrite drops it at the source. This is what keeps multi-site URLs stable when sites go live from the data store.
The url config in config.server.ts controls how multisite URLs are constructed.
prefix— Path segments prepended to subpage URLs. Use:siteIdand:localeIdas placeholders that are replaced with the resolved values at URL build time.search— Query parameters appended to subpage URLs. Use the same:siteIdand:localeIdplaceholder syntax.excludeRoutes— Glob patterns for routes that skip prefixing (e.g., API resource routes and server actions).seoRoutes— Business Manager-mirrored product, category, and optional standalone-content prefixes keyed by Commerce site ID. Prefixes are static segments without slashes. Category mode isid-suffixorslug-path.
Both prefix and search are optional. Use either, both, or neither depending on your URL strategy.
When url.search is configured, buildUrl injects the search params using searchParams.set(). This overwrites any existing query param with the same key, but preserves all other query params. For example, a product search URL like /product?q=shoes with search: '?lng=:localeId' resolves to /global/product?q=shoes&lng=en-GB—the existing q param is preserved and lng is appended. If your URL already contained a lng param, the configured value takes precedence.
url.prefix, url.excludeRoutes, and url.seoRoutes are protected configuration paths. They cannot be overridden via PUBLIC__ environment variables at runtime. Changing these values requires updating config.server.ts and rebuilding the application. React Router compiles these values into the route structure.
When configured, seoRoutes registers the union of every site’s product, category, and content prefixes as static {prefix}/* routes. A build fails if a prefix is invalid, reserved, shared across resource types, or collides with another route branch. Product, category, and standalone-content route modules must be leaf routes when aliases are enabled.
Use the semantic builders for product, category, and standalone-content destinations. They return only the functional path; the storefront Link and navigation wrappers add the outer site and locale prefix.
Product builders preserve slug hierarchies, remove a terminal Commerce-generated .html suffix, and encode each path segment. If the product ID itself ends in .html, the builder emits .html.html; the resolver removes only the final extension and preserves the complete ID. In slug-path mode, use authoritative Category.slug or PathRecord.slug; don’t substitute an ID or display name. Category suggestions without an authoritative slug link to search by category ID instead. For merchant-authored legacy category paths, use createCategoryUrlFromLegacyPath().
Every active site must have an seoRoutes entry. Do not enable it until every site’s PDP and PLP grammar is decided. The slug-path mode requires Shopper Products 1.13 and Shopper Search 1.15.
Configuring content.prefix enables /{content-prefix}/content/{optional-slug-path}/{contentId} and /{content-prefix}/page/{optional-slug-path}/{pageId}. The explicit content or page segment selects the Shopper Experience API. A trailing .html is permanently redirected to the suffix-free canonical URL.
For a standalone Page Designer page with an above-the-fold region, expose a string criticalRegionId in the page type’s data and set it to that top-level region ID. The route marks only that region as critical; all other regions keep their streaming behavior.
Business Manager is the source of truth for SEO URL grammar. Map its product, category, category-mode, and optional content prefixes to the corresponding seoRoutes.<siteId> values. This mapping is manual: when a Business Manager setting changes, update config.server.ts, rebuild, and redeploy.
| Business Manager (in Merchant Tools > Site > SEO & Discoverability) | config.server.ts |
|---|---|
| Product URL segment / prefix | seoRoutes.<siteId>.product.prefix |
| Category URL segment / prefix | seoRoutes.<siteId>.category.prefix |
| Category URLs carry the category ID vs. a pure slug path | seoRoutes.<siteId>.category.mode (id-suffix | slug-path) |
| Content/landing URL segment / prefix | seoRoutes.<siteId>.content.prefix (optional) |
| Site and locale in the path | url.prefix (for example, /:siteId/:localeId) |
| Locale-to-alias display | localeAliasMap / siteAliasMap |
Keep Business Manager URL Rules, url.seoRoutes, and seoFallback.sites aligned. For an otherwise-unmatched GET or HEAD, the terminal route can make one Shopper SEO URL Mapping call without the incoming query string, retries, or mapping-chain traversal. Valid product, category, owned-content, URL redirect, and hybrid mappings redirect through the configured builders; mapping misses and rejected results remain 404 responses.
External redirect origins and forwarded query parameters are denied unless the active Commerce site explicitly allows them in seoFallback.sites. Origins must be exact HTTPS origins. contentOwned authorizes fallback mappings only; direct deterministic content routes don’t call URL Mapping.
Map B2C Commerce site IDs and locale IDs to shorter, URL-friendly aliases. Define these at the app level in config.server.ts.
With siteAliasMap, the site RefArchGlobal appears as global in URLs: /global/en-GB/product/123
With localeAliasMap, the locale en-GB appears as gb: /global/gb/product/123
Both alias maps are optional. Without them, the raw B2C Commerce IDs appear in URLs.
The URL config (prefix, search) controls how URLs are built. The detection config controls how site and locale are read back from incoming requests. These must stay in sync.
The default detection config is:
The middleware checks each source in the specified order and uses the first match.
Each field controls a specific detection behavior:
order—The priority sequence for resolving the value. The middleware tries each source in order and uses the first match. For example,['path', 'querystring', 'cookie', 'header']checks the URL path first, then query parameters, then cookies, then HTTP headers.lookupFromPathIndex—Which URL path segment to read when'path'is in theorder.0means the first segment (e.g.,/global/en-GB/...→'global'),1means the second segment (e.g.,/global/en-GB/...→'en-GB').lookupQuerystring—The query parameter name to check when'querystring'is in theorder. For example,'site'looks for?site=global, and'lng'looks for?lng=en-GB.lookupCookie—The cookie name to check when'cookie'is in theorder. For example,'site_id'checks thesite_idcookie, and'lng'checks thelngcookie.lookupHeader—The HTTP header to check when'header'is in theorder. For example,'X-Site-Id'checks theX-Site-Idrequest header, and'Accept-Language'checks theAccept-Languageheader.caches—Where to persist the resolved value.['cookie']means the resolved value is stored in a cookie for subsequent requests, ensuring consistency across page navigations.
The query param key for locale must be lng to match the i18next cookie key and default detection config. The query param key for site defaults to site. If you use different keys in your url.search config, update the corresponding lookupQuerystring value in the detection config.
The siteContext config in config.server.ts controls how site-context cookies are stored. Site-context cookies are site, locale, and currency cookies. The siteContext fields are optional.
currencyCookieName—The cookie name used to persist the shopper’s selected currency. Defaults to'currency'.cookieOptions—Cookie attributes that are applied to all three site-context cookies: site, locale, and currency. Valid options includehttpOnly,maxAge,secure,sameSite,path,domain, and so on. These options follow the standard Cookie options from React Router. Defaults to{ path: '/', sameSite: 'lax', secure: true, httpOnly: true }.
Below are common URL patterns you can achieve by combining prefix, search, and the corresponding detection config overrides. The available placeholders are :siteId and :localeId, which are resolved from the current site and locale, after alias mapping.
This is the default configuration. Both site and locale are visible in every URL, including the homepage. The default detection config expects site at path index 0 and locale at path index 1, which matches this pattern. No detection config override is needed.
Best for: Most multisite storefronts. Clean, fully deterministic URLs.
| Page | URL |
|---|---|
| Homepage (RefArchGlobal, en-GB) | /global/en-GB/ |
| Product (RefArchGlobal, en-GB) | /global/en-GB/product/123 |
| Product (RefArch, en-US) | /us/en-US/product/123 |
| Category (RefArchGlobal, it-IT) | /global/it-IT/category/womens |
With this pattern, the site is resolved via the detection fallback chain. You must update localeDetectionConfig to set lookupFromPathIndex: 0, because the locale is now the first path segment instead of the second. It is also recommended to explicitly remove 'path' from siteDetectionConfig.order so that the middleware does not attempt to interpret the locale string (for example, en-GB) as a site ID.
Best for: Single-site storefronts with multiple locales, or when the site is determined by cookie or domain.
| Page | URL |
|---|---|
| Homepage (en-GB) | /en-GB/ |
| Product (en-GB) | /en-GB/product/123 |
| Product (en-US) | /en-US/product/123 |
| Category (it-IT) | /it-IT/category/womens |
The site is resolved from path index 0. Because the locale is not in the path, remove path from its detection order so the middleware reads ?lng= before falling back to the cookie or header.
Best for: Shorter path segments while keeping locale in the URL for shareability.
| Page | URL |
|---|---|
| Homepage (RefArchGlobal, en-GB) | /global/?lng=en-GB |
| Product (RefArchGlobal, en-GB) | /global/product/123?lng=en-GB |
| Product (RefArch, en-US) | /us/product/123?lng=en-US |
| Category (RefArchGlobal, it-IT) | /global/category/womens?lng=it-IT |
Without a prefix, React Router doesn’t need site or locale route params in its route definitions. Remove path from both detection orders so the middleware reads the query parameters before cookies or headers.
Best for: Storefronts that want clean paths and don’t mind query params.
| Page | URL |
|---|---|
| Homepage | /?site=global&lng=en-GB |
| Product | /product/123?site=global&lng=en-GB |
| Category | /category/womens?site=us&lng=en-US |
Remove path from both detection orders. The locale resolves from ?lng= and the site resolves from the query string when configured, then from the cookie or header.
Best for: Single-site storefronts that want locale-aware URLs without path changes.
| Page | URL |
|---|---|
| Homepage | /?lng=en-GB |
| Product | /product/123?lng=en-GB |
| Category | /category/womens?lng=it-IT |
This configuration produces the cleanest possible URLs with no site or locale identifiers at all. The multisite middleware still runs, but it resolves site and locale entirely from cookies and headers using the detection fallback chain. On a first visit with no cookies, the defaultSiteId and defaultLocale values from config.server.ts determine which site and locale are used.
Remove path from both detection orders because neither value is represented in the URL path. With one site and one locale, the defaults are used on the first visit and persisted in cookies for subsequent requests.
Best for: Single-site, single-locale storefronts that don’t need site or locale identifiers in the URL.
| Page | URL |
|---|---|
| Homepage | / |
| Product | /product/123 |
| Category | /category/womens |
The Storefront Next template includes built-in switcher components for site, locale, and currency. Each switcher posts to the /action/set-site-context server action, which sets the appropriate cookies and redirects to the updated URL.
The site switcher component is at src/components/site-switcher. When a shopper selects a new site:
- The dropdown calls
i18n.changeLanguagewith the default locale of the new site. - A
POSTis submitted to/action/set-site-contextwith the newsiteIdandlocaleId. - The server action sets the
site_idandlngcookies and issues a redirect. - The page reloads with the new site and locale applied.
The locale switcher component is at src/components/locale-switcher. When a shopper selects a new locale:
- The current URL path is stripped of its site/locale prefix using
stripPathPrefix. - The new URL is rebuilt with the updated locale using
buildUrl. i18n.changeLanguageis called with the new locale string.- A
POSTis submitted to/action/set-site-contextwith the updatedlocaleId. - The server action sets the
lngcookie. - React Router triggers a full loader revalidation so all page data is reloaded with the new locale context.
When a shopper selects a currency, the currency switcher posts the new currency to /action/set-site-context, which stores it in the currency cookie (default name: currency). React Router then automatically revalidates all loaders, updating prices across the page.
The active currency is resolved using the following priority order:
| Priority | Source |
|---|---|
| 1 | Currency cookie (shopper’s explicit selection) |
| 2 | preferredCurrency from the active locale config |
| 3 | defaultCurrency from the active site config |
Use useSite() to access the current site, language, and currency in React components:
useSite() throws if called outside a SiteProvider. In the template, SiteProvider is mounted in root.tsx and wraps the entire app.
useSite also returns the locale object that contains the i18next language string, but Storefront Next doesn’t use it for locale switching. It uses the language value, which represents the language for the app.
Storefront Next provides multisite-aware navigation utilities that automatically prepend the active site and locale prefix to every URL. Always use these instead of the standard React Router equivalents.
Import Link and NavLink from @/components/link instead of from React Router. These components are drop-in replacements that automatically prefix paths with the active site and locale.
External URLs are passed through unchanged. Object to values with rooted pathnames are site-prefixed. Search-only strings and pathname-less objects with search resolve against the current route before URL context is applied. Empty, hash-only, and relative object targets retain React Router’s native resolution.
Import useNavigate from @/hooks/use-navigate instead of from React Router. The returned navigate function applies the site and locale prefix automatically.
Use the useCurrentSiteAndLocaleRef hook when you need to build URLs manually outside of Link or useNavigate:
React Router’s <Form> component does not go through buildUrl. If you pass an action prop to <Form>, you must prefix it manually:
Using <Form action="/some-path"> without prefixing produces a URL without the site/locale prefix, which causes a 404 error.
All redirect() calls in loaders and actions must include the site-context prefix. Use buildUrlFromContext, which reads the resolved site and locale from router context.
Engagement adapters are initialized once at application startup with static configuration from config.server.ts. The current site and locale are injected at event-send time through EventSiteInfo, resolved from the site-context middleware.
useAnalytics gets the active { site, language } from useSite(), constructs { siteId: site.id, localeId: language }, and passes it to the event mediator for every tracking call.
| Adapter | Site-context aware? | How it uses site context |
|---|---|---|
| Active Data | Yes | Uses the event-time site ID and locale ID to build its endpoint URL. |
| Einstein | No | Uses the static site ID from configuration at initialization. |
| Data 360 | Yes | Prefers the event-time site ID for siteId and internalOrganizationId, then falls back to configured site ID. |
Engagement adapter configuration is protected. To change it, update config.server.ts, rebuild, and redeploy.
-
Homepage URL uses site and locale prefix. All URLs—including the homepage—use the configured site and locale prefix (e.g.,
/global/en-GB/). Requests to bare/are redirected server-side to the default site and locale prefix. This redirect is handled in the homepage loader (_app._index.tsx) and can be customized. -
URL configuration changes require a rebuild.
url.prefix,url.excludeRoutes, andurl.seoRoutesare protected paths. They can’t be changed viaPUBLIC__environment variables. Updateconfig.server.tsand rebuild the application. -
Keep URL construction and detection synchronized. When a site or locale is no longer represented in the path, remove
pathfrom that value’s detection order. When path indexes or query parameter names change, update the corresponding detection configuration. -
Query param keys are not arbitrary. The locale query param key must be
lng(matching the i18next cookie). The site key defaults tosite. Custom keys require matching updates to the detection config. -
Use multisite-aware navigation. Always use
LinkandNavLinkfrom@/components/link,useNavigatefrom@/hooks/use-navigate, andbuildUrlFromContextfor server-side redirects. These automatically apply the URL prefix. Using React Router’sLinkorredirectdirectly produces URLs without the prefix, resulting in 404 errors.In loaders and actions, use
buildUrlFromContextfrom@/lib/url.serverto build redirect URLs: -
Keep i18n and site config in sync. The locale IDs in your
i18n.supportedLngsmust match theidvalues incommerce.sites[].supportedLocales. A mismatch causes the locale switcher to show unsupported locales or hide valid ones. -
Always include framework routes in excludeRoutes. The
/resource/**and/action/**patterns must remain inexcludeRoutes. Removing them causes resource routes and server actions to be incorrectly prefixed with site/locale segments. -
Prefix React Router Form action values manually. React Router’s
<Form>component does not go throughbuildUrl. If you use<Form action="/some-path">, prefix the action yourself usingbuildUrlwithuseConfiganduseCurrentSiteAndLocaleRef. See Navigate with Site Context for a code example.