1.0.0 SDK / June 2026 template
template-retail-rsc-app
Minor Changes
-
Move
<WishlistProvider>from the global_app.tsxshell to route-local mounts via a new<DeferredWishlistProvider>helper. Routes that render wishlist UI (home, PDP, PLP, search, cart) hydrate their own provider via the loader; routes without wishlist UI (about-us, account/* non-wishlist pages, order confirmation) no longer pay the SCAPIgetOrCreateWishlistcost on cold load.<WishlistMergeToast>stays at the app shell. -
Add
useScapiFetchClient/useScapiFetchHelperhooks and<WishlistProvider>: small UI mutations (e.g. PLP heart click) now hit a generic SCAPI resource route viafetchinstead ofuseFetcher, so they no longer trigger React Router loader revalidation. Wishlist initial state is seeded once at the app shell behind<Suspense>/<Await errorElement>, so SCAPI failures still render the page 200 with empty hearts. Hydrates wishlists for both guest (gcid) and registered (rcid) sessions. Provider exposes topic-scoped subscription hooks (useIsInWishlist,useWishlistEntry,useWishlistCount,useWishlistIds,useWishlistActions) backed byuseSyncExternalStoreso a single heart toggle re-renders only that one tile, not every other heart in the grid -
Tighten the
@salesforce/storefront-next-runtime/configpublic surface for V1 GA.**Removed from public exports** (followups to): - `ConfigContext` — internal React context backing `useConfig`. Read config via `useConfig` instead. - `createAppConfig` — one-line `.app` accessor with no semantic value. Use `staticConfig.app` directly. - The `middleware.ts` source file and `createAppConfigMiddleware` factory were also deleted; they had B2C-Commerce-specific validation hardcoded in (`commerce.api.{clientId,organizationId,shortCode}`, `SCAPI_PROXY_HOST`) and the retail template ships its own validating middleware. Future templates write their own. **Added** — `AppConfigShape` interface as a module-augmentation hook for `getConfig` / `useConfig` typing. Each template `declare module`s once in its own types file and gets typed access without the per-call `<AppConfig>` generic: ```ts // In your template's src/types/config.ts: declare module '@salesforce/storefront-next-runtime/config' { interface AppConfigShape extends AppConfig {} } ``` **Other changes** - `Locale`, `Site`, `Url` JSDoc reframed as opt-in baseline shapes — `BaseConfig<App>` is generic so future templates can ignore them. - `defineConfig` JSDoc now explicitly notes it reads `process.env` at call time (server-only side effect). - `mergeEnvConfig`'s engagement-specific protected-paths error message is now generic. - Retail-flavored examples in `schema.ts` / `utils.ts` JSDoc neutralized. - Template (`template-retail-rsc-app`): all 110+ `getConfig<AppConfig>` / `useConfig<AppConfig>` call sites now use the augmented hook (no per-call generic). - Template `commerce.sites: Site[]` now imports the richer `Site` shape from `@salesforce/storefront-next-runtime/site-context` (reconciles the duplicate `Site` types that existed across both subpaths). - `contact-info.tsx` migrated from `useContext(ConfigContext)` to `useConfig` (the only production caller of the bypass pattern). - README-CONFIG.md fixed: `loadConfig` subpath was wrong (`/load-config` → `/config/load-config`); `appConfigContext` now documented for custom middleware composition; `AppConfigShape` augmentation pattern documented. Public surface delta: 13 runtime + 5 type exports → 5 + 6 (plus `loadConfig` at the separate `./config/load-config` subpath). > **Semver note for future maintainers:** This entry is a `minor` bump _only because the package is still pre-V1 GA_. Removing public exports is a `major` once V1 ships — do not paste this pattern after lock-in. -
Replace current icons with actual brands logos that adhere to their design guideline
-
Centralize all route path strings into
src/route-paths.ts. Renaming a route segment (e.g.,/product→/p) now requires changing a single constant instead of updating 60+ files across the codebase. -
Support wildcard (
*) patterns inPUBLIC__app__hybrid__legacyRoutes.*matches any path content (including/) — so/categoryLv1/*matches the entire subtree instead of having to enumerate every URL. Combinable with named params (e.g./category/:cat/*), and*may appear anywhere in the pattern, not just trailing. Note that/parent/*does not match the bare/parent(no trailing slash) — list/parentseparately if you need both. Exact paths and:parampatterns are unchanged. -
Add a
.devcontainer/devcontainer.jsonso customer-generated retail projects open one-click in GitHub Codespaces or any Dev Containers-compatible IDE. Pins Node 24 (mcr.microsoft.com/devcontainers/typescript-node:24-bookworm) and activatespnpm@10.28.0via Corepack, forwards the dev server (5173, auto-opens preview) and Storybook (6006), and auto-installs the recommended VS Code extension pack (ESLint, Prettier, Tailwind, Playwright, Vitest, Salesforce DX, Commerce Vibes). The Commerce Vibes VSIX is downloaded from a versioned GitHub Release asset and verified against a sha256 checksum, so the installed extension can't drift if the upstream tag is moved or the asset replaced; the install runs inpostAttachCommandso it survives Codespace restarts that wipe/tmp. Workspace ESLint config also disablesjsonc/no-commentsfordevcontainer.jsonsince the Dev Container spec defines the file as JSONC. -
Add a heart icon to the header that takes shoppers to their wishlist. Guests land on
/wishlist; signed-in shoppers land on/account/wishlist. -
Add a public Wishlist page so guest shoppers can view items they've saved before signing in. Signed-in customers continue to see their wishlist under the account section. An empty guest wishlist now shows a Sign in link to load saved items from the account.
-
Add
action.otp-requestandaction.otp-verifyroutes for OTP verification flow -
Add Einstein product recommendations to the cart page: render "You might also like" (driven by basket items) and "Recently viewed" carousels below the cart line items, reusing the existing
<ProductRecommendations>component and theEINSTEIN_RECOMMENDERS.CART_*constants -
Add server action hooks extension system: extensions can register handlers that run at specific points in checkout server actions (fraud checks, address verification, payment tokenization, shipping method filtering) via
target-config.json -
Add support for optional hard request timeouts to the SCAPI clients
-
Added wishlist analytics events for add, remove, view, merge.
-
Align auth cookie names with PWA Kit so hybrid sf-next + SFRA storefronts share session semantics:
customerId→customer_id,encUserId→enc_user_id,cc-idp-at→idp_access_token. Sf-next now reads, writes, and clears the new names; older browsers may still hold the legacy cookies for one TTL window (up to 90 days forencUserId, ~30 min forcc-idp-at) — they are ignored and expire on their own.customer_idremains write-never (derived per request from the SLAS access token JWTisbclaim); the destroy path clears it on logout/error. HttpOnly posture and the missing PWA Kit cookies (cc-at-expires,cc-nx-expires,customer_type,idp_refresh_token,id_token,uido) are out of scope and tracked for follow-up. -
Align CSS color tokens with the Figma design system: rewrite
core.cssfrom the Figma "Market Street" tokens JSON, split nested palettes (sidebar,agentic) into dedicated files, dedupestatus.css, fix dangling Tailwind aliases, replace remaining hard-coded hex usages -
Allow guest shoppers to add and remove items from the wishlist without signing in: clicking the heart on a product no longer redirects to login, the secondary "Add to wishlist" CTA on the product detail page works for guests, and cart line wishlist hearts stay filled across refresh for guests. The guest
/wishlistpage and merge-on-sign-in flow ship in follow-up PRs. -
Block checkout when cart items exceed available inventory: added cart-wide validation that disables the "Continue to Checkout" button and displays an error banner when any items exceed available stock (ATS or store inventory for BOPIS). Supports bonus product exclusion and proper ARIA attributes for accessibility
-
Cart line right column: Reworked the
ProductItemprice column so list and sale prices stay on one horizontal row with right alignment, hid theProductPricepromotion callout on cart lines (no long promo copy under prices), and tightened the Saved badge with smaller typography plus analignEndwrapper so it sits flush right. The quantity block usesCartQuantityPickerwith aw-fitwrapper and right-aligned desktop stacking (items-end). Line-item trailing adds the gift row at the end of the right column: checkbox, "This is a gift." label, and a "Learn more" text control (no URL) styled like the label with a narrow gap between label and learn-more; locale strings drop the colon from the quantity label and carry gift copy only. -
Email Verification Feature - Account Details
-
Implement change email flow for passwordless shoppers. Passwordless shoppers are prompted for OTP verification before editing their email and again after the update to verify ownership and reauthenticate the session
-
Merge a guest's wishlist into their account wishlist on sign-in, sign-up, OTP verification, or social login. Items the shopper saved while browsing as a guest are copied to the account wishlist (skipping duplicates by product), the guest list is cleaned up, and a confirmation toast appears on the destination page. Items that fail to copy are skipped without blocking the redirect, and a partial-success message tells the shopper if anything didn't make it
-
Pre-select all quick-add swatches on product tiles from
representedProduct's variant (variationValues), so every axis (color, size, …) in the quick-add modal matches the variant the tile advertises -
Prefetch basket data when hovering over cart badge
-
Product tile links to variant PDP for add-to-cart readiness
-
Remove
passwordlessLogin.enabledconfig flag in favor of the "Enable Email Verification" site preference in Business Manager. When enabled, passwordless login, registration, and email verification for password registration will be supported -
Resolve PDP/PLP pages by aspect type instead of hardcoded page IDs.
fetchPagefalls back to SCAPI'sgetPagesendpoint when nopageIdis provided, looking up the page assigned to the given product or category viaaspectTypeId. Product and category routes now passaspectType: 'pdp'/'plp', and the Page Designer resolution middleware intercepts/pagesrequests in addition to/pages/{pageId}, wrapping the resolved page in aPageResult-shaped{ data: [page] }envelope -
Show a sign-in nudge above the login form when a guest has saved wishlist items waiting to merge
-
Show error toast when shipping address yields no available shipping methods
-
Show estimated total in mobile order summary accordion heading, and switch heading text from "Estimated Total" to "Total" once shipping and tax are known
-
Sticky Continue and Place Order buttons on mobile checkout
-
Wire MRT-based Page Designer page resolution end-to-end: when
features.mrtBasedPageDesignerResolutionis on, thepageDesignerResolutionMiddlewareSCAPI middleware now resolvesgetPagerequests against the page and site manifests stored in the MRT data store instead of round-tripping to ECOM. Synthesized responses are tagged with anx-page-manifest-hitresponse header so observability tooling can tell at a glance which path served the response. Design/preview requests (those carryingmodeorpdTokenquery params) still fall through to SCAPI. A newSFCC_PD_PAGE_RESOLUTION_DEBUG=trueenv gate enables a parity-troubleshooting passthrough that times the upstreamgetPagecall and logs the parsed response body. -
Add
editorDefinitionsupport to the@AttributeDefinitiondecorator for Page Designer components. Custom attribute editors (e.g.einstein.globalrecommenderselector) can now be declared on a decorated attribute and are emitted aseditor_definitionin the generated cartridge JSON. Per the SFCC schema,editor_definitionis only valid on attributes of typecustom. -
Wire SDK security-headers middleware into the React Router middleware chain. Apply per-request CSP nonce to the inline
__APP_CONFIG__script. New customer-facing doc:docs/README-SECURITY-HEADERS.md. -
Add
id_tokenandidp_refresh_tokencookies to align sf-next with PWA Kit's SLAS cookie set, follow-up to the cookie-name alignment work in.id_token(OIDC ID token) is written with the same expiry as the access token;idp_refresh_token(IDP refresh token, used during social login) is written with the same expiry as the SLAS refresh token. Both follow sf-next's existing convention: HttpOnly, Secure, siteId-namespaced (id_token_<siteId>,idp_refresh_token_<siteId>), and cleared on the destroy path alongside the rest of the auth cookies. Neither field is added toPublicSessionData, so the tokens stay server-only. -
Adopt a two-track versioning model: the SDK packages stay on SemVer (locked together), while the template moves to a CalVer date stamp (e.g. "June 2026"). Generated projects now record their origin in
package.jsonunder a namespacedstorefrontNextblock (templateRelease,templateVersion,minSdkVersion), so a customer can always tell which template release they generated from.create-storefrontsurfaces the template release in its completion banner and links the new compatibility matrix (docs/COMPATIBILITY.md), which maps each template stamp to the SDK version it needs.
Patch Changes
-
Fix change email flow for passwordless shoppers. Passwordless shoppers with verified email status must verify current email before being able to edit their email
-
Remove email from the personal information card on the account page
-
Fix change email flow for passwordless shoppers failing on the second attempt. After a successful email change, the OTP modal now opens correctly to reauthenticate with the new email address
-
Fix product tile swatches not rendering when SCAPI search hits omit
variationAttributes. Some data sets surface variation data only onvariants[].variationValues, with no top-levelvariationAttributeseven whenexpand=variationsis requested.getDecoratedVariationAttributespreviously returned[]in that case, so the swatch row disappeared entirely. The function now synthesizes aVariationAttribute[]fromvariants[].variationValueswhen the hit-level array is missing, then runs the existing decorator + master-imageGroupslookup so swatch images still resolve. -
Fix duplicate toast when saving customer preferences
-
Lazy-load below-the-fold UITarget extension components to fix TBT regression
-
Fix
/resource/recommendationsreturning HTTP 403 on MRT by enabling Expresstrust proxyand usingresolveRequestOriginin the same-origin gate -
Add success toast for reset password for passwordless users
-
Defer the cart's below-the-fold product recommendations until the browser is idle. The two recommendation carousels in
CartContentnow mount through auseDeferredRender-gated wrapper, which keeps the lazyProductRecommendationschunk request, the Einstein fetch, and the<Suspense>reconciliation off the critical render path on cart entry. This reduces TBT without changing what eventually renders. -
Server-side recommendations (read path only): cart and account overview now load Einstein recommendations in their loaders, PD-placed ProductRecommendations load via component-loader, useRecommenders is rewired to a new /resource/recommendations BFF, and Einstein recs reads move from the engagement adapter to a plain server-side function. The engagement adapter's analytics surface (sendEvent) is unchanged.
-
Fix: store
dwsourcecode_*cookie value as the bare source-code string (e.g.email) instead of a JSON-encoded object so SFRA storefronts running side-by-side on the same cookie name can read it. The shopper-context cookie (storefront-next-context_*) is unchanged. An empty?src=parameter now writes an empty cookie (effectively clearing it) instead of{"sourceCode":""}. -
Prevent redundant client-side basket fetch on PDP by making
useBasketread-only by default; consumers that need a client-sideGET /baskets/{id}opt in via{ autoLoad: true }(e.g. PDP edit-mode inuseProductActions). BOPIS delivery-option validation now runs both client-side (toast) and server-side (cart-item / cart-bundle / cart-set add actions). -
Fix Promise identity in
<Region>page mode. The component previously wrapped the page prop withPromise.resolve(props.page)on every render, minting a fresh Promise that<Await>tracks by identity — re-suspending in a tight loop. The component now branches on whether the page prop is a Promise: Promise inputs are passed through unchanged so the caller's stable reference flows into<Await>, while plain (already-resolved) inputs render synchronously without Suspense. -
Document the Promise identity rule for Suspense in
docs/README-SUSPENSE.mdand surface it inAGENTS.md. Add aperformance-review-promise-stabilitysub-skill under.claude/skills/performance-review/that detects unstable promise references fed to<Await resolve>/use. -
Short-circuit passwordless authorize at checkout when the email-verification site pref is disabled. Adds
features.passwordlessLogin.skipWhenEmailVerificationDisabled(defaulttrue) for opt-out, and fixes the standard login modal so its form submits to the site/locale-prefixed/loginroute and closes on successful login. -
Bump
handlebarsfrom 4.7.8 to 4.7.9 instorefront-next-devand add pnpm overrides for@isaacs/brace-expansion@^5.0.1,axios@^1.15.2,basic-ftp@^5.3.1,flatted@^3.4.2, andshell-quote@^1.8.4. Overrides live in bothpnpm-workspace.yaml(protects this monorepo's dev/CI) andtemplate-retail-rsc-app'spnpm.overrides(so customer-generated projects pick them up since pnpm only honors overrides at the workspace root). Addresses 10 of 11 critical CVEs from a pre-GA Snyk scan; none are exploitable in the shipped runtime — every vulnerable package is reachable only via dev/build tooling — but the duplication ensures customer projects get clean Snyk output on day one. -
Make the SLAS access-token JWT the single source of truth for
userType, alongsidecustomerId,usid, andaccessTokenExpiry. All four fields are now derived from the same JWT decode whenever a token response is written into the session, so they always travel together — there is no path where one drifts from the other.Fixes an in-request session-state bug where, immediately after a guest signed in (standard email/password, passwordless OTP, or social IDP), `session.customerId` kept the guest `gcid` for the rest of the request even though the registered access token carrying the correct `rcid` had already been persisted. Any in-request consumer that read `session.customerId` (basket merge, wishlist merge, anything pasting `customerId` into a SCAPI URL path) targeted the wrong customer. The bug self-healed on the next request because the auth middleware re-derived state from cookies before any handler ran. Internally: - `updateAuthStorageDataByTokenResponse` now derives `userType` from the JWT's `isb` claim (registered iff `rcid` is present and non-empty) and uses that value to cap the refresh-token expiry. - The four login sites (`standard-login.server.ts`, `_empty.login.tsx` passwordless auto-verify, `action.verify-passwordless-otp.ts`, `social-login.server.ts`) collapse from two `updateAuth(...)` calls to one. - The auth middleware's ingest path uses the JWT-derived `userType` whenever a decodable access token is present; cookie names (`cc-nx` / `cc-nx-g`) are now consulted only as a cold-start fallback when no access token has been issued yet, and as the response-side decision for which refresh-cookie to write or delete. - `getCustomerIdFromClaims` no longer takes a `userType` argument — it returns `rcid ?? gcid`, which is the right answer for either user type without disambiguation. -
Restore guest wishlist support on the heart button. A guest clicking the heart now silently adds the item to their guest wishlist via the existing
WishlistProviderand SCAPI's product-list endpoints (which accept guest gcid tokens). On sign-in, the existingmergeWishlistflow transfers the items to the registered account.The `WishlistButton` previously wrapped its toggle action with `useRequireAuth`, which redirected guests to `/login` before the provider's `add` ever ran. The wrapper predated the rest of the guest-aware wishlist architecture (server-side hydration via `fetchWishlistInitialState`, action routes that explicitly support gcid tokens, the `/wishlist` guest route with its sign-in banner). Removing it lets the rest of the stack do the right thing for guests with no other changes. The component also drops the now-dead pending-action plumbing (`useCheckAndExecutePendingAction`, the URL-scrubbing `useEffect`, and the `pendingActionRef`/`wasPendingRef` refs) — those existed solely to support the post-login redirect-and-resume flow that no longer applies. -
Add
data-testid="address-card"to the address-card component so E2E tests can target it unambiguously. Previously the e2e harness used[data-slot="card"]:has([data-slot="card-footer"]), which also matched the tracking-consent banner and produced sporadic test failures. -
Re-bucket and normalize cart-domain Storybook titles per Pattern 7. Moves
Components/BonusProductModal,COMMON/My Cart,COMMON/Promo Popover, andSKELETON/CartSkeletoninto theCART/bucket; converts PascalCase titles (CartContent,CartEmpty,CartTitle,CartSummarySection,CartInventoryErrorBanner,PromoCodeForm,PromoCodeFields) to sentence case to match audited components likeCart Item Edit ButtonandMini Cart Item. Storybook left-rail navigation now groups all cart-domain stories together. -
Bump
@salesforce/b2c-tooling-sdkto^1.11.0and@salesforce/b2c-clito^1.12.0(latest published versions). Scope the oclifinithook to sfnext commands only so it no-ops cleanly when the package is loaded as a plugin under a host CLI (e.g.b2c sfnext); standalonesfnextbehavior is unchanged. -
Stop the data-store middleware from crashing MRT requests on transient DAL failures.
DataStoreServiceErrorandDataStoreUnavailableErrornow both honoronUnavailable, and the four built-in middlewares (customSitePreferencesMiddleware,customGlobalPreferencesMiddleware,gcpPreferencesMiddleware,loginPreferencesMiddleware) default to'fallback'so the request continues with the configured fallback value instead of throwing.SFNEXT_DATA_STORE_UNAVAILABLE_MODEis preserved as an opt-in escape hatch — set it to'throw'to restore fail-fast behavior.DataStoreNotFoundErrorkeeps its existing missing-state semantics; errors thrown fromtransformstill propagate.createDataStoreMiddleware's factory default for customer-authored middlewares stays at'throw'(no change to the public API contract).Adds `dataStoreLoggerContext`, `getDataStoreLogger`, and the `DataStoreLogger` interface under `@salesforce/storefront-next-runtime/data-store`. Hosts can inject a request-scoped structured logger so SDK warnings emit with `correlationId`, `method`, and `path` bindings instead of bare `console.warn`. The storefront template's `loggingMiddleware` wires this automatically; when unset, falls back to a console-based logger that fail-soft serializes metadata (cyclic / unserializable values won't crash the request). -
refactor(extensions): remove ProductContentAdapter infrastructure
Remove the client-side ProductContentAdapter layer (provider, store, mock, hooks, types) now that all data previously flowing through it has been migrated to SSR extensions (SFDC_EXT_RATINGS_REVIEWS, SFDC_EXT_SHIPPING_DELIVERY, SFDC_EXT_PRODUCT_CONTENT, SFDC_EXT_BNPL). Migrate shared types to their owning extensions, strip ProductContentProvider wrappers from routes, and rewrite the adapter pattern guide to cover only the remaining engagement adapter (Einstein, Active Data). -
ext-ratings-reviews: migrate Ratings & Reviews to SSR extension. Move reviews data fetching from the client adapter into the PDP and account-order-detail route loaders as deferred Promises, render the customer reviews section, write-review button, and per-line "Rate & Review" affordance via new
<UITarget>s, add a serveraction.add-reviewroute with Zod validation, and relocate the components, provider, fixtures, and locale strings intosrc/extensions/ratings-reviews/. -
feat(extensions): add Shipping & Delivery SSR extension
Add SFDC_EXT_SHIPPING_DELIVERY extension with estimated delivery card and modal on PDP, zip-code-based shipping calculator integrated into the BOPIS delivery options, and a fulfillment & shipping info modal. Data is fetched server-side via route loader as a deferred Promise and rendered through UITargets. Includes extension markers for clean install/uninstall, locale strings, Storybook stories, and unit tests. -
Fix account details email section buttons overflowing on mobile by adding flex-wrap so Verify Email and Change Email buttons wrap below the email address on narrow viewports
-
Fix auth callback URLs (social login, passwordless, password reset, SLAS
redirect_uri) and SEO canonical/hreflang URLs on hybrid storefronts behind eCDN. These were built fromEXTERNAL_DOMAIN_NAME(which resolves to the MRT-assigned host on hybrid deployments) instead of the customer's custom domain — social login redirected to the MRT host and 404'd, password-mode signup hitredirect_uri doesn't match the registered redirects, magic-link emails carried the wrong origin, and<link rel="canonical">/ hreflang URLs pointed at the lambda-internal hostname.A new `requestOriginMiddleware` stashes the in-flight request in router context; `getAppOrigin(context)` lazily parses `x-forwarded-host` / `x-forwarded-proto` (with comma-split, leading-empty handling, and exact-match localhost detection) and memoizes per request. The SDK's host-header middleware already populates `x-forwarded-host` from `EXTERNAL_DOMAIN_NAME` when no upstream value is present, so a single deployment serves any custom domain without per-environment config. `createApiClients`, all auth-flow callbacks, the magic-link emailers, and `buildSeoMetaDescriptors` now resolve origin via context. `schema-url.ts` `getPublicOrigin` delegates to the same parser so JSON-LD URLs share the auth path's correctness fixes. -
Fix BNPL target render loop on PDP. The previous implementation combined two deferred loader Promises with
Promise.all([...])inside render, producing a fresh Promise on every render. React Router's<Await>tracks resolution by Promise identity, so each new Promise re-suspended and re-rendered in a microtask-fast loop — starvinguseFetcher({ key: 'basket-products' })of stable commit cycles (mini-cart never opened) and driving express-payments image churn. Each deferred Promise now gets its own<Suspense>/<Await>boundary so the loader's original Promise references flow through unchanged. -
Allow the OOTB Einstein engagement adapter through the default CSP. The adapter fires browser
navigator.sendBeaconcalls to the CQuotient activities API (https://api.cquotient.com/v3/activities/...), which is governed byconnect-src. The defaultconnect-srcdid not list that host, so every Einstein beacon (viewPage, viewProduct, viewCategory, …) was blocked by CSP and reported a console violation on a freshly generated project. Addedhttps://api.cquotient.comto the defaultconnect-srcdirective. -
Fix three dev-server console errors in freshly generated projects (
pnpm dev):
- Invalid hook call / "Cannot read properties of null (reading 'useContext')": on first dev load Vite discovered the React-importing runtime SDK entry points (/config,/security/react,/site-context,/design/react/core,/routing/app-wrapper,/i18n/client) lazily at request time, triggering a dep re-optimization + full reload that transiently loaded a second React instance. Also pre-bundles the i18n peer deps the SDK imports internally but the template does not import from its own source (i18next-browser-languagedetector, used by/i18n/client; andremix-i18next/middleware, used by the SDK's/i18nbarrel) — these are discovered late for the same reason. (react-i18nextneeds no entry: the template imports it directly, so Vite's initial source crawl already finds it.) All are now in the first-passoptimizeDeps.include, so a single optimization runs with one shared React and no mid-session reload.
- CSP blocked the Vite HMR websocket (ws://localhost:24678): the security middleware now appendsws://localhost:*,ws://127.0.0.1:*, andwss://localhost:*toconnect-srconly when running locally (BUNDLE_IDunset orlocal). Deployed/MRT responses are unchanged.
- Nonce hydration mismatch on the inlinewindow.__APP_CONFIG__script: browsers strip thenoncecontent attribute from the DOM after applying CSP, so the client sawnonce=""against the server's real value. AddedsuppressHydrationWarningto that script element; the CSP nonce still applies. -
Fall back to category-level page assignment during manifest resolution when a product-keyed page lookup misses.
resolveDynamicPageIdandresolvePagenow accept an optionalcategoryId(string or Promise) consulted only after the product lookup misses, and the Page Designer resolution middleware threads the request's categoryId through whenever a productId is also present. -
Make
en-GBthe authoritative locale for type-safetkeys. Previouslyen-GBwas the runtime fallback (config.server.ts) and the source the type augmentation read (i18next.server.ts), but every locale'sindex.ts— includingen-GBitself — usedsatisfies DeepPartial<typeof enUS>, makingen-USthe compile-time root. The two only happened to agree becauseen-GBcurrently holds every key. This flips thesatisfieschain toDeepPartial<typeof enGB>so the compile-time and runtime sources of truth match. No runtime behavior change. -
Merge
product.jsonback intotranslations.jsonfor all 17 locales. The split was originally introduced to dodge a TypeScript compiler crash; with the locale tree settled, the merge now typechecks cleanly. Keeps a single canonical translations file per locale. The runtime backend still registersproductas a top-level i18next namespace, so allt('product:…')anduseTranslation('product')call sites are unaffected. -
Cut
pnpm lintruntime by disabling@typescript-eslint/no-misused-promiseschecksVoidReturn.attributes. The sub-check scans every JSX event-handler attribute (onClick,onChange, …) against the value's return type, which fans out across thousands of TSX files and pushedpnpm lintpast 30 minutes on customer 1x CI runners. The rule still fires on argument, property, return, and variable positions, so it continues to catch real misuse — only the JSX-attribute traversal is dropped. -
Add a11y e2e tests for Order and Account page
-
Add comment on how enhanced flatRoutes from runtime works
-
Add
list:extension-pointsscript to enumerate all UITarget IDs and server action hook IDs in the template -
All SCAPI types and clients are now imported from a template-local
@/scapibarrel instead of@salesforce/storefront-next-runtime/scapi. The barrel transparently re-exports the runtime SDK by default and substitutes locally-generated namespaces for any client overridden viasfnext scapi add.src/lib/api-clients.server.tsis updated to substitute override entries in place (and rebuildclients.auth/clients.baskethelpers when their underlying clients are overridden) so call sites see the override schema everywhere. An ESLintno-restricted-importsrule (warn) flags any direct runtime-path imports outside the@/scapibarrel andapi-clients.server.ts, letting pilots migrate file-by-file -
Build a per-request
AttributeResolutionContext(createAttributeResolutionContext) that injectshost,siteId,locale, optionalpageLibraryDomain, and aresolveMediaUrlimplementation (buildStorefrontMediaUrl) which reproduces the canonical/on/demandware.static/-/{libraryDomain}/{locale}/{fingerprint?}/{path}URL shape — including thelibraryDomain === '-'site-rooted swap. The context is passed throughprocessPageso the runtime resolver can absolutize image, file, and markup URLs without leaving the MRT. -
Cart badge supports client-only rendering
-
Clean up template test files to use mock config rather than hard coded site, locale, and currency.
-
Consolidate mini-cart data fetching:
useBasketWithProductsanduseBasketWithPromotionsare merged intouseMiniCartData;/resource/basket-products-promotionsis removed and/resource/basket-productsnow returns{ basket, productsById } -
Continue cleanup of hardcoded locales and currencies in template tests and stories: replace remaining
'en-GB'/'en-US'/'GBP'/'USD'literals with values frommockSiteObject/mockAltSiteObject, fix cross-helper inconsistencies in mock site objects, and standardize onmockSiteObjectovermockConfig.commerce.sites[0]. -
Dedupe identical GET/HEAD requests at the SCAPI fetch layer; mutations (POST/PUT/PATCH/DELETE) invalidate the request-scoped cache on settle
-
Eliminate untyped
anyand remove stale@ts-expect-errorsuppressions in non-test source. Wishlist actions now read SCAPI fields (id,customerProductListItems) directly; the customer-address form usesz.input/z.outputto handle zod's.default(false)transform; engagement adapter config gains specificeinstein/dataCloud/activeDatashapes; decorator helpers acceptunknowninstead ofany; i18next dynamic translation keys are typed viaParseKeys<'countries'>. -
Fix
c_headerMenuOrientationcasing in the navigation-menu-mega story (was'horizontal', should be'Horizontal'to match the SCAPI custom attribute enum) -
Fix
<DynamicImage>SSR preload collapsing distinct breakpoints into one cache slot -
Fix multi-ship shipping method UI: correct step title size, wrap each shipment in a bordered card with a gray header band showing
Shipment N → Recipient, separated address line, divider between address and shipping options, unbold method name second line, and full-width continue button -
Fix passwordless email action to treat SLAS 404 (unknown user) as a normal guest path instead of a server failure. The standardized-error-handling PR wrapped any non-400 SLAS error into HTTP 500, which surfaced as HTTP 502 at the MRT edge for guests entering an unregistered email at checkout. The action now returns HTTP 200 with
{ success: false, email }for 404s, matching the pre-PR behavior and letting the contact step proceed without opening the OTP modal. -
Fix passwordless registration basket invalidation: skip
mergeBasket(transferBasket) when verifying OTP for a new registration, since SLAS creates the account under the same guest usid and the basket is already implicitly owned by the new registered customer. AddisRegistrationform field toOtpModalandaction.verify-passwordless-otpto distinguish registration from login flows -
Fix
pnpm installfailing after dependency changes (e.g.pnpm add,pnpm update, or any edit that invalidates the lockfile): add apnpm.overridesentry that pinshappy-dom's transitive@types/nodeto^24.0.0, which dropsundici-types@6.21.0from the dependency graph. Thatundici-typesversion was published without npm provenance and triggersERR_PNPM_TRUST_DOWNGRADEwhen pnpm re-resolves dependencies from the registry -
Fix Sen font re-downloading on every soft navigation. Move the
@font-facedeclaration from an inline<style>block insrc/root.tsxtosrc/theme/base.cssso it lives on a persistent<link rel=stylesheet>and is registered once per full page load instead of being churned on every route change -
Fix split-ship address selection UI to match design: large heading with header separator, single "Add new address" outline button in the card header (alongside "Ship items to one address" link) instead of per-item links, full-width delivery address dropdowns, and full-width continue button
-
Fix stale cart badge count when the SCAPI-side basket can no longer be loaded. When
getBasketexhausts both the read and create-fallback paths (5xx, auth, network), the basket middleware now expires the__sfdc_basketsnapshot cookie viaSet-Cookie: Expires=…1970…so the next request renders an accurate (zero) badge instead of the stale count from the dead snapshot. -
Fix tax line in Order summary
-
Fix unclickable X button on applied coupon badges in the cart. The
<CloseIcon>was a direct SVG child of<Badge>whose CVA includes[&>svg]:pointer-events-none, so the click never reached the handler. Extracted the X into a sibling<Button variant="secondary" size="icon-sm">styled to read as one connected pill with the badge, and disabled it while the matchingremoveFetchersubmission is in flight to prevent double-submits -
Fix wishlist heart hanging on PLP/Search after click: pin the non-critical promise inside
DeferredProductGridso loader revalidations triggered by tile-level mutations (e.g.useFetcher.submit) no longer re-suspend<Await>and unmount in-flight fetchers -
GA-clean cleanup: remove internal work-item IDs, unresolved development TODOs, and "(internal use)" labels from the template package
-
Hoist
Suspense/AwaitfromProductGridto route level via newDeferredProductGridwrapper for consistent deferred data pattern -
Polish
package.jsonscripts for GA: reduced from 50+ entries to 30, grouped under namespace prefixes (storybook:,cartridge:,config:,dev:). Consolidated the 10test-storybook:*variants into a singlepnpm storybook:test --type=snapshot|interaction|a11y [--update] [--coverage] [--static]orchestrator (the only retained wrapper script — it manages a real server lifecycle). Removed redundant variants in favor of direct CLI invocations and forwarding extra flags to the underlying tools (e.g.pnpm test --coverageforwards to vitest). Renames:generate:cartridge→cartridge:generate,validate:cartridge→cartridge:validate,deploy:cartridge→cartridge:deploy,generate:eslint→eslint:generate-config,list:extension-points→extensions:list,build-storybook→storybook:build. Removed (use direct CLI or e2e subpackage scripts; documented inAGENTS.md):bundlesize:test/:analyze,test:coverage/:agent/:agent:coverage/:ui,lint:check-extensions/:colors, all 10test-storybook:*variants,e2e:headless/:verbose/:debug/:report,a11y:headless/:report/:update-baseline,deploy:cartridge:clean,generate:story-tests/:coverage. -
Preload off-screen product gallery slides so swipes resolve from cache instead of fresh DIS requests
-
Refactor basket actions to reduce code duplication
-
Refactor routes to use React Router v7 generated route type imports
-
Remove dark mode tokens entirely (Phase 1 follow-up to the CSS color token audit).
-
Remove
resolve.dedupeandoptimizeDeps.includeReact/React Router defaults fromvite.config.ts; these are now provided by the SDK'sbaseConfigPlugin -
Remove the redundant
customerIdauth cookie.customerIdis now derived per-request from the SLAS access token JWTisbclaim (viagcid/rcid) and continues to flow throughgetAuth(context)server-side anduseAuthclient-side. The middleware no longer reads, writes, or actively clears the cookie — any value left in existing browsers is ignored and expires on its own. Theusidcookie is kept — sf-next readsusidfrom the JWTsubclaim, but the cookie is retained so hybrid storefronts can forward it to ECOM (which does not parse the access token forusid); the cookie value is also used as a cold-start fallback to preserve SLAS session continuity when no access token is present. -
Rename
docs/README-CUSTOM-APIS.md→docs/README-SCAPI.mdand restructure to cover both override and custom-API workflows, with a pilot-upgrade migration section -
Rename
sfnext create-storefrontconfig prompt labels for clarity:API Client ID→SLAS Client ID,API Organization ID→Organization ID,API Short Code→Short Code,Default Site ID→Site ID(matches Commerce Cloud documentation nomenclature). Labels are sourced from the template'sconfig-meta.json. -
Reorganize
src/lib/into domain-first folders: each commerce concept (address/,auth/,cart/,checkout/,customer/,images/,marketing/,order/,payment/,product/,shopper-context/,turnstile/) is now a self-contained folder, and adapters are consolidated underlib/adapters/<type>/. Cross-cutting utilities remain at thelib/root. -
Replace
deepMergeimport from@salesforce/storefront-next-runtime/configwith a localtest-utils/deep-mergeutility — the SDK no longer exportsdeepMergeas public API -
Replace hand-rolled
Mock<Component>Storybook stories with the real components forTrackingConsentBanner,CurrencySwitcher, andLocaleSwitcher. Each story now imports the production component and relies on the existing global preview decorator (memory router + provider chain) instead of a hand-rolled JSX duplicate. Splits/action/set-site-contextso currency submissions resolve immediately and locale submissions hang (preventing the unforgeablewindow.location.hrefredirect from crashing the test runner). -
Replace the array-based SCAPI middleware context with a keyed
ScapiMiddlewareRegistry. Consumers now callgetScapiMiddlewareRegistry(context).register('my-key', entry). Re-registering the same key replaces the entry in place without changing its execution order, making registration idempotent across re-entrant router middleware. The registry is lazily allocated per request, so producers can no longer accidentally accumulate entries on a module-level default. -
Replace
useMatcheswith typeduseRouteLoaderDatain root Layout -
Retype route actions and loaders for end-to-end type inference: consumers can now use
useFetcher<typeof action>/useLoaderData<typeof loader>and TypeScript derives the response shape. Standardized every action ondata(payload, init?)returns. See README-DATA.md for the pattern. -
Rewrite extension descriptions in
src/extensions/config.jsonto use "Enables" instead of "allows" (Salesforce style guide reserves "allow" for permissions). Also fix the Buy Online Pickup In Store description, which incorrectly claimed Multiship was a required dependency — only Store Locator is required. -
Scope the cart's
shopperProducts.getProductsrequest to an explicitexpandlist (availability,bundled_products,images,prices,promotions,variations) covering only the fields the cart UI consumes. Dropsset_products,recommendations,links,options,custom_properties, andvalidationfrom the response. -
Scope the mini cart's
shopperProducts.getProductsrequest to an explicitexpandlist covering only the fields the mini cart UI consumes. -
Simplify .env.default and decouple E2E baseline overrides
-
Split
vite.config.tsinto per-plugin files undervite-plugins/, and extract checkout chunking into a dedicatedcodeSplittingplugin. Plugin order now putscodeSplittingbeforestorefrontNextso the SDK's i18n plugin can wrap the user-suppliedmanualChunks. -
Standardize SCAPI requests error handling - Cart Page
-
Standardize SCAPI requests error handling - Product Detail Page
-
Standardize SCAPI requests error handling - Product List Page
-
Storybook infra: extract memory-router setup into
.storybook/decorators/, slimpreview.tsx, and standardize per-component story mocks -
Unify sanitizePrefix and stripPathPrefix into one helper
-
Rewrite adapter pattern guide to reflect analytics-only scope; fix eventToggles casing in config example (snake_case keys, not camelCase)
-
Dispatch OTP transparently when SLAS reports an unverified email at the checkout contact step
-
PDP variation swatches now activate optimistically on click, eliminating the perceived lag while the variant loader resolves. The selected swatch styling is derived from the in-flight navigation target so the clicked button reflects as active on the next paint, then reconciles cleanly with the canonical URL once the navigation settles. Quick-view and cart-edit modal flows are unaffected — they pass an explicit selection override and continue to bypass URL-driven selection entirely.
-
Reduce SCAPI
productSearchresponse payload on PLPs by requesting only the image viewTypes the OOTB product tile reads (mediumfor the hero,swatchfor color thumbnails) via theimgTypesquery parameter. Configurable viasearch.products.images— a role-keyed object whose values are derived (Object.values) into the SCAPI filter, so adding a new role widens the filter automatically. Set a role toundefinedto opt out of search filtering for that role. -
Remove unused dependencies: @radix-ui/react-select, zustand, get-port, and stale express 4.x devDependency duplicate
-
Fix three cart-domain Storybook snapshot harnesses that were silently capturing
nullbaselines:cart-quantity-picker,cart-item-edit-button, andcart-item-modal. Replaced full-replacementvi.mock('react-router')withimportOriginalpartial mocks soRouterProviderkeeps working and<Story />actually renders into the snapshot. Re-recorded the 12 affected snapshots. -
Normalize the
payment-schedule-modal-contentcomponent-folder layout per Pattern 13. The component lived as a flatinfo-modal/renderers/payment-schedule-modal-content.tsxnext to itspayment-schedule-modal-content/stories/folder; moved it intopayment-schedule-modal-content/index.tsxso component, stories, and snapshots all live under one folder. Imports unchanged thanks to TypeScript module resolution; only the relative../typeswas retargeted to../../typesfrom the deeper file. -
Replace exact inventory counts on the PDP with bucketed status messages — In stock, Few items left, 1 item left, Out of stock — and handle perpetual-quantity items cleanly so they no longer surface a literal unit count
-
Fix:
getOrCreateWishlistno longer blocks the loader for ~1.5s on every freshcustomerId. The POST response fromcreateCustomerProductListis now used directly when it carries anid, eliminating the hardcodedsetTimeout(1500)+ post-create GET that was previously waiting for index propagation. The sleep + re-fetch remains as a fallback for the rare case where the POST returns a body withoutid, gated by awarnlog so production telemetry can confirm. Affects every wishlist-mounting route (homepage, cart, PDP, PLP, search, account overview) on first load after SLAS issues a newgcid. -
/wishlistno longer redirects shoppers with expired sessions to/login. Guest shoppers (and registered shoppers whose session is no longer usable) now see the guest wishlist with a persistent sign-in banner so they can recover at their own pace.
Full Changelog: v0.4.2...v2026.6.0
storefront-next-dev
Minor Changes
-
Tighten the
@salesforce/storefront-next-runtime/configpublic surface for V1 GA.**Removed from public exports** (followups to): - `ConfigContext` — internal React context backing `useConfig`. Read config via `useConfig` instead. - `createAppConfig` — one-line `.app` accessor with no semantic value. Use `staticConfig.app` directly. - The `middleware.ts` source file and `createAppConfigMiddleware` factory were also deleted; they had B2C-Commerce-specific validation hardcoded in (`commerce.api.{clientId,organizationId,shortCode}`, `SCAPI_PROXY_HOST`) and the retail template ships its own validating middleware. Future templates write their own. **Added** — `AppConfigShape` interface as a module-augmentation hook for `getConfig` / `useConfig` typing. Each template `declare module`s once in its own types file and gets typed access without the per-call `<AppConfig>` generic: ```ts // In your template's src/types/config.ts: declare module '@salesforce/storefront-next-runtime/config' { interface AppConfigShape extends AppConfig {} } ``` **Other changes** - `Locale`, `Site`, `Url` JSDoc reframed as opt-in baseline shapes — `BaseConfig<App>` is generic so future templates can ignore them. - `defineConfig` JSDoc now explicitly notes it reads `process.env` at call time (server-only side effect). - `mergeEnvConfig`'s engagement-specific protected-paths error message is now generic. - Retail-flavored examples in `schema.ts` / `utils.ts` JSDoc neutralized. - Template (`template-retail-rsc-app`): all 110+ `getConfig<AppConfig>` / `useConfig<AppConfig>` call sites now use the augmented hook (no per-call generic). - Template `commerce.sites: Site[]` now imports the richer `Site` shape from `@salesforce/storefront-next-runtime/site-context` (reconciles the duplicate `Site` types that existed across both subpaths). - `contact-info.tsx` migrated from `useContext(ConfigContext)` to `useConfig` (the only production caller of the bypass pattern). - README-CONFIG.md fixed: `loadConfig` subpath was wrong (`/load-config` → `/config/load-config`); `appConfigContext` now documented for custom middleware composition; `AppConfigShape` augmentation pattern documented. Public surface delta: 13 runtime + 5 type exports → 5 + 6 (plus `loadConfig` at the separate `./config/load-config` subpath). > **Semver note for future maintainers:** This entry is a `minor` bump _only because the package is still pre-V1 GA_. Removing public exports is a `major` once V1 ships — do not paste this pattern after lock-in. -
Add action hooks Vite plugin: generates
virtual:action-hooksmodule from extensiontarget-config.jsonregistrations, enabling server-side extension points in checkout actions -
Add
baseConfigPluginthat contributes framework-wide Vite defaults (React/React Router dedupe, pre-bundled React Router entries) so these don't have to live in the template'svite.config.ts -
Add
enabledboolean field totarget-config.jsoncomponent entries: components withenabled: falseare skipped at build time and tree-shaken from the production bundle; entries without the field default totrue(backward compatible) -
Add
@salesforce/storefront-next-runtimeas a peer dependency so the CLI can read the runtime'sBUILT_IN_CLIENT_DEFAULTSmap directly — keeping the per-client config (basePath, locale-awareness) in sync with the SDK by construction -
Add
sfnext scapi addsupport for overriding built-in SCAPI shopper clients (e.g.shopperProducts) in addition to adding net-new custom APIs. Override-vs-custom is detected automatically from the resolved client key; overrides inherit the SDK's per-client locale-awareness and base path so they behave identically to the SDK client they replace. Also addssfnext scapi availableto survey the active tenant and report which built-in shopper APIs havec_*custom attributes worth overriding -
Make
allowedActionOriginsgeneric: readSFW_FUNCTIONAL_DOMAINenv var instead of hardcoding the functional domain, and add Pomerium reverse proxy origin pattern -
Remove internal exports from the main package entry point (
actionHooksPlugin,extractPatterns,testPatterns,clearCache,createServer,loadProjectConfig,loadConfigFromEnv,trimExtensions,generateMetadata) to reduce the public API surface for GA; these are either composed internally bystorefrontNextTargetsor available via dedicated subpath exports -
Add
editorDefinitionsupport to the@AttributeDefinitiondecorator for Page Designer components. Custom attribute editors (e.g.einstein.globalrecommenderselector) can now be declared on a decorated attribute and are emitted aseditor_definitionin the generated cartridge JSON. Per the SFCC schema,editor_definitionis only valid on attributes of typecustom. -
Adopt a two-track versioning model: the SDK packages stay on SemVer (locked together), while the template moves to a CalVer date stamp (e.g. "June 2026"). Generated projects now record their origin in
package.jsonunder a namespacedstorefrontNextblock (templateRelease,templateVersion,minSdkVersion), so a customer can always tell which template release they generated from.create-storefrontsurfaces the template release in its completion banner and links the new compatibility matrix (docs/COMPATIBILITY.md), which maps each template stamp to the SDK version it needs.
Patch Changes
-
Fix
/resource/recommendationsreturning HTTP 403 on MRT by enabling Expresstrust proxyand usingresolveRequestOriginin the same-origin gate -
Bump
handlebarsfrom 4.7.8 to 4.7.9 instorefront-next-devand add pnpm overrides for@isaacs/brace-expansion@^5.0.1,axios@^1.15.2,basic-ftp@^5.3.1,flatted@^3.4.2, andshell-quote@^1.8.4. Overrides live in bothpnpm-workspace.yaml(protects this monorepo's dev/CI) andtemplate-retail-rsc-app'spnpm.overrides(so customer-generated projects pick them up since pnpm only honors overrides at the workspace root). Addresses 10 of 11 critical CVEs from a pre-GA Snyk scan; none are exploitable in the shipped runtime — every vulnerable package is reachable only via dev/build tooling — but the duplication ensures customer projects get clean Snyk output on day one. -
Bump
@salesforce/b2c-tooling-sdkto^1.11.0and@salesforce/b2c-clito^1.12.0(latest published versions). Scope the oclifinithook to sfnext commands only so it no-ops cleanly when the package is loaded as a plugin under a host CLI (e.g.b2c sfnext); standalonesfnextbehavior is unchanged. -
Fix three dev-server console errors in freshly generated projects (
pnpm dev):
- Invalid hook call / "Cannot read properties of null (reading 'useContext')": on first dev load Vite discovered the React-importing runtime SDK entry points (/config,/security/react,/site-context,/design/react/core,/routing/app-wrapper,/i18n/client) lazily at request time, triggering a dep re-optimization + full reload that transiently loaded a second React instance. Also pre-bundles the i18n peer deps the SDK imports internally but the template does not import from its own source (i18next-browser-languagedetector, used by/i18n/client; andremix-i18next/middleware, used by the SDK's/i18nbarrel) — these are discovered late for the same reason. (react-i18nextneeds no entry: the template imports it directly, so Vite's initial source crawl already finds it.) All are now in the first-passoptimizeDeps.include, so a single optimization runs with one shared React and no mid-session reload.
- CSP blocked the Vite HMR websocket (ws://localhost:24678): the security middleware now appendsws://localhost:*,ws://127.0.0.1:*, andwss://localhost:*toconnect-srconly when running locally (BUNDLE_IDunset orlocal). Deployed/MRT responses are unchanged.
- Nonce hydration mismatch on the inlinewindow.__APP_CONFIG__script: browsers strip thenoncecontent attribute from the DOM after applying CSP, so the client sawnonce=""against the server's real value. AddedsuppressHydrationWarningto that script element; the CSP nonce still applies. -
Fix Managed Runtime returning HTTP 502 for any response that ends with no body. Previously, routes that returned
Response(null, { status: <any> })— for example analytics beacon proxies,204 No Contentresponses, and 5xx error responses returned without a body — produced a generic502 InternalServerErrorExceptionfrom API Gateway instead of the application's intended status code. Empty-body 3xx redirects already had a workaround; this extends it to all status codes.If your storefront uses the built-in Active Data analytics adapter, every `__Analytics-Start` beacon was failing in production with this 502, silently dropping analytics events. Recent Active Data dashboards may underreport — review with that in mind after deploying. -
Fix the hybrid-proxy Vite plugin dropping the original request's query params when SFCC redirects to a canonical SFRA URL (e.g.
/cart→/s/{siteId}/{locale}/Cart-Show). The proxy now merges the request's query string into the redirect target, with the redirect target winning on key collisions and multi-value keys (e.g.pmid=PROMO1&pmid=PROMO2) preserved on both sides. Local-development only — MRT/eCDN deployments are unaffected. -
Fix the hybrid proxy plugin so SFRA's
plugin_redirectcartridge actually redirects in local development.plugin_redirectsometimes returns HTTP 200 with aLocationheader instead of a proper 3xx redirect; browsers only followLocationon 3xx responses, so the proxied page rendered blank. TheproxyReshandler now detects this case (status 200 + non-emptyLocation), converts it to a 302 with the localhost-rewrittenLocation, drops the upstream body, and preservesSet-Cookieso session continuity is maintained. Affects local development only — production routing is handled by Cloudflare eCDN and is unaffected. -
Drop stale
SFDC_EXT_THEME_SWITCHERreferences after the theme-switcher extension was removed from the template: replace fixtures inextensibility/dependency-utils.test.tsandextensibility/target-utils.test.tswithSFDC_EXT_MULTISHIP/multiship, and refresh CLI examples inextensions install,extensions remove, and the package README to useSFDC_EXT_BOPIS -
Fix dev-only
TypeError: Cannot read properties of null (reading 'currency')(or similar) in loaders that consume site context, caused by Vite externalizing the SDK for some import sites and transforming it for others — producing twodist/site-context.jsmodule records and twocreateContextkeys.baseConfigPluginnow setsssr.noExternal: ['@salesforce/storefront-next-runtime']so all SSR import sites resolve through Vite's transform cache and share one module record. Production builds were never affected (managedRuntimeBundlePluginalready inlines the SDK). Reverts the relevant part of. -
Fix
managedRuntimeBundleplugin build error when a CSS file references apublic/or bundled asset (e.g.@font-face { src: url(...) }).renderBuiltUrlnow returns{ relative: true }forhostType === 'css'instead of the JS runtime expression Vite rejects inside CSSurl. The browser resolves the relative path against the stylesheet's own URL — which already carries the per-deployBUNDLE_ID— so the reference stays correct across deploys -
Mark
Scriptscomponent (./react-router/Scripts) as private internal API — it is automatically applied via thepatchReactRouterVite plugin and should not be imported directly -
Rename the configuration labels that the
sfnext create-storefrontcommand asks for when run against the bundled retail template, to align with Commerce Cloud documentation:SLAS Client ID(formerlyAPI Client ID),Organization ID(formerlyAPI Organization ID),Short Code(formerlyAPI Short Code),Site ID(formerlyDefault Site ID). Labels are sourced from the template'sconfig-meta.json. -
The
sfnext scapicodegen now emits a template-localsrc/scapi/index.tsbarrel that wildcards the runtime SCAPI surface and shadows overridden namespaces with locally-generated wrappers;sfnext scapi listgroups output into Overrides and Custom APIs sections;sfnext scapi removecleans up the per-override namespace wrapper alongside the schema/types/operations files -
Validate
allowedActionOriginsinreactRouterConfigResolvedto prevent overrides in workspace environments -
Warn at build time when multiple UITarget components or action hook handlers share the same target ID and order value (non-deterministic execution order)
-
When a strict CSP with nonces is in use, the SDK now propagates the request nonce to its internal bundle-config inline script (the SDK transparently substitutes the customer's
<Scripts>import with its own wrapper at build time, so this happens automatically). No customer action required.
storefront-next-runtime
Minor Changes
-
Tighten the
@salesforce/storefront-next-runtime/configpublic surface for V1 GA.**Removed from public exports** (followups to): - `ConfigContext` — internal React context backing `useConfig`. Read config via `useConfig` instead. - `createAppConfig` — one-line `.app` accessor with no semantic value. Use `staticConfig.app` directly. - The `middleware.ts` source file and `createAppConfigMiddleware` factory were also deleted; they had B2C-Commerce-specific validation hardcoded in (`commerce.api.{clientId,organizationId,shortCode}`, `SCAPI_PROXY_HOST`) and the retail template ships its own validating middleware. Future templates write their own. **Added** — `AppConfigShape` interface as a module-augmentation hook for `getConfig` / `useConfig` typing. Each template `declare module`s once in its own types file and gets typed access without the per-call `<AppConfig>` generic: ```ts // In your template's src/types/config.ts: declare module '@salesforce/storefront-next-runtime/config' { interface AppConfigShape extends AppConfig {} } ``` **Other changes** - `Locale`, `Site`, `Url` JSDoc reframed as opt-in baseline shapes — `BaseConfig<App>` is generic so future templates can ignore them. - `defineConfig` JSDoc now explicitly notes it reads `process.env` at call time (server-only side effect). - `mergeEnvConfig`'s engagement-specific protected-paths error message is now generic. - Retail-flavored examples in `schema.ts` / `utils.ts` JSDoc neutralized. - Template (`template-retail-rsc-app`): all 110+ `getConfig<AppConfig>` / `useConfig<AppConfig>` call sites now use the augmented hook (no per-call generic). - Template `commerce.sites: Site[]` now imports the richer `Site` shape from `@salesforce/storefront-next-runtime/site-context` (reconciles the duplicate `Site` types that existed across both subpaths). - `contact-info.tsx` migrated from `useContext(ConfigContext)` to `useConfig` (the only production caller of the bypass pattern). - README-CONFIG.md fixed: `loadConfig` subpath was wrong (`/load-config` → `/config/load-config`); `appConfigContext` now documented for custom middleware composition; `AppConfigShape` augmentation pattern documented. Public surface delta: 13 runtime + 5 type exports → 5 + 6 (plus `loadConfig` at the separate `./config/load-config` subpath). > **Semver note for future maintainers:** This entry is a `minor` bump _only because the package is still pre-V1 GA_. Removing public exports is a `major` once V1 ships — do not paste this pattern after lock-in. -
Remove internal data-store context objects and key constants from public exports (
sitePreferencesContext,customGlobalPreferencesContext,gcpPreferencesContext,loginPreferencesContext,DEFAULT_SITE_PREFERENCES_KEY,DEFAULT_CUSTOM_GLOBAL_PREFERENCES_KEY,DEFAULT_GCP_PREFERENCES_KEY) and remove subpath entry points (./data-store/custom-site-preferences,./data-store/custom-global-preferences,./data-store/gcp-preferences) — these are internal plumbing that allows bypassing middleware invariants. -
Add
BUILT_IN_CLIENT_DEFAULTS(andBUILT_IN_CLIENT_KEYS,isBuiltInClientKey) — a single source of truth for per-built-in-client config (basePath, locale-awareness) consumed by bothcreateCommerceApiClientsand the dev CLI's SCAPI override codegen.createCommerceApiClientsnow drives its baseUrl + globalParams selection from this map so the two never drift -
Add
createLazyDataStoreMiddlewareandreadLazyDataStoreEntryexports under@salesforce/storefront-next-runtime/data-store. The lazy variant stores a memoized loader in router context instead of fetching up front, so routes that never read the entry never pay for the data-store call. Repeated reads within the same request share the in-flight promise. InternalloadDataStoreEntryhelper unifies the eager and lazy fetch + error paths. -
Add MRT-side attribute resolution for Page Designer manifests:
processPagenow walks each component'sdatamap through a type-driven dispatch table that converts manifest envelopes into the same wire shape SCAPI'sgetPagecontroller would have returned. Coversimage(URL resolution viaresolveMediaUrl),file(envelope → URL string),markup(?$staticlink$placeholder rewriting via the newrewriteMarkuphelper), andcms_record(recursive dispatch of inner attributes). Unknown attribute types pass through unchanged with a deduped one-time warning for forward compatibility. Pipeline-action placeholders ($link-...$,$url(...)$,$include(...)$) are intentionally not rewritten — storefront-next uses React composition for navigation. Module is platform-neutral; callers inject anAttributeResolutionContext(host,resolveMediaUrl, optionalpageLibraryDomain). -
Add Shopper Availability v1.1.0 SCAPI client (
clients.shopperAvailability) for retrieving product inventory availability without loading full product details -
Email Verification Feature - Account Details
-
Extend
PageManifestwith optionalcomponentTypes(per-typeIdattribute definitions hoisted by the manifest builder) andpageLibraryDomain(used by the markup URL rewriter). Both are optional so older manifests still load.componentInfoentries gain optionalname,fragment, andcustomfields, and previously-requiredvisibilityRules/regionsare now optional (omitted when empty).RegionInfofields (name,componentTypeExclusions,componentTypeInclusions,maxComponents) shift fromT | nullto optional-and-omitted to match the on-the-wire compact shape. -
Update shopper-customers OAS from v1.3.2 to v1.8.0 and replace shopper-login OAS v1.42.2 with auth OAS v1.47.0
-
Update Shopper Login/Auth OAS to 1.48.0 and Shopper Customers OAS to 1.8.0
-
Add default security response headers middleware (
createSecurityHeadersMiddlewareexported from@salesforce/storefront-next-runtime/security). Ships strict defaults for CSP (with per-request nonce), Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. Customers extend defaults viaconfig.server.ts;defaultCspDirectivesis exported for spread.
Patch Changes
-
Demote expected DataStoreNotFoundError logs from warn to debug to eliminate dev server log noise
-
Security middleware: relax
frame-ancestorsonly on Page Designer / Business Manager preview requests (?mode=EDIT|PREVIEW). On those requests the SDK extendsframe-ancestorsto allow Salesforce-owned admin host families (*.unified.demandware.net,*.commercecloud.salesforce.com,*.demandware.net) and suppressesX-Frame-Options(which has no host-list form). Normal shopper traffic continues to shipframe-ancestors 'self'andX-Frame-Options: SAMEORIGIN. Customers who already extendframe-ancestorsthemselves are unaffected — their override is respected verbatim across all requests. -
Fall back to SCAPI page resolution when the page manifest declares data bindings. The manifest-driven resolver cannot resolve bindings yet (planned for 26.7), so it now returns
nulland lets the caller fetch the page from SCAPI instead. Tracked under W-22770039. -
Stop the data-store middleware from crashing MRT requests on transient DAL failures.
DataStoreServiceErrorandDataStoreUnavailableErrornow both honoronUnavailable, and the four built-in middlewares (customSitePreferencesMiddleware,customGlobalPreferencesMiddleware,gcpPreferencesMiddleware,loginPreferencesMiddleware) default to'fallback'so the request continues with the configured fallback value instead of throwing.SFNEXT_DATA_STORE_UNAVAILABLE_MODEis preserved as an opt-in escape hatch — set it to'throw'to restore fail-fast behavior.DataStoreNotFoundErrorkeeps its existing missing-state semantics; errors thrown fromtransformstill propagate.createDataStoreMiddleware's factory default for customer-authored middlewares stays at'throw'(no change to the public API contract).Adds `dataStoreLoggerContext`, `getDataStoreLogger`, and the `DataStoreLogger` interface under `@salesforce/storefront-next-runtime/data-store`. Hosts can inject a request-scoped structured logger so SDK warnings emit with `correlationId`, `method`, and `path` bindings instead of bare `console.warn`. The storefront template's `loggingMiddleware` wires this automatically; when unset, falls back to a console-based logger that fail-soft serializes metadata (cyclic / unserializable values won't crash the request). -
Allow the OOTB Einstein engagement adapter through the default CSP. The adapter fires browser
navigator.sendBeaconcalls to the CQuotient activities API (https://api.cquotient.com/v3/activities/...), which is governed byconnect-src. The defaultconnect-srcdid not list that host, so every Einstein beacon (viewPage, viewProduct, viewCategory, …) was blocked by CSP and reported a console violation on a freshly generated project. Addedhttps://api.cquotient.comto the defaultconnect-srcdirective. -
Fix three dev-server console errors in freshly generated projects (
pnpm dev):
- Invalid hook call / "Cannot read properties of null (reading 'useContext')": on first dev load Vite discovered the React-importing runtime SDK entry points (/config,/security/react,/site-context,/design/react/core,/routing/app-wrapper,/i18n/client) lazily at request time, triggering a dep re-optimization + full reload that transiently loaded a second React instance. Also pre-bundles the i18n peer deps the SDK imports internally but the template does not import from its own source (i18next-browser-languagedetector, used by/i18n/client; andremix-i18next/middleware, used by the SDK's/i18nbarrel) — these are discovered late for the same reason. (react-i18nextneeds no entry: the template imports it directly, so Vite's initial source crawl already finds it.) All are now in the first-passoptimizeDeps.include, so a single optimization runs with one shared React and no mid-session reload.
- CSP blocked the Vite HMR websocket (ws://localhost:24678): the security middleware now appendsws://localhost:*,ws://127.0.0.1:*, andwss://localhost:*toconnect-srconly when running locally (BUNDLE_IDunset orlocal). Deployed/MRT responses are unchanged.
- Nonce hydration mismatch on the inlinewindow.__APP_CONFIG__script: browsers strip thenoncecontent attribute from the DOM after applying CSP, so the client sawnonce=""against the server's real value. AddedsuppressHydrationWarningto that script element; the CSP nonce still applies. -
Fall back to category-level page assignment during manifest resolution when a product-keyed page lookup misses.
resolveDynamicPageIdandresolvePagenow accept an optionalcategoryId(string or Promise) consulted only after the product lookup misses, and the Page Designer resolution middleware threads the request's categoryId through whenever a productId is also present. -
Exclude OTP endpoints from auth token invalidation — 401 responses from
/oauth2/otp/now surface as normal API errors instead of triggeringAuthTokenInvalidError -
Make
@react-router/devand@react-router/fs-routesoptional peer dependencies to avoid dependency conflicts for consumers that only use the/designexports -
processPagenow requiresdefaultLocaleandattrCtxin itsPageProcessorContextand accepts an optionalcomponentTypesmap. Componentdatais composed in priority order (active-locale content → default-locale content →attrDef.defaultValue) when type definitions are available, falling back to the legacy spread merge when they aren't. Top-level pagedatais also resolved when present. -
Re-export
createAuthHelpersfrom the SCAPI entry point so the template can rebuildclients.authagainst an overriddenshopperLoginclient -
Remove internal config utilities from public exports (
pathToObject,parseEnvValue,extractValidPaths,mergeEnvConfig,MergeEnvConfigOptions,createAppConfigMiddleware,deepMerge) — these are internal plumbing and should not be part of the V1 GA public surface -
Remove
usidfromPasswordlessExchangeTokenOptionsand theexchangeTokenimplementation —usidis not a valid field in the SLASPasswordLessLoginTokenRequestschema (auth-oas-1.48.0) and was silently ignored by the server -
Unify sanitizePrefix and stripPathPrefix into one helper
-
VariationEntry.pagenow carries SCAPI-shape page metadata (name,aspectTypeId,description,pageTitle,pageDescription,pageKeywords) populated by the manifest builder; non-default-locale overrides live on a newpageContentoverlay applied at request time. -
Coerce string values returned by the data binding API into typed values when resolving expressions. The data provider returns every field as a string, so
"true"/"false"are now resolved to booleans and numeric strings (e.g."2026","-12.5") to numbers. Strings that aren't booleans or finite numbers — including empty/whitespace-only strings and HTML — pass through unchanged.