Skip to content

fix: keep shopper agent product links on the PWA storefront (W-23948585) - #4076

Merged
ashwinshanker-sf merged 10 commits into
developfrom
fix/shopper-agent-product-links-pwa-route
Nov 2, 2026
Merged

ashwinshanker-sf merged 10 commits into
developfrom
fix/shopper-agent-product-links-pwa-route

Conversation

@ashwinshanker-sf

@ashwinshanker-sf ashwinshanker-sf commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Fixes W-23948585. In the Commerce Client (Cimulate) shopper agent, clicking a product card sent shoppers to the SFRA product page instead of the PWA product page.

The agent backend (plugin_commerce_client, an SFRA-only cartridge) builds product links with URLUtils.abs('Product-Show', ...). So each card link is an SFRA URL on the backend origin, e.g. https://<backend>/en_US/dipsea-tie-headband-2040793A5H.html. The widget's onNavigate only fires for same-origin links, so these clicks were never routed through the PWA.

The same logic is in storefront-next#2944.

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Changes

  • New app/utils/shopper-agent-product-links.js:
    • Finds links in the widget's product slots by their cim-widget-* classes: carousel cards, the product detail title and comparison columns. Links in agent message text and card descriptions are skipped.
    • Reads the product id from the backend URL when it is certain: the pid parameter, or a last path segment with no - (/<slug>/<id>.html).
    • A segment with a - is ambiguous: it may be <slug>-<id> or an id that itself has dashes (DH-387, t-shirt-123). Those are looked up through Shopper SEO getUrlMapping (useCommerceApi('shopperSeo')), which applies the site's own URL rules. Each backend address is looked up once. A failed lookup (offline, server error, token not ready) is not remembered, so it is retried the next time the widget draws the link.
    • Until the lookup answers, or when it finds no product, the link keeps its backend URL, so it never points at a storefront 404.
    • Rewrites the link's href to the PWA product page, so hover, copy-link, Cmd/Ctrl-click and new-tab all use it. Plain same-tab clicks go through the router.
    • Uses a MutationObserver to rewrite cards the widget draws later, and links it reuses for another product.
  • ShopperAgent (CommerceClientAgentWindow) attaches the routing and detaches it on unmount. The product path is /product/<id> with the id escaped as one path segment (encodeURIComponent), so an id with /, ? or # can't leave the PDP. useMultiSite().buildUrl adds the site/locale prefix.
  • If a widget release renames its classes, links keep the backend URL, so nothing breaks.

How to Test-Drive This PR

  1. Configure a storefront whose Commerce Client agent is backed by plugin_commerce_client.
  2. Ask the agent for products.
  3. Hover a product card. The link should show the PWA address /<site>/<locale>/product/<id>.
  4. Click it. The PWA PDP should open without a full page load.
  5. Cmd/Ctrl-click it. The PWA PDP should open in a new tab.
  6. Links inside agent message text should be unchanged.
  7. For a product whose link is <slug>-<id>.html, the network tab should show one Shopper SEO url-mapping call, after which the link shows the PWA address.

Unit tests: npm test -- app/utils/shopper-agent-product-links.test.js app/components/shopper-agent/index.test.js

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

🤖 Generated with Claude Code

@git2gus

git2gus Bot commented Oct 23, 2026

Copy link
Copy Markdown

Git2Gus App is installed but the .git2gus/config.json doesn't have right values. You should add the required configuration.

@cc-prodsec

cc-prodsec commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

ashwinshanker-sf and others added 2 commits October 23, 2026 14:33
The agent backend sends product links as SFRA URLs on the backend origin,
which the widget's onNavigate never sees, so product card clicks left the
PWA for the SFRA PDP. Read the product id from links in the widget's
product slots, point their href at the PWA product page, and route plain
clicks through the router. Port of storefront-next#2944.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ashwinshanker-sf
ashwinshanker-sf force-pushed the fix/shopper-agent-product-links-pwa-route branch from f96c7ee to baeb173 Compare October 23, 2026 21:34
@ashwinshanker-sf
ashwinshanker-sf marked this pull request as ready for review October 23, 2026 22:37
@ashwinshanker-sf
ashwinshanker-sf requested a review from a team as a code owner October 23, 2026 22:37

@joeluong-sfcc joeluong-sfcc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two notes. The first is worth a look, the second is optional. The rest of the change reads correctly: the effect deps are stable, the observer settles, and the tests cover the locale-switch re-attach.

