Storefront Next Routing
Storefront Next uses React Router 7 framework mode with file-based routes. Every route module lives in src/routes/; the file name is the URL. Your project also ships docs/README-MULTI-SITE.md (URL prefix, locale and site detection) and docs/README-SEO.md (titles, meta tags, canonical URLs); read those for depth.
How routes are discovered
src/routes.ts imports flatRoutes from @salesforce/storefront-next-runtime/routing (not @react-router/fs-routes). It is a drop-in wrapper that:
- scans
src/routes/(test files are ignored), - merges routes from
src/extensions/<name>/routes/(seestorefront-next:sfnext-extensions), - wraps every route in the
app.url.prefixpattern (default/:siteId/:localeId) throughsrc/app-wrapper.tsx, and appliesapp.url.seoRoutesaliases to the product and category splats.
The two SLAS callback routes (resource.slas-reset-password-callback.ts, resource.slas-passwordless-login-callback.ts) are deliberately excluded from discovery in src/routes.ts and registered by hand at the bare paths /reset-password-callback and /passwordless-login-callback, outside the prefix, so a single callback URL works for all sites. If you add another server-to-server callback that cannot carry a site prefix, follow the same pattern in src/routes.ts.
Route tree (what exists)
| Pattern | Files | Notes |
|---|---|---|
| _app layout | _app.tsx | header/footer shell; loads navigation; shouldRevalidate returns false |
| Home, search, cart, wishlist | _app._index.tsx, _app.search.tsx, _app.cart.tsx, _app.wishlist.tsx, _app.about-us.tsx | |
| Product detail | _app.p.$.tsx | splat; URL shape is build-time configurable (url.seoRoutes) |
| Category / listing | _app.c.$.tsx | splat; URL shape is build-time configurable (url.seoRoutes) |
| Account | _app.account.tsx (layout + auth guard), _app.account.overview, .orders, .orders.$orderNo, .addresses, .payment-methods, .wishlist, .passkeys, .store-preferences | |
| Orders | _app.order-confirmation.$orderNo.tsx, _app.order-lookup.* (results.$orderNo, verify.$orderNo) | param is $orderNo |
| _checkout layout | _checkout.tsx, _checkout.checkout.tsx | checkout chrome |
| _empty layout | _empty.tsx, _empty.login, .signup, .forgot-password, .reset-password, .logout, .maintenance, .oauth2.jwks, .preview.component, _empty.$.tsx (catch-all) | no header/footer; there is no _auth layout |
| Mutations | action.*.ts(x) (cart, wishlist, checkout, OTP, passkey, consent, site context...) | no UI; see storefront-next:sfnext-data-fetching |
| Data endpoints | resource.*.ts(x) (recommendations, basket-products, category-products, analytics-proxy, resource.api.client.$resource.ts...) | no UI |
Naming rules (flat routes): . becomes /, $name is a param, $ alone is a splat, a leading _ segment is a pathless layout, _index is the index route. Pick the layout by prefix: _app. for standard pages, _checkout. for checkout, _empty. for header-less pages.
Add a page
// src/routes/_app.shipping-policy.tsx -> /shipping-policy
import type { Route } from './+types/_app.shipping-policy';
import { SeoMeta } from '@/components/seo-meta';
import { Link } from '@/components/link';
import { routes } from '@/route-paths';
export async function loader({ request }: Route.LoaderArgs) {
return { pageUrl: new URL(request.url).href };
}
export default function ShippingPolicy({ loaderData }: Route.ComponentProps) {
return (
<>
<SeoMeta title="Shipping policy" description="How we ship." openGraph={{ url: loaderData.pageUrl }} />
<h1>Shipping policy</h1>
<Link to={routes.cart}>Back to cart</Link>
</>
);
}
Checklist for a new page:
- Create the file under
src/routes/with the right layout prefix. - Import types from
./+types/<file-name>(Route.LoaderArgs,Route.ComponentProps,Route.ActionArgs). These are generated bypnpm typecheck(runsreact-router typegen) andpnpm dev; run one if the import is unresolved. - Add an entry to
routes(page) orresourceRoutes(action/resource) insrc/route-paths.ts. That file is the single source of truth for navigable paths. Keep it in sync when you rename or delete route files. - Render
<SeoMeta>(and<JsonLd>from@/components/json-ldfor structured data) inside the component. React 19 hoists these tags to<head>and they work with streaming. Usemetaexports only when a route already does. - Add locale strings for user-facing text (
storefront-next:sfnext-i18n) and a test next to the route (storefront-next:sfnext-testing).
Links and navigation: always use the project wrappers
import { Link, NavLink } from '@/components/link';
import { useNavigate } from '@/hooks/use-navigate';
import { href } from 'react-router';
import { routes, routeHref, resourceRoutes } from '@/route-paths';
<Link to={routes.cart}>Cart</Link>
<Link to={routeHref(routes.accountOrderDetail, { orderNo })}>Order</Link>
Link, NavLink and useNavigate from @/components/link and @/hooks/use-navigate apply the site/locale prefix (buildUrl). The React Router originals compile but silently produce unprefixed URLs that break multi-site routing. AGENTS.md makes this a hard rule.
For product and category URLs use the helpers in @/route-paths (createProductUrl, createCategoryUrl, createCategoryNavigationUrl) with the SEO URL context so links follow url.seoRoutes. In server code (redirects), use buildUrlFromContext(routes.login, context) from @/lib/url.server:
import { redirect } from 'react-router';
import { routes } from '@/route-paths';
import { buildUrlFromContext } from '@/lib/url.server';
throw redirect(buildUrlFromContext(routes.login, context));
Form and fetcher action targets come from resourceRoutes (for example resourceRoutes.cartItemAdd), which are excluded from the site prefix via app.url.excludeRoutes in config.server.ts (['/resource/**', '/action/**'] by default). A new action.* or resource.* route needs no prefix handling; a new page route gets it automatically.
URL configuration
app.url in config.server.ts (prefix, excludeRoutes and seoRoutes are protected config paths: set them in the file and rebuild, they cannot be overridden with PUBLIC__ env vars; see storefront-next:sfnext-configuration) controls prefix, excludeRoutes, search-param mode, detection and seoRoutes (configurable product and category URL grammars). Changing the prefix or seoRoutes affects every link; follow docs/README-MULTI-SITE.md in your project rather than editing ad hoc.
Protecting routes
There is no _auth layout. Guard in the loader: read the session with getAuth(context) from @/middlewares/auth.server and throw redirect(...) to routes.login (see _app.account.tsx and _app.wishlist.tsx). Auth details: storefront-next:sfnext-authentication.
Rules to keep in mind
- Only server
loaderandactionexports are allowed; noclientLoader/clientAction. - Throw
new Response('...', { status: 404 })for missing content so the right HTTP status reaches crawlers. - Resource routes called from the browser should reject cross-origin requests (see
resource.recommendations.tsfor the pattern). - Deeper references: ROUTE-CONVENTIONS.md for exports and naming, URLS-AND-SEO-ROUTES.md for prefixes and SEO aliases.
Finding more
Read AGENTS.md in your project for the doc index. For other framework questions use b2c docs search "storefront next routing" (or the docs_search MCP tool).
Related Skills
storefront-next:sfnext-data-fetching- loaders, actions, fetchersstorefront-next:sfnext-revalidation-shouldRevalidatepolicy per routestorefront-next:sfnext-seo- canonical URLs, structured data, sitemapsstorefront-next:sfnext-page-designer-@PageTypeand regions on routesstorefront-next:sfnext-extensions- routes contributed by extensionsstorefront-next:sfnext-authentication- sessions and login flowsstorefront-next:sfnext-overview- architecture map