Repository navigation
fix: keep shopper agent product links on the PWA storefront (W-23948585) - #4076
Conversation
|
Git2Gus App is installed but the |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
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>
f96c7ee to
baeb173
Compare
…product-links-pwa-route # Conflicts: # packages/template-retail-react-app/CHANGELOG.md
joeluong-sfcc
left a comment
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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) | |||
There was a problem hiding this comment.
[NIT] The internal ticket and the storefront-next reference can go as the PR link already covers traceability.
| - [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) |
There was a problem hiding this comment.
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.
via Shopper SEO; changelog nit
…product-links-pwa-route # Conflicts: # packages/template-retail-react-app/CHANGELOG.md
30cfec8 to
229336d
Compare
| try { | ||
| const productId = await lookupProductId(urlSegment) | ||
| if (productId) return productId | ||
| } catch { |
There was a problem hiding this comment.
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>
| useEffect( | ||
| () => | ||
| attachAgentProductLinkRouting({ | ||
| navigateToProduct: (productId) => navigate(productUrlBuilder({id: productId})), |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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>
0cd9df0
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 withURLUtils.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'sonNavigateonly 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
Changes
app/utils/shopper-agent-product-links.js:cim-widget-*classes: carousel cards, the product detail title and comparison columns. Links in agent message text and card descriptions are skipped.pidparameter, or a last path segment with no-(/<slug>/<id>.html).-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 SEOgetUrlMapping(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.hrefto 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.MutationObserverto 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().buildUrladds the site/locale prefix.How to Test-Drive This PR
plugin_commerce_client./<site>/<locale>/product/<id>.<slug>-<id>.html, the network tab should show one Shopper SEOurl-mappingcall, 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.jsChecklists
General
Accessibility Compliance
You must check off all items in one of the follow two lists:
or...
Localization
🤖 Generated with Claude Code