Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/pwa-kit-create-app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@ 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',
// Determine where the localeRef is located. Valid values include 'path|query_param|none'. Defaults to: 'none'
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@ 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',
// Determine where the localeRef is located. Valid values include 'path|query_param|none'. Defaults to: 'none'
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
Expand Down
1 change: 1 addition & 0 deletions packages/pwa-kit-react-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
4 changes: 3 additions & 1 deletion packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 (
<ServerContext.Provider value={{}}>
<Router ref={onHydrate}>
<Router ref={onHydrate} basename={routerBasename}>
<CorrelationIdProvider
correlationId={() => {
// If we are hydrating an error page use the server correlation id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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 (
<ServerContext.Provider value={{req, res}}>
<Router location={location} context={routerContext}>
<Router location={location} context={routerContext} basename={routerBasename}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the basename prop here and in main.jsx automatically adds the base path to almost all URLs.
This includes links that use the Link component and places where we use history to navigate.

This basename prop does not apply to places where we update window.location directly, hence other changes in this PR.

<CorrelationIdProvider
correlationId={res.locals.requestId}
resetOnPageChange={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import * as utils from './utils'
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 (client-side)', () => {
const configs = [{foo: 'bar'}]
Expand Down Expand Up @@ -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('')
})
})
19 changes: 19 additions & 0 deletions packages/pwa-kit-react-sdk/src/ssr/universal/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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() : ''
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This utility function exists to support the feature toggle since calling getEnvBasePath directly bypasses the toggle.

I opted to separate this from getEnvBasePath to support cases where a project wants the base path only on express routes. For example, suppose a project wants to have www.example.com/emea/mobify/proxy/... (envBasePath: /emea) along with shopper facing urls like www.example.com/gb/category/.. or www.example.com/fr/product/.. where gb and fr are site aliases in our existing multi-site implementation and /emea is a base path for a target MRT environment. Without this separation, shopper facing urls would be www.example.com/emea/gb/category/...

Note that to support the above scenario, projects would need a CDN to route requests with /emea, /gb, /fr to the same MRT environment.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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('')
})
})
1 change: 1 addition & 0 deletions packages/pwa-kit-runtime/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
10 changes: 9 additions & 1 deletion packages/pwa-kit-runtime/src/utils/ssr-namespace-paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ''
Expand Down
1 change: 1 addition & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 19 additions & 11 deletions packages/template-retail-react-app/app/components/_app/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -340,25 +340,33 @@ const App = (props) => {
<link
rel="alternate"
hrefLang={locale.id.toLowerCase()}
href={`${appOrigin}${getPathWithLocale(locale.id, buildUrl, {
location: {
...location,
search: ''
href={`${appOrigin}${getRouterBasePath()}${getPathWithLocale(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: please break this down to a variable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

locale.id,
buildUrl,
{
location: {
...location,
search: ''
}
}
})}`}
)}`}
key={locale.id}
/>
))}
{/* A general locale as fallback. For example: "en" if default locale is "en-GB" */}
<link
rel="alternate"
hrefLang={site.l10n.defaultLocale.slice(0, 2)}
href={`${appOrigin}${getPathWithLocale(locale.id, buildUrl, {
location: {
...location,
search: ''
href={`${appOrigin}${getRouterBasePath()}${getPathWithLocale(
locale.id,
buildUrl,
{
location: {
...location,
search: ''
}
}
})}`}
)}`}
/>
{/* A wider fallback for user locales that the app does not support */}
<link rel="alternate" hrefLang="x-default" href={`${appOrigin}/`} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

// <Error> is rendered when:
//
Expand Down Expand Up @@ -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}/` : '/'
}}
/>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}}
/>
</Box>
Expand Down
Loading
Loading