Skip to content

1.0.0 SDK / June 2026 template

Choose a tag to compare

@github-actions github-actions released this 09 Jun 21:09

template-retail-rsc-app

Minor Changes

  • Move <WishlistProvider> from the global _app.tsx shell 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 SCAPI getOrCreateWishlist cost on cold load. <WishlistMergeToast> stays at the app shell.

  • Add useScapiFetchClient / useScapiFetchHelper hooks and <WishlistProvider>: small UI mutations (e.g. PLP heart click) now hit a generic SCAPI resource route via fetch instead of useFetcher, 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 by useSyncExternalStore so a single heart toggle re-renders only that one tile, not every other heart in the grid

  • Tighten the @salesforce/storefront-next-runtime/config public 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 in PUBLIC__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 /parent separately if you need both. Exact paths and :param patterns are unchanged.

  • Add a .devcontainer/devcontainer.json so 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 activates pnpm@10.28.0 via 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 in postAttachCommand so it survives Codespace restarts that wipe /tmp. Workspace ESLint config also disables jsonc/no-comments for devcontainer.json since 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-request and action.otp-verify routes 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 the EINSTEIN_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: customerIdcustomer_id, encUserIdenc_user_id, cc-idp-atidp_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 for encUserId, ~30 min for cc-idp-at) — they are ignored and expire on their own. customer_id remains write-never (derived per request from the SLAS access token JWT isb claim); 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.css from the Figma "Market Street" tokens JSON, split nested palettes (sidebar, agentic) into dedicated files, dedupe status.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 /wishlist page 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 ProductItem price column so list and sale prices stay on one horizontal row with right alignment, hid the ProductPrice promotion callout on cart lines (no long promo copy under prices), and tightened the Saved badge with smaller typography plus an alignEnd wrapper so it sits flush right. The quantity block uses CartQuantityPicker with a w-fit wrapper 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.enabled config 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. fetchPage falls back to SCAPI's getPages endpoint when no pageId is provided, looking up the page assigned to the given product or category via aspectTypeId. Product and category routes now pass aspectType: 'pdp' / 'plp', and the Page Designer resolution middleware intercepts /pages requests in addition to /pages/{pageId}, wrapping the resolved page in a PageResult-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.mrtBasedPageDesignerResolution is on, the pageDesignerResolutionMiddleware SCAPI middleware now resolves getPage requests against the page and site manifests stored in the MRT data store instead of round-tripping to ECOM. Synthesized responses are tagged with an x-page-manifest-hit response header so observability tooling can tell at a glance which path served the response. Design/preview requests (those carrying mode or pdToken query params) still fall through to SCAPI. A new SFCC_PD_PAGE_RESOLUTION_DEBUG=true env gate enables a parity-troubleshooting passthrough that times the upstream getPage call and logs the parsed response body.

  • Add editorDefinition support to the @AttributeDefinition decorator for Page Designer components. Custom attribute editors (e.g. einstein.globalrecommenderselector) can now be declared on a decorated attribute and are emitted as editor_definition in the generated cartridge JSON. Per the SFCC schema, editor_definition is only valid on attributes of type custom.

  • 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_token and idp_refresh_token cookies 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 to PublicSessionData, 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.json under a namespaced storefrontNext block (templateRelease, templateVersion, minSdkVersion), so a customer can always tell which template release they generated from. create-storefront surfaces 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 on variants[].variationValues, with no top-level variationAttributes even when expand=variations is requested. getDecoratedVariationAttributes previously returned [] in that case, so the swatch row disappeared entirely. The function now synthesizes a VariationAttribute[] from variants[].variationValues when the hit-level array is missing, then runs the existing decorator + master-imageGroups lookup 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/recommendations returning HTTP 403 on MRT by enabling Express trust proxy and using resolveRequestOrigin in 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 CartContent now mount through a useDeferredRender-gated wrapper, which keeps the lazy ProductRecommendations chunk 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 useBasket read-only by default; consumers that need a client-side GET /baskets/{id} opt in via { autoLoad: true } (e.g. PDP edit-mode in useProductActions). 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 with Promise.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.md and surface it in AGENTS.md. Add a performance-review-promise-stability sub-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 (default true) for opt-out, and fixes the standard login modal so its form submits to the site/locale-prefixed /login route and closes on successful login.

  • Bump handlebars from 4.7.8 to 4.7.9 in storefront-next-dev and add pnpm overrides for @isaacs/brace-expansion@^5.0.1, axios@^1.15.2, basic-ftp@^5.3.1, flatted@^3.4.2, and shell-quote@^1.8.4. Overrides live in both pnpm-workspace.yaml (protects this monorepo's dev/CI) and template-retail-rsc-app's pnpm.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, alongside customerId, usid, and accessTokenExpiry. 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 WishlistProvider and SCAPI's product-list endpoints (which accept guest gcid tokens). On sign-in, the existing mergeWishlist flow 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, and SKELETON/CartSkeleton into the CART/ bucket; converts PascalCase titles (CartContent, CartEmpty, CartTitle, CartSummarySection, CartInventoryErrorBanner, PromoCodeForm, PromoCodeFields) to sentence case to match audited components like Cart Item Edit Button and Mini Cart Item. Storybook left-rail navigation now groups all cart-domain stories together.

  • Bump @salesforce/b2c-tooling-sdk to ^1.11.0 and @salesforce/b2c-cli to ^1.12.0 (latest published versions). Scope the oclif init hook 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); standalone sfnext behavior is unchanged.

  • Stop the data-store middleware from crashing MRT requests on transient DAL failures. DataStoreServiceError and DataStoreUnavailableError now both honor onUnavailable, 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_MODE is preserved as an opt-in escape hatch — set it to 'throw' to restore fail-fast behavior. DataStoreNotFoundError keeps its existing missing-state semantics; errors thrown from transform still 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 server action.add-review route with Zod validation, and relocate the components, provider, fixtures, and locale strings into src/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 from EXTERNAL_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 hit redirect_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 — starving useFetcher({ 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.sendBeacon calls to the CQuotient activities API (https://api.cquotient.com/v3/activities/...), which is governed by connect-src. The default connect-src did 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. Added https://api.cquotient.com to the default connect-src directive.

  • 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; and remix-i18next/middleware, used by the SDK's /i18n barrel) — these are discovered late for the same reason. (react-i18next needs no entry: the template imports it directly, so Vite's initial source crawl already finds it.) All are now in the first-pass optimizeDeps.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 appends ws://localhost:*, ws://127.0.0.1:*, and wss://localhost:* to connect-src only when running locally (BUNDLE_ID unset or local). Deployed/MRT responses are unchanged.
    - Nonce hydration mismatch on the inline window.__APP_CONFIG__ script: browsers strip the nonce content attribute from the DOM after applying CSP, so the client saw nonce="" against the server's real value. Added suppressHydrationWarning to 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. resolveDynamicPageId and resolvePage now accept an optional categoryId (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-GB the authoritative locale for type-safe t keys. Previously en-GB was the runtime fallback (config.server.ts) and the source the type augmentation read (i18next.server.ts), but every locale's index.ts — including en-GB itself — used satisfies DeepPartial<typeof enUS>, making en-US the compile-time root. The two only happened to agree because en-GB currently holds every key. This flips the satisfies chain to DeepPartial<typeof enGB> so the compile-time and runtime sources of truth match. No runtime behavior change.

  • Merge product.json back into translations.json for 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 registers product as a top-level i18next namespace, so all t('product:…') and useTranslation('product') call sites are unaffected.

  • Cut pnpm lint runtime by disabling @typescript-eslint/no-misused-promises checksVoidReturn.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 pushed pnpm lint past 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-points script 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 @/scapi barrel 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 via sfnext scapi add. src/lib/api-clients.server.ts is updated to substitute override entries in place (and rebuild clients.auth / clients.basket helpers when their underlying clients are overridden) so call sites see the override schema everywhere. An ESLint no-restricted-imports rule (warn) flags any direct runtime-path imports outside the @/scapi barrel and api-clients.server.ts, letting pilots migrate file-by-file

  • Build a per-request AttributeResolutionContext (createAttributeResolutionContext) that injects host, siteId, locale, optional pageLibraryDomain, and a resolveMediaUrl implementation (buildStorefrontMediaUrl) which reproduces the canonical /on/demandware.static/-/{libraryDomain}/{locale}/{fingerprint?}/{path} URL shape — including the libraryDomain === '-' site-rooted swap. The context is passed through processPage so 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: useBasketWithProducts and useBasketWithPromotions are merged into useMiniCartData; /resource/basket-products-promotions is removed and /resource/basket-products now 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 from mockSiteObject/mockAltSiteObject, fix cross-helper inconsistencies in mock site objects, and standardize on mockSiteObject over mockConfig.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 any and remove stale @ts-expect-error suppressions in non-test source. Wishlist actions now read SCAPI fields (id, customerProductListItems) directly; the customer-address form uses z.input/z.output to handle zod's .default(false) transform; engagement adapter config gains specific einstein/dataCloud/activeData shapes; decorator helpers accept unknown instead of any; i18next dynamic translation keys are typed via ParseKeys<'countries'>.

  • Fix c_headerMenuOrientation casing 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. Add isRegistration form field to OtpModal and action.verify-passwordless-otp to distinguish registration from login flows

  • Fix pnpm install failing after dependency changes (e.g. pnpm add, pnpm update, or any edit that invalidates the lockfile): add a pnpm.overrides entry that pins happy-dom's transitive @types/node to ^24.0.0, which drops undici-types@6.21.0 from the dependency graph. That undici-types version was published without npm provenance and triggers ERR_PNPM_TRUST_DOWNGRADE when pnpm re-resolves dependencies from the registry

  • Fix Sen font re-downloading on every soft navigation. Move the @font-face declaration from an inline <style> block in src/root.tsx to src/theme/base.css so 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 getBasket exhausts both the read and create-fallback paths (5xx, auth, network), the basket middleware now expires the __sfdc_basket snapshot cookie via Set-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 matching removeFetcher submission is in flight to prevent double-submits

  • Fix wishlist heart hanging on PLP/Search after click: pin the non-critical promise inside DeferredProductGrid so 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/Await from ProductGrid to route level via new DeferredProductGrid wrapper for consistent deferred data pattern

  • Polish package.json scripts for GA: reduced from 50+ entries to 30, grouped under namespace prefixes (storybook:, cartridge:, config:, dev:). Consolidated the 10 test-storybook:* variants into a single pnpm 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 --coverage forwards to vitest). Renames: generate:cartridgecartridge:generate, validate:cartridgecartridge:validate, deploy:cartridgecartridge:deploy, generate:eslinteslint:generate-config, list:extension-pointsextensions:list, build-storybookstorybook:build. Removed (use direct CLI or e2e subpackage scripts; documented in AGENTS.md): bundlesize:test/:analyze, test:coverage/:agent/:agent:coverage/:ui, lint:check-extensions/:colors, all 10 test-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.dedupe and optimizeDeps.include React/React Router defaults from vite.config.ts; these are now provided by the SDK's baseConfigPlugin

  • Remove the redundant customerId auth cookie. customerId is now derived per-request from the SLAS access token JWT isb claim (via gcid/rcid) and continues to flow through getAuth(context) server-side and useAuth client-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. The usid cookie is kept — sf-next reads usid from the JWT sub claim, but the cookie is retained so hybrid storefronts can forward it to ECOM (which does not parse the access token for usid); 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.mddocs/README-SCAPI.md and restructure to cover both override and custom-API workflows, with a pilot-upgrade migration section

  • Rename sfnext create-storefront config prompt labels for clarity: API Client IDSLAS Client ID, API Organization IDOrganization ID, API Short CodeShort Code, Default Site IDSite ID (matches Commerce Cloud documentation nomenclature). Labels are sourced from the template's config-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 under lib/adapters/<type>/. Cross-cutting utilities remain at the lib/ root.

  • Replace deepMerge import from @salesforce/storefront-next-runtime/config with a local test-utils/deep-merge utility — the SDK no longer exports deepMerge as public API

  • Replace hand-rolled Mock<Component> Storybook stories with the real components for TrackingConsentBanner, CurrencySwitcher, and LocaleSwitcher. 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-context so currency submissions resolve immediately and locale submissions hang (preventing the unforgeable window.location.href redirect from crashing the test runner).

  • Replace the array-based SCAPI middleware context with a keyed ScapiMiddlewareRegistry. Consumers now call getScapiMiddlewareRegistry(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 useMatches with typed useRouteLoaderData in 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 on data(payload, init?) returns. See README-DATA.md for the pattern.

  • Rewrite extension descriptions in src/extensions/config.json to 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.getProducts request to an explicit expand list (availability,bundled_products,images,prices,promotions,variations) covering only the fields the cart UI consumes. Drops set_products, recommendations, links, options, custom_properties, and validation from the response.

  • Scope the mini cart's shopperProducts.getProducts request to an explicit expand list covering only the fields the mini cart UI consumes.

  • Simplify .env.default and decouple E2E baseline overrides

  • Split vite.config.ts into per-plugin files under vite-plugins/, and extract checkout chunking into a dedicated codeSplitting plugin. Plugin order now puts codeSplitting before storefrontNext so the SDK's i18n plugin can wrap the user-supplied manualChunks.

  • 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/, slim preview.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 productSearch response payload on PLPs by requesting only the image viewTypes the OOTB product tile reads (medium for the hero, swatch for color thumbnails) via the imgTypes query parameter. Configurable via search.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 to undefined to 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 null baselines: cart-quantity-picker, cart-item-edit-button, and cart-item-modal. Replaced full-replacement vi.mock('react-router') with importOriginal partial mocks so RouterProvider keeps working and <Story /> actually renders into the snapshot. Re-recorded the 12 affected snapshots.

  • Normalize the payment-schedule-modal-content component-folder layout per Pattern 13. The component lived as a flat info-modal/renderers/payment-schedule-modal-content.tsx next to its payment-schedule-modal-content/stories/ folder; moved it into payment-schedule-modal-content/index.tsx so component, stories, and snapshots all live under one folder. Imports unchanged thanks to TypeScript module resolution; only the relative ../types was retargeted to ../../types from 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: getOrCreateWishlist no longer blocks the loader for ~1.5s on every fresh customerId. The POST response from createCustomerProductList is now used directly when it carries an id, eliminating the hardcoded setTimeout(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 without id, gated by a warn log so production telemetry can confirm. Affects every wishlist-mounting route (homepage, cart, PDP, PLP, search, account overview) on first load after SLAS issues a new gcid.

  • /wishlist no 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/config public 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-hooks module from extension target-config.json registrations, enabling server-side extension points in checkout actions

  • Add baseConfigPlugin that contributes framework-wide Vite defaults (React/React Router dedupe, pre-bundled React Router entries) so these don't have to live in the template's vite.config.ts

  • Add enabled boolean field to target-config.json component entries: components with enabled: false are skipped at build time and tree-shaken from the production bundle; entries without the field default to true (backward compatible)

  • Add @salesforce/storefront-next-runtime as a peer dependency so the CLI can read the runtime's BUILT_IN_CLIENT_DEFAULTS map directly — keeping the per-client config (basePath, locale-awareness) in sync with the SDK by construction

  • Add sfnext scapi add support 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 adds sfnext scapi available to survey the active tenant and report which built-in shopper APIs have c_* custom attributes worth overriding

  • Make allowedActionOrigins generic: read SFW_FUNCTIONAL_DOMAIN env 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 by storefrontNextTargets or available via dedicated subpath exports

  • Add editorDefinition support to the @AttributeDefinition decorator for Page Designer components. Custom attribute editors (e.g. einstein.globalrecommenderselector) can now be declared on a decorated attribute and are emitted as editor_definition in the generated cartridge JSON. Per the SFCC schema, editor_definition is only valid on attributes of type custom.

  • 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.json under a namespaced storefrontNext block (templateRelease, templateVersion, minSdkVersion), so a customer can always tell which template release they generated from. create-storefront surfaces 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/recommendations returning HTTP 403 on MRT by enabling Express trust proxy and using resolveRequestOrigin in the same-origin gate

  • Bump handlebars from 4.7.8 to 4.7.9 in storefront-next-dev and add pnpm overrides for @isaacs/brace-expansion@^5.0.1, axios@^1.15.2, basic-ftp@^5.3.1, flatted@^3.4.2, and shell-quote@^1.8.4. Overrides live in both pnpm-workspace.yaml (protects this monorepo's dev/CI) and template-retail-rsc-app's pnpm.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-sdk to ^1.11.0 and @salesforce/b2c-cli to ^1.12.0 (latest published versions). Scope the oclif init hook 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); standalone sfnext behavior 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; and remix-i18next/middleware, used by the SDK's /i18n barrel) — these are discovered late for the same reason. (react-i18next needs no entry: the template imports it directly, so Vite's initial source crawl already finds it.) All are now in the first-pass optimizeDeps.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 appends ws://localhost:*, ws://127.0.0.1:*, and wss://localhost:* to connect-src only when running locally (BUNDLE_ID unset or local). Deployed/MRT responses are unchanged.
    - Nonce hydration mismatch on the inline window.__APP_CONFIG__ script: browsers strip the nonce content attribute from the DOM after applying CSP, so the client saw nonce="" against the server's real value. Added suppressHydrationWarning to 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 Content responses, and 5xx error responses returned without a body — produced a generic 502 InternalServerErrorException from 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_redirect cartridge actually redirects in local development. plugin_redirect sometimes returns HTTP 200 with a Location header instead of a proper 3xx redirect; browsers only follow Location on 3xx responses, so the proxied page rendered blank. The proxyRes handler now detects this case (status 200 + non-empty Location), converts it to a 302 with the localhost-rewritten Location, drops the upstream body, and preserves Set-Cookie so session continuity is maintained. Affects local development only — production routing is handled by Cloudflare eCDN and is unaffected.

  • Drop stale SFDC_EXT_THEME_SWITCHER references after the theme-switcher extension was removed from the template: replace fixtures in extensibility/dependency-utils.test.ts and extensibility/target-utils.test.ts with SFDC_EXT_MULTISHIP/multiship, and refresh CLI examples in extensions install, extensions remove, and the package README to use SFDC_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 two dist/site-context.js module records and two createContext keys. baseConfigPlugin now sets ssr.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 (managedRuntimeBundlePlugin already inlines the SDK). Reverts the relevant part of.

  • Fix managedRuntimeBundle plugin build error when a CSS file references a public/ or bundled asset (e.g. @font-face { src: url(...) }). renderBuiltUrl now returns { relative: true } for hostType === 'css' instead of the JS runtime expression Vite rejects inside CSS url. The browser resolves the relative path against the stylesheet's own URL — which already carries the per-deploy BUNDLE_ID — so the reference stays correct across deploys

  • Mark Scripts component (./react-router/Scripts) as private internal API — it is automatically applied via the patchReactRouter Vite plugin and should not be imported directly

  • Rename the configuration labels that the sfnext create-storefront command asks for when run against the bundled retail template, to align with Commerce Cloud documentation: SLAS Client ID (formerly API Client ID), Organization ID (formerly API Organization ID), Short Code (formerly API Short Code), Site ID (formerly Default Site ID). Labels are sourced from the template's config-meta.json.

  • The sfnext scapi codegen now emits a template-local src/scapi/index.ts barrel that wildcards the runtime SCAPI surface and shadows overridden namespaces with locally-generated wrappers; sfnext scapi list groups output into Overrides and Custom APIs sections; sfnext scapi remove cleans up the per-override namespace wrapper alongside the schema/types/operations files

  • Validate allowedActionOrigins in reactRouterConfigResolved to 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/config public 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 (and BUILT_IN_CLIENT_KEYS, isBuiltInClientKey) — a single source of truth for per-built-in-client config (basePath, locale-awareness) consumed by both createCommerceApiClients and the dev CLI's SCAPI override codegen. createCommerceApiClients now drives its baseUrl + globalParams selection from this map so the two never drift

  • Add createLazyDataStoreMiddleware and readLazyDataStoreEntry exports 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. Internal loadDataStoreEntry helper unifies the eager and lazy fetch + error paths.

  • Add MRT-side attribute resolution for Page Designer manifests: processPage now walks each component's data map through a type-driven dispatch table that converts manifest envelopes into the same wire shape SCAPI's getPage controller would have returned. Covers image (URL resolution via resolveMediaUrl), file (envelope → URL string), markup (?$staticlink$ placeholder rewriting via the new rewriteMarkup helper), and cms_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 an AttributeResolutionContext (host, resolveMediaUrl, optional pageLibraryDomain).

  • 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 PageManifest with optional componentTypes (per-typeId attribute definitions hoisted by the manifest builder) and pageLibraryDomain (used by the markup URL rewriter). Both are optional so older manifests still load. componentInfo entries gain optional name, fragment, and custom fields, and previously-required visibilityRules/regions are now optional (omitted when empty). RegionInfo fields (name, componentTypeExclusions, componentTypeInclusions, maxComponents) shift from T | null to 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 (createSecurityHeadersMiddleware exported 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 via config.server.ts; defaultCspDirectives is exported for spread.

Patch Changes

  • Demote expected DataStoreNotFoundError logs from warn to debug to eliminate dev server log noise

  • Security middleware: relax frame-ancestors only on Page Designer / Business Manager preview requests (?mode=EDIT|PREVIEW). On those requests the SDK extends frame-ancestors to allow Salesforce-owned admin host families (*.unified.demandware.net, *.commercecloud.salesforce.com, *.demandware.net) and suppresses X-Frame-Options (which has no host-list form). Normal shopper traffic continues to ship frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN. Customers who already extend frame-ancestors themselves 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 null and 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. DataStoreServiceError and DataStoreUnavailableError now both honor onUnavailable, 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_MODE is preserved as an opt-in escape hatch — set it to 'throw' to restore fail-fast behavior. DataStoreNotFoundError keeps its existing missing-state semantics; errors thrown from transform still 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.sendBeacon calls to the CQuotient activities API (https://api.cquotient.com/v3/activities/...), which is governed by connect-src. The default connect-src did 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. Added https://api.cquotient.com to the default connect-src directive.

  • 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; and remix-i18next/middleware, used by the SDK's /i18n barrel) — these are discovered late for the same reason. (react-i18next needs no entry: the template imports it directly, so Vite's initial source crawl already finds it.) All are now in the first-pass optimizeDeps.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 appends ws://localhost:*, ws://127.0.0.1:*, and wss://localhost:* to connect-src only when running locally (BUNDLE_ID unset or local). Deployed/MRT responses are unchanged.
    - Nonce hydration mismatch on the inline window.__APP_CONFIG__ script: browsers strip the nonce content attribute from the DOM after applying CSP, so the client saw nonce="" against the server's real value. Added suppressHydrationWarning to 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. resolveDynamicPageId and resolvePage now accept an optional categoryId (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 triggering AuthTokenInvalidError

  • Make @react-router/dev and @react-router/fs-routes optional peer dependencies to avoid dependency conflicts for consumers that only use the /design exports

  • processPage now requires defaultLocale and attrCtx in its PageProcessorContext and accepts an optional componentTypes map. Component data is 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 page data is also resolved when present.

  • Re-export createAuthHelpers from the SCAPI entry point so the template can rebuild clients.auth against an overridden shopperLogin client

  • 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 usid from PasswordlessExchangeTokenOptions and the exchangeToken implementation — usid is not a valid field in the SLAS PasswordLessLoginTokenRequest schema (auth-oas-1.48.0) and was silently ignored by the server

  • Unify sanitizePrefix and stripPathPrefix into one helper

  • VariationEntry.page now carries SCAPI-shape page metadata (name, aspectTypeId, description, pageTitle, pageDescription, pageKeywords) populated by the manifest builder; non-default-locale overrides live on a new pageContent overlay 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.