return undefined // malformed percent-escape
}
const slugPrefix = productName ? `${toSlug(productName)}-` : ''
if (slugPrefix.length > 1 && segment.toLowerCase().startsWith(slugPrefix)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the card text slugs differently from B2C's URL (for example Café Chair becomes caf-chair, while the URL may say cafe-chair), the strip is skipped and the whole cafe-chair-<id> segment is returned as the product id. The card then links to a product page that 404s. Same result when the card text isn't the exact product name. Worth confirming B2C's slug rules match toSlug, or falling back to the part after the last - when the full segment doesn't resolve as a product.

@@ -1,4 +1,5 @@
## v10.4.0-dev (Oct 20, 2026)
- [Bugfix] Keep Commerce Client shopper-agent product links on the PWA Kit storefront. The agent backend sends product links as SFRA URLs on the backend origin (e.g. `/en_US/<slug>-<id>.html`), which the widget's `onNavigate` never sees, so clicking a product card left for the SFRA PDP. The storefront now reads the product id from those links in the widget's product slots (cards, product detail title, comparison columns), rewrites their `href` to the PWA product page, and routes plain clicks through the router. Links in agent message text are left alone. Port of storefront-next#2944. [#4076](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/4076) (W-23948585)

@joeluong-sfcc joeluong-sfcc Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[NIT] The internal ticket and the storefront-next reference can go as the PR link already covers traceability.

Suggested change
- [Bugfix] Keep Commerce Client shopper-agent product links on the PWA Kit storefront. The agent backend sends product links as SFRA URLs on the backend origin (e.g. `/en_US/<slug>-<id>.html`), which the widget's `onNavigate` never sees, so clicking a product card left for the SFRA PDP. The storefront now reads the product id from those links in the widget's product slots (cards, product detail title, comparison columns), rewrites their `href` to the PWA product page, and routes plain clicks through the router. Links in agent message text are left alone. Port of storefront-next#2944. [#4076](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/4076) (W-23948585)
- [Bugfix] Keep Commerce Client shopper-agent product links on the PWA Kit storefront. The agent backend sends product links as SFRA URLs on the backend origin (e.g. `/en_US/<slug>-<id>.html`), which the widget's `onNavigate` never sees, so clicking a product card left for the SFRA PDP. The storefront now reads the product id from those links in the widget's product slots (cards, product detail title, comparison columns), rewrites their `href` to the PWA product page, and routes plain clicks through the router. Links in agent message text are left alone. [#4076](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/4076)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Line 2 was edited for the lookup, but it still ends with (W-23948585) and still says "Port of storefront-next#2944". Customers read this file, so those two can go. The [#4076] link covers traceability.

…product-links-pwa-route

# Conflicts:
#	packages/template-retail-react-app/CHANGELOG.md
@ashwinshanker-sf
ashwinshanker-sf force-pushed the fix/shopper-agent-product-links-pwa-route branch from 30cfec8 to 229336d Compare October 29, 2026 21:59
try {
const productId = await lookupProductId(urlSegment)
if (productId) return productId
} catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A failed lookup (token not ready, offline, 5xx) is handled the same as "no product found". lookUp returns undefined, then lookedUp.set(backendHref, null) (line 257) stops that address from being looked up again while the routing stays attached, so the card keeps the SFRA URL. Only a not-found answer should cache null. Consider continuing to the next segment on not-found and rethrowing other errors, with the .then skipping the lookedUp.set when the lookup rejected, so the address stays retryable.

A failed Shopper SEO lookup (offline, token not ready, 5xx) was cached the
same as "no product found", so the link kept its backend URL for as long
as routing stayed attached. Only a not-found answer is cached now; a failed
lookup leaves the address retryable the next time the widget draws it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
joeluong-sfcc
joeluong-sfcc previously approved these changes Nov 2, 2026

@joeluong-sfcc joeluong-sfcc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

anikolicSf
anikolicSf previously approved these changes Nov 2, 2026
useEffect(
() =>
attachAgentProductLinkRouting({
navigateToProduct: (productId) => navigate(productUrlBuilder({id: productId})),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unescaped product id: The id comes from the backend URL (pid param or the decoded last segment), so it is untrusted. productUrlBuilder uses encodeURI, which does not escape /, ? or #.

For example, ?pid=../account gives /<site>/<locale>/product/../account, which the browser resolves to a different same-origin page. The storefront-next mirror (#2944) avoids this because createProductUrl encodes the id as a single path segment.

Suggested fix, in both navigateToProduct and getProductHref:

const toProductPath = (productId) => productUrlBuilder({id: encodeURIComponent(productId)})

Or skip the rewrite when the id contains /, ? or #.

if (!nameKey) return undefined
// The last split that matches wins, so a repeated separator after the name stays out of the id.
let productId
for (let i = segment.indexOf('-'); i !== -1; i = segment.indexOf('-', i + 1)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wrong id for hyphenated product ids: A name-prefix match is treated as conclusive, so it bypasses the Shopper SEO lookup. For product id t-shirt-123 shown as "T-Shirt" (URL /shirts/t-shirt-123.html), this returns 123. The link then points at /product/123, which is a different product or a 404.

The same loop is in the storefront-next mirror, where Prizm flagged it: https://github.com/commerce-emu/storefront-next/pull/2944#discussion_r4418837094

Suggested fix: when the segment has more than one -, or the part after the split could itself be a valid id, send it through lookupProductId instead of returning the split result. Please apply the same change in both PRs so they stay in sync.

- Any last path segment with `-` is ambiguous (`<slug>-<id>` or an id like
  `t-shirt-123`), so it is resolved through Shopper SEO instead of split by
  the product name shown.
- Encode the product id as one path segment when building the PDP link.
- Changelog: drop the ticket number and port note.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ashwinshanker-sf
ashwinshanker-sf merged commit e3a83a0 into develop Nov 2, 2026
41 checks passed
@ashwinshanker-sf
ashwinshanker-sf deleted the fix/shopper-agent-product-links-pwa-route branch November 2, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants