---
schema_version: "1"
id: 136157239dd54bf5ace4f161c7191dd59b16360dff3e86e1bb76ba06efa65108
title: Configure Multisite URLs
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/sfnext/guides/sfnext-multi-site.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-multi-site.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 7718c8331214d11c32027ff577f955a151fb99f54368ebd842a43bd4bfbcf5f4
products:
  - Composable Storefront
  - Salesforce APIs
---
# 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.

## How Multisite URLs Work

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.sites` defines the available sites and their supported locales.
- `defaultSiteId` determines the fallback site when no site can be resolved from the URL, cookie, or header.
- `url` defines the URL pattern, which includes the path prefix, query parameters, and excluded routes.

### Default Template Behavior

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.

## Define Your Sites

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](#mrt-data-store-sites). If the MRT Data Store sites option is turned off, sites and locales are retrieved from `config.server.ts`.

```typescript
// config.server.ts
export default defineConfig({
  app: {
    defaultSiteId: "RefArchGlobal",
    commerce: {
      sites: [
        {
          id: "RefArchGlobal",
          defaultLocale: "en-GB",
          defaultCurrency: "GBP",
          supportedLocales: [
            { id: "en-GB", preferredCurrency: "GBP" },
            { id: "fr-FR", preferredCurrency: "EUR" },
            { id: "it-IT", preferredCurrency: "EUR" },
            { id: "ja-JP", preferredCurrency: "JPY" },
            { id: "zh-CN", preferredCurrency: "CNY" },
          ],
          supportedCurrencies: ["EUR", "GBP"],
        },
        {
          id: "RefArch",
          defaultLocale: "en-US",
          defaultCurrency: "USD",
          supportedLocales: [{ id: "en-US", preferredCurrency: "USD" }],
          supportedCurrencies: ["USD"],
        },
      ],
    },
  },
});
```

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 an `id` and a `preferredCurrency` that is applied when the user selects that locale.
- **`supportedCurrencies`** — The currencies available for manual selection on this site.
- **`defaultSiteId`** — Defined at the `app` level, this determines which site is used on a first visit when no `site_id` cookie exists.

**note:** 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.

### MRT Data Store Sites

On by default. When `commerce.sitesFromDal` is on, live site data synced through the [MRT Data Store](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-mrt-data-store.md) 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.

```bash
# Opt out via env (see README-CONFIG.md) to keep static commerce.sites authoritative
# PUBLIC__app__commerce__sitesFromDal=false
```

## Configure URL Patterns

The `url` config in `config.server.ts` controls how multisite URLs are constructed.

```typescript
url: {
    prefix: '/:siteId/:localeId',
    search: '?lng=:localeId',
    excludeRoutes: ['/resource/**', '/action/**'],
}
```

- **`prefix`** — Path segments prepended to subpage URLs. Use `:siteId` and `:localeId` as placeholders that are replaced with the resolved values at URL build time.
- **`search`** — Query parameters appended to subpage URLs. Use the same `:siteId` and `:localeId` placeholder syntax.
- **`excludeRoutes`** — Glob patterns for routes that skip prefixing (e.g., API resource routes and server actions).

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.

**warning:** `url.prefix` and `url.excludeRoutes` 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. This is because the prefix determines the React Router route structure, which is baked into the build.

## Use Alias Maps for Clean URLs

Map B2C Commerce site IDs and locale IDs to shorter, URL-friendly aliases. Define these at the `app` level in `config.server.ts`.

```typescript
// config.server.ts
export default defineConfig({
  app: {
    siteAliasMap: {
      RefArchGlobal: "global",
      RefArch: "us",
    },
    localeAliasMap: {
      "en-US": "us",
      "en-GB": "gb",
    },
  },
});
```

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.

## Configure Site and Locale Detection

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:

```typescript
// Default site detection
siteDetectionConfig: {
    order: ['path', 'querystring', 'cookie', 'header'],
    lookupFromPathIndex: 0,
    lookupQuerystring: 'site',
    lookupCookie: 'site_id',
    lookupHeader: 'X-Site-Id',
    caches: ['cookie'],
}

// Default locale detection
localeDetectionConfig: {
    order: ['path', 'querystring', 'cookie', 'header'],
    lookupFromPathIndex: 1,
    lookupQuerystring: 'lng',
    lookupCookie: 'lng',
    lookupHeader: 'Accept-Language',
    caches: ['cookie'],
}
```

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 the `order`. `0` means the first segment (e.g., `/global/en-GB/...` → `'global'`), `1` means the second segment (e.g., `/global/en-GB/...` → `'en-GB'`).
- **`lookupQuerystring`**—The query parameter name to check when `'querystring'` is in the `order`. For example, `'site'` looks for `?site=global`, and `'lng'` looks for `?lng=en-GB`.
- **`lookupCookie`**—The cookie name to check when `'cookie'` is in the `order`. For example, `'site_id'` checks the `site_id` cookie, and `'lng'` checks the `lng` cookie.
- **`lookupHeader`**—The HTTP header to check when `'header'` is in the `order`. For example, `'X-Site-Id'` checks the `X-Site-Id` request header, and `'Accept-Language'` checks the `Accept-Language` header.
- **`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.

**important:** 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.

## Configure Site-Context Cookies

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.

```typescript
// config.server.ts
export default defineConfig({
  app: {
    siteContext: {
      currencyCookieName: "currency",
      cookieOptions: {
        httpOnly: true,
        secure: true,
        sameSite: "lax",
        maxAge: 604800, // 7 days
      },
    },
  },
});
```

- **`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 include `httpOnly`, `maxAge`, `secure`, `sameSite`, `path`, `domain`, and so on. These options follow the standard [Cookie](https://reactrouter.com/api/cookie) options from React Router. Defaults to `{ path: '/', sameSite: 'lax', secure: true, httpOnly: true }`.

## URL Pattern Use Cases

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.

### Site and Locale in the Path (Default)

```typescript
// config.server.ts
export default defineConfig({
  app: {
    url: {
      prefix: "/:siteId/:localeId",
      excludeRoutes: ["/resource/**", "/action/**"],
    },
    // No detection config override needed — the defaults match this URL pattern.
  },
});
```

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` |

### Locale Only in the Path

```typescript
// config.server.ts
export default defineConfig({
  app: {
    url: {
      prefix: "/:localeId",
      excludeRoutes: ["/resource/**", "/action/**"],
    },
    // Required: locale moved from path index 1 to 0.
    localeDetectionConfig: { lookupFromPathIndex: 0 },
    // Recommended: explicitly remove 'path' from site detection order so the
    // middleware doesn't attempt to read a locale string (e.g., 'en-GB') as a
    // site ID. Without this override, site detection will try path index 0,
    // fail to match a valid site, and fall through — which works but is less
    // explicit. Removing 'path' from site detection is the safer and more
    // defensive approach.
    siteDetectionConfig: { order: ["querystring", "cookie", "header"] },
  },
});
```

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` |

### Site in the Path, Locale in Query Params

```typescript
// config.server.ts
export default defineConfig({
  app: {
    url: {
      prefix: "/:siteId",
      search: "?lng=:localeId",
      excludeRoutes: ["/resource/**", "/action/**"],
    },
    // No detection config override required.
    // Site is still at path index 0 (default).
    // Locale detection tries path index 1 first (e.g., 'product'), doesn't find a valid
    // locale, and falls through to querystring where it finds ?lng=en-GB.
  },
});
```

No detection config override is required. The site is still resolved from path index 0. For locale, the default detection tries path index 1 first, doesn't find a valid locale ID there, and falls through to the `?lng=` query parameter.

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` |

### Everything in Query Params

```typescript
// config.server.ts
export default defineConfig({
  app: {
    url: {
      search: "?site=:siteId&lng=:localeId",
      excludeRoutes: ["/resource/**", "/action/**"],
    },
    // No detection config override required.
    // Path detection reads page path segments (e.g., 'product', '123'), doesn't find
    // valid site/locale IDs, and falls through to querystring where it finds
    // ?site=global&lng=en-GB.
  },
});
```

Without a `prefix`, React Router doesn't need site/locale route params in its route definitions. No detection config override is required—the default detection tries path segments first, doesn't find valid site or locale IDs, and naturally falls through to the query parameters.

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` |

### Locale Only in Query Params

```typescript
// config.server.ts
export default defineConfig({
  app: {
    url: {
      search: "?lng=:localeId",
      excludeRoutes: ["/resource/**", "/action/**"],
    },
    // No detection config override required.
    // Path detection doesn't find valid site/locale IDs in page path segments
    // and falls through. Site resolves from cookie or header; locale from ?lng=.
  },
});
```

No detection config override is required. The default detection falls through path segments to find the locale in the `?lng=` query parameter. Site is resolved from cookie or header via the same fallback chain.

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` |

### No Prefix (Single Site)

```typescript
// config.server.ts
export default defineConfig({
  app: {
    defaultSiteId: "RefArch",
    commerce: {
      sites: [
        {
          id: "RefArch",
          defaultLocale: "en-US",
          defaultCurrency: "USD",
          supportedLocales: [{ id: "en-US", preferredCurrency: "USD" }],
          supportedCurrencies: ["USD"],
        },
      ],
    },
    url: {
      prefix: "/",
      excludeRoutes: ["/resource/**", "/action/**"],
    },
    // No detection config override required.
    // Path detection doesn't find valid site/locale IDs in page path segments
    // and falls through. Both site and locale resolve from cookie or header.
  },
});
```

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.

No detection config override is required. The default detection tries path segments first, doesn't find valid site or locale IDs, and falls through to cookies and headers. Because there is only one site and one locale configured, the defaults are always used on the first visit and persisted via 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` |

## Switch Sites, Locales, and Currencies

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.

### Site Switcher

The site switcher component is at `src/components/site-switcher`. When a shopper selects a new site:

1. The dropdown calls `i18n.changeLanguage` with the default locale of the new site.
2. A `POST` is submitted to `/action/set-site-context` with the new `siteId` and `localeId`.
3. The server action sets the `site_id` and `lng` cookies and issues a redirect.
4. The page reloads with the new site and locale applied.

### Locale Switcher

The locale switcher component is at `src/components/locale-switcher`. When a shopper selects a new locale:

1. The current URL path is stripped of its site/locale prefix using `stripPathPrefix`.
2. The new URL is rebuilt with the updated locale using `buildUrl`.
3. `i18n.changeLanguage` is called with the new locale string.
4. A `POST` is submitted to `/action/set-site-context` with the updated `localeId`.
5. The server action sets the `lng` cookie.
6. React Router triggers a full loader revalidation so all page data is reloaded with the new locale context.

### Currency Switcher

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`).

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     |

## Access Site Context in React Components

Use `useSite()` to access the current site, language, and currency in React components:

```typescript
import { useSite } from "@salesforce/storefront-next-runtime/site-context";

function MyComponent() {
  const { site, language, currency } = useSite();
  // site: Site object (id, supportedLocales, supportedCurrencies, etc.)
  // language: current locale ID (e.g., 'en-GB')
  // currency: current currency code (e.g., 'GBP')
}
```

`useSite()` throws if called outside a `SiteProvider`. In the template, `SiteProvider` is mounted in `root.tsx` and wraps the entire app.

**note:** `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.

## Navigate with Site Context

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.

### Link and NavLink

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.

```typescript
import { Link, NavLink } from "@/components/link";

// Renders as /global/en-GB/product/123
<Link to="/product/123">View Product</Link>

// External URLs and non-string `to` values (e.g., objects with pathname) pass
// through unchanged, so you can use the same component for all link types.
<Link to="https://example.com">External</Link>
```

### useNavigate

Import `useNavigate` from `@/hooks/use-navigate` instead of from React Router. The returned navigate function applies the site and locale prefix automatically.

```typescript
import { useNavigate } from "@/hooks/use-navigate";

function MyComponent() {
  const navigate = useNavigate();

  // Navigate to a prefixed path string
  navigate("/product/123");

  // Navigate to root (resolves to the site/locale homepage, e.g., /global/en-GB/)
  navigate("/");

  // Navigate with an object
  navigate({ pathname: "/product/123", search: "?color=red" });

  // History navigation (back/forward) passes through unchanged
  navigate(-1);
}
```

