diff --git a/packages/pwa-kit-create-app/CHANGELOG.md b/packages/pwa-kit-create-app/CHANGELOG.md index 4b1dbf1adc..35ad7ea4df 100644 --- a/packages/pwa-kit-create-app/CHANGELOG.md +++ b/packages/pwa-kit-create-app/CHANGELOG.md @@ -1,5 +1,6 @@ ## v3.16.0-dev (Dec 17, 2025) - Move envBasePath into ssrParameters [#3590](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3590) +- Support adding base paths to shopper facing URLs [#3615](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3615) ## v3.15.0 (Dec 17, 2025) - Add new Google Cloud API configuration and Bonus Product configuration [#3523](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3523) diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs index 6091197612..d742ad0167 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs @@ -45,6 +45,9 @@ module.exports = { locale: 'path', // This boolean value dictates whether or not default site or locale values are shown in the url. Defaults to: false showDefaults: true, + // This boolean value dictates whether or not the base path, defined in ssrParameters.envBasePath, + // is shown in shopper facing urls. Defaults to: false + showBasePath: false, {{else}} // Determine where the siteRef is located. Valid values include 'path|query_param|none'. Defaults to: 'none' site: 'none', @@ -52,6 +55,9 @@ module.exports = { locale: 'none', // This boolean value dictates whether or not default site or locale values are shown in the url. Defaults to: false showDefaults: false, + // This boolean value dictates whether or not the base path, defined in ssrParameters.envBasePath, + // is shown in shopper facing urls. Defaults to: false + showBasePath: false, {{/if}} // This boolean value dictates whether the plus sign (+) is interpreted as space for query param string. Defaults to: false interpretPlusSignAsSpace: false diff --git a/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs b/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs index b4a5f31841..0f03d0c855 100644 --- a/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs +++ b/packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs @@ -45,6 +45,9 @@ module.exports = { locale: 'path', // This boolean value dictates whether default site or locale values are shown in the url. Defaults to: false showDefaults: true, + // This boolean value dictates whether or not the base path, defined in ssrParameters.envBasePath, + // is shown in shopper facing urls. Defaults to: false + showBasePath: false, {{else}} // Determine where the siteRef is located. Valid values include 'path|query_param|none'. Defaults to: 'none' site: 'none', @@ -52,6 +55,9 @@ module.exports = { locale: 'none', // This boolean value dictates whether or not default site or locale values are shown in the url. Defaults to: false showDefaults: false, + // This boolean value dictates whether or not the base path, defined in ssrParameters.envBasePath, + // is shown in shopper facing urls. Defaults to: false + showBasePath: false, {{/if}} // This boolean value dictates whether the plus sign (+) is interpreted as space for query param string. Defaults to: false interpretPlusSignAsSpace: false diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md index f5b23db119..ab070910ae 100644 --- a/packages/pwa-kit-react-sdk/CHANGELOG.md +++ b/packages/pwa-kit-react-sdk/CHANGELOG.md @@ -1,5 +1,6 @@ ## v3.16.0-dev (Dec 17, 2025) - Move envBasePath into ssrParameters [#3590](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3590) +- Support adding base paths to shopper facing URLs [#3615](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3615) ## v3.15.0 (Dec 17, 2025) diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx index 79b09b2790..92bb229e86 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx +++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx @@ -17,6 +17,7 @@ import {loadableReady} from '@loadable/component' import {uuidv4} from '../../utils/uuidv4.client' import PropTypes from 'prop-types' import logger from '../../utils/logger-instance' +import {getRouterBasePath} from '../universal/utils' /* istanbul ignore next */ export const registerServiceWorker = (url) => { @@ -44,10 +45,11 @@ export const registerServiceWorker = (url) => { export const OuterApp = ({routes, error, WrappedApp, locals, onHydrate}) => { const AppConfig = getAppConfig() const isInitialPageRef = useRef(true) + const routerBasename = getRouterBasePath() || undefined return ( - + { // If we are hydrating an error page use the server correlation id. diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js index abd4215110..3b18b959cf 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js +++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js @@ -40,6 +40,8 @@ import logger from '../../utils/logger-instance' import PerformanceTimer from '../../utils/performance' import {PERFORMANCE_MARKS} from '../../utils/performance-marks' +import {getRouterBasePath} from '../universal/utils' + const CWD = process.cwd() const BUNDLES_PATH = path.resolve(CWD, 'build/loadable-stats.json') @@ -270,9 +272,11 @@ export const render = (req, res, next) => { const OuterApp = ({req, res, error, App, appState, routes, routerContext, location}) => { const AppConfig = getAppConfig() + const routerBasename = getRouterBasePath() || undefined + return ( - + ({ + getConfig: jest.fn() +})) +jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths', () => ({ + getEnvBasePath: jest.fn() +})) describe('getProxyConfigs (client-side)', () => { const configs = [{foo: 'bar'}] @@ -33,3 +42,51 @@ describe('getAssetUrl (client-side)', () => { expect(utils.getAssetUrl('/path')).toBe('test.com/path') }) }) + +describe('getRouterBasePath (client-side)', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should return base path when showBasePath is true', () => { + const mockBasePath = '/test-base' + getEnvBasePath.mockReturnValue(mockBasePath) + getConfig.mockReturnValue({ + app: { + url: { + showBasePath: true + } + } + }) + + expect(utils.getRouterBasePath()).toBe(mockBasePath) + }) + + test('should return empty string when showBasePath is undefined', () => { + getConfig.mockReturnValue({ + app: { + url: {} + } + }) + + expect(utils.getRouterBasePath()).toBe('') + }) + + test('should return empty string when showBasePath is false', () => { + getConfig.mockReturnValue({ + app: { + url: { + showBasePath: false + } + } + }) + + expect(utils.getRouterBasePath()).toBe('') + }) + + test('should return empty string when app config is missing', () => { + getConfig.mockReturnValue({}) + + expect(utils.getRouterBasePath()).toBe('') + }) +}) diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js index ada90c474a..ec94813bb4 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.js @@ -9,6 +9,7 @@ */ import {proxyConfigs} from '@salesforce/pwa-kit-runtime/utils/ssr-shared' import {getEnvBasePath, bundleBasePath} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths' +import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config' const onClient = typeof window !== 'undefined' @@ -53,3 +54,21 @@ export const getProxyConfigs = () => { // Clone to avoid accidental mutation of important configuration variables. return configs.map((config) => ({...config})) } + +/** + * Returns the base path (defined in config.ssrParameters.envBasePath) + * for React Router routes when showBasePath is enabled in the app config. + * + * This function should be used when working with a React Router route + * (The route is defined in routes.jsx). + * + * Use getEnvBasePath (pwa-kit-runtime) if you are working with an express route\ + * (The route is defined in ssr.js). + * + * @returns {string} - The base path if showBasePath is true, otherwise an empty string + */ +export const getRouterBasePath = () => { + const config = getConfig() + const showBasePath = config?.app?.url?.showBasePath === true + return showBasePath ? getEnvBasePath() : '' +} diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.server.test.js b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.server.test.js index fbb948ff35..a53aa6c0b9 100644 --- a/packages/pwa-kit-react-sdk/src/ssr/universal/utils.server.test.js +++ b/packages/pwa-kit-react-sdk/src/ssr/universal/utils.server.test.js @@ -13,9 +13,66 @@ import * as utils from './utils' import {proxyConfigs} from '@salesforce/pwa-kit-runtime/utils/ssr-shared' +import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config' +import {getEnvBasePath} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths' + +jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({ + getConfig: jest.fn() +})) +jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths', () => ({ + getEnvBasePath: jest.fn() +})) describe('getProxyConfigs (server-side)', () => { test('should return the currently used proxy configs', () => { expect(utils.getProxyConfigs()).toEqual(proxyConfigs) }) }) + +describe('getRouterBasePath (server-side)', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should return base path when showBasePath is true', () => { + const mockBasePath = '/test-base' + getEnvBasePath.mockReturnValue(mockBasePath) + getConfig.mockReturnValue({ + app: { + url: { + showBasePath: true + } + } + }) + + expect(utils.getRouterBasePath()).toBe(mockBasePath) + }) + + test('should return empty string when showBasePath is undefined', () => { + getConfig.mockReturnValue({ + app: { + url: {} + } + }) + + expect(utils.getRouterBasePath()).toBe('') + }) + + test('should return empty string when showBasePath is false', () => { + getConfig.mockReturnValue({ + app: { + url: { + showBasePath: false + } + } + }) + + expect(utils.getRouterBasePath()).toBe('') + }) + + test('should return empty string when app config is missing', () => { + getConfig.mockReturnValue({}) + + expect(utils.getRouterBasePath()).toBe('') + }) +}) diff --git a/packages/pwa-kit-runtime/CHANGELOG.md b/packages/pwa-kit-runtime/CHANGELOG.md index b484394ec8..991dfa4ede 100644 --- a/packages/pwa-kit-runtime/CHANGELOG.md +++ b/packages/pwa-kit-runtime/CHANGELOG.md @@ -1,5 +1,6 @@ ## v3.16.0-dev (Dec 17, 2025) - Move envBasePath into ssrParameters [#3590](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3590) +- Support adding base paths to shopper facing URLs [#3615](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3615) ## v3.15.0 (Dec 17, 2025) - Fix multiple set-cookie headers [#3508](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3508) diff --git a/packages/pwa-kit-runtime/src/utils/ssr-namespace-paths.js b/packages/pwa-kit-runtime/src/utils/ssr-namespace-paths.js index 54c1250556..41358192ce 100644 --- a/packages/pwa-kit-runtime/src/utils/ssr-namespace-paths.js +++ b/packages/pwa-kit-runtime/src/utils/ssr-namespace-paths.js @@ -23,7 +23,15 @@ const SLAS_PRIVATE_CLIENT_PROXY_PATH = `${MOBIFY_PATH}/slas/private` /* * Returns the base path. This is prepended to a /mobify path. - * Returns an empty string if the base path is not set or is '/'. + * Returns an empty string if the base path is not set. + * Throws an error if the base path is not valid. + * + * Use this function if you are working with an express route + * (ie. The route is defined in ssr.js). + * + * Use getRouterBasePath (pwa-kit-react-sdk) if you are working + * with a React Router route + * (ie. The route is defined in routes.jsx). */ export const getEnvBasePath = () => { let basePath = '' diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index bc2a1ecf06..4e89a27d1c 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -1,5 +1,6 @@ ## v8.4.0-dev (Dec 17, 2025) - Move envBasePath into ssrParameters [#3590](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3590) +- Support adding base paths to shopper facing URLs [#3615](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3615) - [Feature] Add `fuzzyPathMatching` to reduce computational overhead of route generation at time of application load [#3530](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3530) - [Bugfix] Fix Passwordless Login landingPath, Reset Password landingPath, and Social Login redirectUri value in config not being used [#3560](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3560) - [Feature] PWA Integration with OMS diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx index 34e310bfb0..f545660243 100644 --- a/packages/template-retail-react-app/app/components/_app/index.jsx +++ b/packages/template-retail-react-app/app/components/_app/index.jsx @@ -9,7 +9,7 @@ import React, {useState, useEffect, useMemo} from 'react' import PropTypes from 'prop-types' import {useHistory, useLocation} from 'react-router-dom' import {StorefrontPreview} from '@salesforce/commerce-sdk-react/components' -import {getAssetUrl} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' +import {getAssetUrl, getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' import useActiveData from '@salesforce/retail-react-app/app/hooks/use-active-data' import {useQuery} from '@tanstack/react-query' import { @@ -293,6 +293,11 @@ const App = (props) => { trackPage() }, [location]) + const getHrefForLocale = (localeId) => + `${appOrigin}${getRouterBasePath()}${getPathWithLocale(localeId, buildUrl, { + location: {...location, search: ''} + })}` + return ( @@ -340,12 +345,7 @@ const App = (props) => { ))} @@ -353,12 +353,7 @@ const App = (props) => { {/* A wider fallback for user locales that the app does not support */} diff --git a/packages/template-retail-react-app/app/components/_error/index.jsx b/packages/template-retail-react-app/app/components/_error/index.jsx index aed17a8081..f0eaa2e785 100644 --- a/packages/template-retail-react-app/app/components/_error/index.jsx +++ b/packages/template-retail-react-app/app/components/_error/index.jsx @@ -18,6 +18,7 @@ import { } from '@salesforce/retail-react-app/app/components/shared/ui' import {BrandLogo, FileIcon} from '@salesforce/retail-react-app/app/components/icons' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' // is rendered when: // @@ -53,7 +54,11 @@ const Error = (props) => { // We need to use window.location.href here rather than history // as the application is in an error state. We need to force a // hard navigation to get back to the normal state. - onClick={() => (window.location.href = '/')} + // Include base path since this bypasses React Router + onClick={() => { + const basePath = getRouterBasePath() + window.location.href = basePath ? `${basePath}/` : '/' + }} /> diff --git a/packages/template-retail-react-app/app/components/drawer-menu/drawer-menu.jsx b/packages/template-retail-react-app/app/components/drawer-menu/drawer-menu.jsx index 699a48691e..b4e43456c9 100644 --- a/packages/template-retail-react-app/app/components/drawer-menu/drawer-menu.jsx +++ b/packages/template-retail-react-app/app/components/drawer-menu/drawer-menu.jsx @@ -54,6 +54,7 @@ import { // Others import {noop} from '@salesforce/retail-react-app/app/utils/utils' import {getPathWithLocale, categoryUrlBuilder} from '@salesforce/retail-react-app/app/utils/url' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner' import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation' @@ -302,7 +303,10 @@ const DrawerMenu = ({ const newUrl = getPathWithLocale(newLocale, buildUrl, { disallowParams: ['refine'] }) - window.location = newUrl + const basePath = getRouterBasePath() + window.location = basePath + ? `${basePath}${newUrl}` + : newUrl }} /> diff --git a/packages/template-retail-react-app/app/components/footer/index.jsx b/packages/template-retail-react-app/app/components/footer/index.jsx index 9500aad913..d341de5421 100644 --- a/packages/template-retail-react-app/app/components/footer/index.jsx +++ b/packages/template-retail-react-app/app/components/footer/index.jsx @@ -27,6 +27,7 @@ import LinksList from '@salesforce/retail-react-app/app/components/links-list' import SocialIcons from '@salesforce/retail-react-app/app/components/social-icons' import {HideOnDesktop, HideOnMobile} from '@salesforce/retail-react-app/app/components/responsive' import {getPathWithLocale} from '@salesforce/retail-react-app/app/utils/url' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' import LocaleText from '@salesforce/retail-react-app/app/components/locale-text' import useMultiSite from '@salesforce/retail-react-app/app/hooks/use-multi-site' import styled from '@emotion/styled' @@ -155,8 +156,8 @@ const Footer = ({...otherProps}) => { const newUrl = getPathWithLocale(target.value, buildUrl, { disallowParams: ['refine'] }) - - window.location = newUrl + const basePath = getRouterBasePath() + window.location = basePath ? `${basePath}${newUrl}` : newUrl }} variant="filled" aria-label={intl.formatMessage({ diff --git a/packages/template-retail-react-app/app/utils/site-utils.js b/packages/template-retail-react-app/app/utils/site-utils.js index 740046d007..b422e36e8b 100644 --- a/packages/template-retail-react-app/app/utils/site-utils.js +++ b/packages/template-retail-react-app/app/utils/site-utils.js @@ -6,6 +6,7 @@ */ import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' /** * This functions takes an url and returns a site object, @@ -101,7 +102,14 @@ export const getSiteByReference = (siteRef) => { * @returns {{siteRef: string, localeRef: string}} - site and locale reference (it could either be id or alias) */ export const getParamsFromPath = (path) => { - const {pathname, search} = getPathnameAndSearch(path) + let {pathname, search} = getPathnameAndSearch(path) + + // Remove the base path from the pathname if present since + // it shifts the location of the site and locale in the pathname + const basePath = getRouterBasePath() + if (basePath && pathname.startsWith(basePath)) { + pathname = pathname.substring(basePath.length) + } const config = getConfig() const {pathMatcher, searchMatcherForSite, searchMatcherForLocale} = getConfigMatcher(config) diff --git a/packages/template-retail-react-app/app/utils/site-utils.test.js b/packages/template-retail-react-app/app/utils/site-utils.test.js index 169685c63c..456b46d17c 100644 --- a/packages/template-retail-react-app/app/utils/site-utils.test.js +++ b/packages/template-retail-react-app/app/utils/site-utils.test.js @@ -11,6 +11,7 @@ import { resolveSiteFromUrl } from '@salesforce/retail-react-app/app/utils/site-utils' import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' import mockConfig from '@salesforce/retail-react-app/config/mocks/default' import { @@ -25,8 +26,18 @@ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => { } }) +jest.mock('@salesforce/pwa-kit-react-sdk/ssr/universal/utils', () => { + const original = jest.requireActual('@salesforce/pwa-kit-react-sdk/ssr/universal/utils') + return { + ...original, + getRouterBasePath: jest.fn(() => '') + } +}) + beforeEach(() => { jest.resetModules() + // Reset the mock after resetModules + getRouterBasePath.mockReturnValue('') }) afterEach(() => { @@ -310,6 +321,27 @@ describe('getParamsFromPath', function () { expect(getParamsFromPath(path)).toEqual(expectedRes) }) }) + + describe('getParamsFromPath with base path', () => { + test('should remove base path from path when showBasePath is true', () => { + const basePath = '/test-base' + getRouterBasePath.mockReturnValue(basePath) + getConfig.mockImplementation(() => ({ + ...mockConfig, + app: { + ...mockConfig.app, + url: { + ...mockConfig.app.url, + showBasePath: true + } + } + })) + + const path = `${basePath}/us/en-US/category/womens` + const result = getParamsFromPath(path) + expect(result).toEqual({siteRef: 'us', localeRef: 'en-US'}) + }) + }) }) describe('resolveLocaleFromUrl', function () { diff --git a/packages/template-retail-react-app/app/utils/url.js b/packages/template-retail-react-app/app/utils/url.js index 25f9a60418..84a2257a40 100644 --- a/packages/template-retail-react-app/app/utils/url.js +++ b/packages/template-retail-react-app/app/utils/url.js @@ -13,6 +13,7 @@ import { getSiteByReference } from '@salesforce/retail-react-app/app/utils/site-utils' import {HOME_HREF, urlPartPositions} from '@salesforce/retail-react-app/app/constants' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' /** * Constructs an absolute URL from a given path and an optional application origin. @@ -119,19 +120,25 @@ export const searchUrlBuilder = (searchTerm) => '/search?q=' + encodeURIComponen /** * Returns a relative URL for a locale short code. - * Based on your app configuration, this function will replace your current locale shortCode with a new one + * Based on your app configuration, this function will replace your current locale shortCode with a new one. * * @param {String} shortCode - The locale short code. * @param {function(*, *, *, *=): string} - Generates a site URL from the provided path, site and locale. * @param {string[]} opts.disallowParams - URL parameters to remove * @param {Object} opts.location - location object to replace the default `window.location` - * @returns {String} url - The relative URL for the specific locale. + * @returns {String} url - The relative URL for the specific locale (without base path). */ export const getPathWithLocale = (shortCode, buildUrl, opts = {}) => { const location = opts.location ? opts.location : window.location let {siteRef, localeRef} = getParamsFromPath(`${location.pathname}${location.search}`) let {pathname, search} = location + // sanitize the base path from current url if existing + const basePath = getRouterBasePath() + if (basePath && pathname.startsWith(basePath)) { + pathname = pathname.substring(basePath.length) + } + // sanitize the site from current url if existing if (siteRef) { pathname = pathname.replace(`/${siteRef}`, '') @@ -167,6 +174,7 @@ export const getPathWithLocale = (shortCode, buildUrl, opts = {}) => { site.alias || site.id, locale?.alias || locale?.id ) + return newUrl } @@ -224,6 +232,7 @@ export const createUrlTemplate = (appConfig, siteRef, localeRef) => { queryLocale && locale && searchParams.append('locale', locale) queryString = `?${searchParams.toString()}` } + return `${sitePath}${localePath}${path}${queryString}` } } @@ -273,6 +282,11 @@ export const removeQueryParamsFromPath = (path, keys) => { export const removeSiteLocaleFromPath = (pathName = '') => { let {siteRef, localeRef} = getParamsFromPath(pathName) + const basePath = getRouterBasePath() + if (basePath && pathName.startsWith(basePath)) { + pathName = pathName.substring(basePath.length) + } + // remove the site alias from the current pathName if (siteRef) { pathName = pathName.replace(new RegExp(`/${siteRef}`, 'g'), '') diff --git a/packages/template-retail-react-app/app/utils/url.test.js b/packages/template-retail-react-app/app/utils/url.test.js index b36784822d..ab01877433 100644 --- a/packages/template-retail-react-app/app/utils/url.test.js +++ b/packages/template-retail-react-app/app/utils/url.test.js @@ -20,6 +20,7 @@ import { } from '@salesforce/retail-react-app/app/utils/url' import {getUrlConfig} from '@salesforce/retail-react-app/app/utils/site-utils' import mockConfig from '@salesforce/retail-react-app/config/mocks/default' +import {getRouterBasePath} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils' afterEach(() => { jest.clearAllMocks() @@ -32,19 +33,20 @@ jest.mock('@salesforce/pwa-kit-react-sdk/utils/url', () => { getAppOrigin: jest.fn(() => 'https://www.example.com') } }) -jest.mock('./utils', () => { - const original = jest.requireActual('./utils') + +jest.mock('./site-utils', () => { + const original = jest.requireActual('./site-utils') return { ...original, - getConfig: jest.fn(() => mockConfig) + getUrlConfig: jest.fn() } }) -jest.mock('./site-utils', () => { - const original = jest.requireActual('./site-utils') +jest.mock('@salesforce/pwa-kit-react-sdk/ssr/universal/utils', () => { + const original = jest.requireActual('@salesforce/pwa-kit-react-sdk/ssr/universal/utils') return { ...original, - getUrlConfig: jest.fn() + getRouterBasePath: jest.fn(() => '') } }) @@ -187,6 +189,22 @@ describe('getPathWithLocale', () => { const relativeUrl = getPathWithLocale('en-GB', buildUrl, {location}) expect(relativeUrl).toBe(`/`) }) + + test('getPathWithLocale returns path without base path if base path is present', () => { + const basePath = '/test-base' + getRouterBasePath.mockReturnValue(basePath) + + const location = new URL( + `http://localhost:3000${basePath}/uk/it-IT/category/newarrivals-womens` + ) + const buildUrl = createUrlTemplate(mockConfig.app, 'uk', 'it-IT') + + const path = getPathWithLocale('fr-FR', buildUrl, {location}) + expect(path).toBe('/uk/fr/category/newarrivals-womens') + expect(path).not.toContain(basePath) + // Caller uses basePath + path for window.location or full href + expect(`${basePath}${path}`).toBe(`${basePath}/uk/fr/category/newarrivals-womens`) + }) }) describe('createUrlTemplate tests', () => { diff --git a/packages/template-retail-react-app/config/default.js b/packages/template-retail-react-app/config/default.js index 100c8f15e3..73fab1312e 100644 --- a/packages/template-retail-react-app/config/default.js +++ b/packages/template-retail-react-app/config/default.js @@ -27,6 +27,7 @@ module.exports = { site: 'path', locale: 'path', showDefaults: true, + showBasePath: false, interpretPlusSignAsSpace: false }, login: {