### useCurrentSiteAndLocaleRef

Use the `useCurrentSiteAndLocaleRef` hook when you need to build URLs manually outside of Link or useNavigate:

```typescript
import { useCurrentSiteAndLocaleRef } from "@salesforce/storefront-next-runtime/site-context";
import { buildUrl } from "@/lib/url";
import { useConfig } from "@salesforce/storefront-next-runtime/config";

function MyComponent() {
  const config = useConfig();
  const currentSiteAndLocaleRef = useCurrentSiteAndLocaleRef();

  const url = buildUrl("/product/123", config.app.url, currentSiteAndLocaleRef.current);
  // url: /global/en-GB/product/123
}
```

### React Router Form Actions

React Router's `<Form>` component does **not** go through `buildUrl`. If you pass an `action` prop to `<Form>`, you must prefix it manually:

```typescript
import { Form } from "react-router";
import { buildUrl } from "@/lib/url";
import { useConfig } from "@salesforce/storefront-next-runtime/config";
import { useCurrentSiteAndLocaleRef } from "@salesforce/storefront-next-runtime/site-context";

function SearchForm() {
  const config = useConfig();
  const currentSiteAndLocaleRef = useCurrentSiteAndLocaleRef();

  const action = buildUrl("/action/search", config.app.url, currentSiteAndLocaleRef.current);

  return (
    <Form method="post" action={action}>
      ...
    </Form>
  );
}
```

**warning:** Using `<Form action="/some-path">` without prefixing produces a URL without the site/locale prefix, which causes a 404 error.

## Important Considerations

1. **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.

2. **URL prefix changes require a rebuild.** `url.prefix` and `url.excludeRoutes` are protected paths. They can't be changed via `PUBLIC__` environment variables. Update `config.server.ts` and rebuild the application.

3. **Detection config overrides are only required when path indexes shift.** The detection middleware uses a fallback chain—if it doesn't find a valid site or locale at the current source, it moves to the next one. In most non-default URL patterns, path detection simply fails to match and falls through to querystring or cookie. The only case where an override is **required** is when a path segment's position changes (e.g., locale moves from index 1 to index 0 in a locale-only prefix).

4. **Query param keys are not arbitrary.** The locale query param key must be `lng` (matching the i18next cookie). The site key defaults to `site`. Custom keys require matching updates to the detection config.

5. **Use multisite-aware navigation.** Always use `Link` and `NavLink` from `@/components/link`, `useNavigate` from `@/hooks/use-navigate`, and `buildUrlFromContext` for server-side redirects. These automatically apply the URL prefix. Using React Router's `Link` or `redirect` directly produces URLs without the prefix, resulting in 404 errors.

   In loaders and actions, use `buildUrlFromContext` from `@/lib/url.server` to build redirect URLs:

   ```typescript
   import { redirect } from "react-router";
   import { buildUrlFromContext } from "@/lib/url.server";

   export async function loader({ request, context }: LoaderFunctionArgs) {
     if (!isAuthenticated) {
       // bare redirect('/login') produces a URL without the prefix → 404
       throw redirect(buildUrlFromContext("/login", context));
     }
   }
   ```

6. **Keep i18n and site config in sync.** The locale IDs in your `i18n.supportedLngs` must match the `id` values in `commerce.sites[].supportedLocales`. A mismatch causes the locale switcher to show unsupported locales or hide valid ones.

7. **Always include framework routes in excludeRoutes.** The `/resource/**` and `/action/**` patterns must remain in `excludeRoutes`. Removing them causes resource routes and server actions to be incorrectly prefixed with site/locale segments.

8. **Prefix React Router Form action values manually.** React Router's `<Form>` component does not go through `buildUrl`. If you use `<Form action="/some-path">`, prefix the action yourself using `buildUrl` with `useConfig` and `useCurrentSiteAndLocaleRef`. See [Navigate with Site Context](#navigate-with-site-context) for a code example.

## See Also

- [Project Configuration](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-project-config.md)
- [Internationalization](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-intl.md)
- [SEO URL Rules Best Practices](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/sfnext-seo-url-rules-best-practices.md)
