Skip to content

feat(core): localized product URLs, breadcrumbs and hreflang for PDP#3352

Merged
hellofanny merged 29 commits into
feat/localized-slugs-linksfrom
feat/localized-product-links
Jun 19, 2026
Merged

feat(core): localized product URLs, breadcrumbs and hreflang for PDP#3352
hellofanny merged 29 commits into
feat/localized-slugs-linksfrom
feat/localized-product-links

Conversation

@hellofanny

@hellofanny hellofanny commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements localized product links for FastStore stores with localization.enabled: true (PDP only — PLP localized slugs are handled separately).

it-IT
image

pt-BR
image

Problem

FastStore stores with localization enabled had three major issues on the PDP:

  1. Localized product URLs (e.g. /it-IT/adidas-polo-uomo-65/p) returned 404 because the Query.product resolver's startsWith guard rejected slugs whose prefix didn't match the Intelligent Search linkText (always in the default locale).
  2. Breadcrumb items were rendered as non-clickable <span> elements and BreadcrumbJsonLd was suppressed entirely.
  3. The LocalizationSelector had no localized product URL to navigate to when the shopper switched locale.
  4. After landing on a 404 from a locale switch, the LocalizationSelector lost the product context and couldn't recover the correct localized URL — and the 404 page showed the bare asPath instead of the full locale-prefixed URL.

Changes

@faststore/api

  • New clients/catalog/ client — uses the public catalog-dataplane/product/{id} endpoint (Accept-Language header, no AppKey/AppToken required) to fetch localized product data (linkId, category.fullPathUriName) per locale
  • Query.product resolver: when startsWith fails and localization is enabled, validates the slug prefix against the - - Catalog Dataplane linkId for the current locale before rejecting
  • New StoreProduct.otherLocales: [StoreProductLocale!] field — fires Promise.all over all configured locales to build localized slug entries; zero overhead for non-localized stores
  • StoreProduct.breadcrumbList resolver: now async; uses category.fullPathUriName from the Catalog Dataplane for localized category URL paths and linkId for the product URL; falls back to slugify(IS name) when no translation exists
  • productTranslationsCache in GraphqlContext.storage — request-scoped cache keyed by productId:locale shared across all three resolvers to avoid duplicate API calls

@faststore/core

  • New LocalizedProductContext — React context that provides otherLocales from the PDP GraphQL response to - any component in the tree (including global components like LocalizationButton)
  • useLocalizedProduct_unstable exported from experimental/index.ts for store customization
  • LocalizationButton / useBindingSelector: uses otherLocales to redirect to the correct localized product URL on locale switch; falls back to the defaultLocale slug when no translation exists for the target locale
  • useBindingSelector: persists otherLocales to sessionStorage keyed by SKU id (fs:otherLocales:{skuId}) so the locale selector can recover the correct product URL even after the user lands on a 404 (where the page no longer provides otherLocales)
  • Breadcrumb.tsx: removed localization.enabled workaround — always renders now that localized URLs are available
    -p.tsx: restored BreadcrumbJsonLd for all locales + added hreflang alternate tags via NextSeo.additionalLinkTags (including x-default pointing to the root store URL)
  • EmptyState: the fromUrl shown on the 404 page now includes the active Next.js locale prefix (e.g. /it-IT/...) so shoppers see the full URL they attempted to visit

Architecture note

The Catalog Dataplane is called once per product+locale combination. For otherLocales (all locales) and breadcrumbList (current locale only), results are shared via productTranslationsCache. A future optimization — replacing the Promise.all with a single "all-locales" endpoint from the Catalog team — would be a contained 2-file change with no behavior impact.

The sessionStorage persistence in useBindingSelector uses a fs:otherLocales:{skuId} key and is intentionally session-scoped (cleared on tab close). It is a best-effort fallback; the locale selector always prefers live otherLocales from context when available.

Test plan

try using brandless acc

  • Visit a localized PDP (e.g. /it-IT/{localized-slug}-{skuId}/p) — page renders correctly
    eg. /adidas-mens-performance-polo-blast-blue-65/p -> switch to it-IT locale! You will be able to see the slug localized.
  • Visit a non-localized locale PDP (e.g. /pt-BR/{default-slug}-{skuId}/p) — page renders correctly (fallback path)
    eg. select en-CA
  • PDP breadcrumb items are clickable <Link> elements with localized category URLs
  • 2-level category hierarchy breadcrumb builds correct intermediate paths
  • LocalizationSelector navigates to the correct localized product URL for each locale
  • LocalizationSelector falls back to the default locale slug for locales without a translation
  • hreflang tags appear in <head> for all configured locales + x-default (lists only the availableLinkIds)
Screenshot 2026-06-11 at 16 33 23 Screenshot 2026-06-11 at 16 37 36
  • BreadcrumbJsonLd appears in page source for all locales
  • Non-localized stores (localization.enabled: false) — no regression, no extra API calls
  • Switch locale on a PDP → land on a 404 → LocalizationSelector still shows/navigates to correct localized URLs for all locales
  • 404 page fromUrl displays the full locale-prefixed path (e.g. /it-IT/some-product/p)

Spec file for more information

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Product pages now expose otherLocales via a dedicated localization context/provider.
    • Product pages generate multi-locale SEO hreflang alternate links.
    • Added localized VTEX catalog support to fetch locale-specific product/category details.
  • Improvements

    • Breadcrumbs now use localized category/product path segments when available and consistently render as links.
    • Locale-aware product slug validation and PDP redirects that use cached or persisted otherLocales.
    • Added request-scoped caching for localized product lookups.
  • Tests

    • Expanded coverage for PDP locale parsing plus otherLocales persistence/recovery behavior.

@hellofanny hellofanny requested a review from a team as a code owner May 27, 2026 02:40
@hellofanny hellofanny requested review from lariciamota and renatomaurovtex and removed request for a team May 27, 2026 02:40
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds VTEX Catalog dataplane client with localized typings and getLocalizedProduct, request-scoped translations cache, GraphQL otherLocales and locale-aware breadcrumb/slug validation, frontend LocalizedProductContext/provider, hreflang injection on product pages, UI/hook changes for PDP-aware locale routing, and tests.

Changes

Multi-locale Product Support

Layer / File(s) Summary
Catalog dataplane client and request cache
packages/api/src/platforms/vtex/clients/catalog/index.ts, packages/api/src/platforms/vtex/clients/index.ts, packages/api/src/platforms/vtex/index.ts
Adds LocalizedCategoryEntry/LocalizedProduct* typings; exports CatalogDataplane with getLocalizedProduct(productId, locale) using Accept-Language; wires client into getClients; adds storage.productTranslationsCache: Map<string, LocalizedProductEntry> for request-scoped caching keyed by productId:locale.
GraphQL schema and product resolvers
packages/api/src/platforms/vtex/typeDefs/product.graphql, packages/api/src/platforms/vtex/resolvers/product.ts, packages/api/src/platforms/vtex/resolvers/query.ts
Adds StoreProduct.otherLocales and StoreProductLocale types; refactors breadcrumbList to async and uses localized category fullPathUriName when available; adds otherLocales resolver returning locale-specific slugs from availableLinkIds; updates Query.product with isLocalizedSlugMatch helper to validate localized slug prefixes via catalog client/cache.
Frontend React context for localized products
packages/core/src/sdk/localization/LocalizedProductContext.tsx, packages/core/src/experimental/index.ts
Introduces LocalizedProductContext, LocalizedProductProvider, and useLocalizedProduct() to supply otherLocales to the component tree; re-exports them with _unstable suffixes as experimental APIs.
Product page SEO and provider integration
packages/core/src/pages/[slug]/p.tsx
Wraps page rendering in LocalizedProductProvider, extends the product query for otherLocales { locale, slug }, computes hreflang alternate links (including x-default), and injects them into NextSeo via additionalLinkTags.
Component updates for locale-aware rendering and switching
packages/core/src/components/ui/Breadcrumb/Breadcrumb.tsx, packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx, packages/core/src/sdk/localization/useBindingSelector.ts, packages/core/src/components/sections/EmptyState/EmptyState.tsx
Breadcrumb always renders Link (removed localization fallback span). LocalizationButton reads otherLocales from context and passes to useBindingSelector(otherLocales). useBindingSelector accepts optional otherLocales, persists/recovers via sessionStorage keyed by SKU id, and save() builds PDP-aware localized product URLs with locale-specific slug selection and fallback. EmptyState prefixes asPath with /${locale} from router when available.
Tests for localization helpers
packages/core/test/sdk/localization/useBindingSelector.test.tsx
Adds tests for getSkuIdFromPdpPath, persistOtherLocales, and recoverOtherLocales using window/sessionStorage stubs and Vitest utilities; covers SKU extraction across URL patterns and round-trip persistence scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PDP_Page
  participant GraphQL_Server
  participant Catalog_Dataplane
  participant Request_Cache
  participant LocalizedProductContext
  participant LocalizationButton
  User->>PDP_Page: request product page
  PDP_Page->>GraphQL_Server: query product + otherLocales
  GraphQL_Server->>Request_Cache: lookup productId:locale
  alt cache miss
    GraphQL_Server->>Catalog_Dataplane: getLocalizedProduct(productId, locale)
    Catalog_Dataplane-->>GraphQL_Server: LocalizedProductResponse
    GraphQL_Server->>Request_Cache: store productId:locale ⟹ entry
  else cache hit
    Request_Cache-->>GraphQL_Server: return entry
  end
  GraphQL_Server-->>PDP_Page: product + otherLocales
  PDP_Page->>LocalizedProductContext: provide otherLocales
  User->>LocalizationButton: open locale selector
  LocalizationButton->>LocalizedProductContext: read otherLocales
  LocalizationButton->>User: redirect to /binding-url/{localized-slug}/p
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • vtex/faststore#3289: Breadcrumb localization link rendering changes overlap this PR's removal of the storeConfig workaround.
  • vtex/faststore#3176: Localization hook logic in useBindingSelector.ts intersects with the new otherLocales/SKU-id persistence behavior.
  • vtex/faststore#3207: Product page locale-aware fetching and otherLocales query extension overlap in SEO/provider integration.

Suggested reviewers

  • lariciamota
  • renatomaurovtex
  • lemagnetic

Catalog speaks in tongues, slugs find their rails,
Breadcrumbs keep clicking where locales prevail,
Provider wraps pages, caches learn fast,
Hreflang waves sail, links point at last,
Switch a language — the PDP lands at last. 🌍✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the primary changes: adding localized product URLs, breadcrumb improvements, and hreflang support for product detail pages.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/localized-product-links

Comment @coderabbitai help to get the list of available commands and usage tips.

@hellofanny hellofanny marked this pull request as draft May 27, 2026 02:41
@codesandbox-ci

codesandbox-ci Bot commented May 27, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (4)
packages/api/src/platforms/vtex/resolvers/query.ts (1)

101-129: ⚖️ Poor tradeoff

Consider extracting shared cache lookup/populate logic.

The cache lookup and populate pattern (lines 110-121) is duplicated across query.ts, breadcrumbList, and otherLocales resolvers. A shared helper would reduce duplication and ensure consistent behavior.

♻️ Proposed helper signature
// In a shared utility file
async function getLocalizedProductEntry(
  ctx: GraphqlContext,
  productId: string,
  locale: string
): Promise<LocalizedProductEntry | null> {
  const cacheKey = `${productId}:${locale}`
  let entry = ctx.storage.productTranslationsCache?.get(cacheKey)

  if (!entry) {
    try {
      const result = await ctx.clients.catalog.getLocalizedProduct(productId, locale)
      entry = { linkId: result.linkId, category: result.category }
      ctx.storage.productTranslationsCache ??= new Map()
      ctx.storage.productTranslationsCache.set(cacheKey, entry)
    } catch {
      return null
    }
  }

  return entry
}

This is lower priority since the current implementation works correctly and the duplication is contained within the localization feature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/resolvers/query.ts` around lines 101 - 129,
Extract the duplicated cache lookup/populate logic into a shared helper (e.g.,
getLocalizedProductEntry) and replace the inline blocks in query.ts (the block
around productGroupID/slugPrefix), breadcrumbList resolver, and otherLocales
resolver to call it; the helper should accept (ctx, productId, locale), compute
cacheKey `${productId}:${locale}`, read ctx.storage.productTranslationsCache,
call ctx.clients.catalog.getLocalizedProduct(productId, locale) inside a
try/catch to build and store { linkId, category } on miss, return the entry or
null on error, and each caller should use the returned entry (null-safe) instead
of duplicating the logic.
packages/api/src/platforms/vtex/resolvers/product.ts (2)

97-99: ⚡ Quick win

Extract duplicated localization check into a helper.

The pattern (ctx.discoveryConfig as any)?.localization?.enabled === true appears twice. Extract to improve readability and type safety.

♻️ Proposed helper

Add a helper function at the top of the file or in a shared utility:

const isLocalizationEnabled = (ctx: GraphqlContext): boolean =>
  (ctx.discoveryConfig as { localization?: { enabled?: boolean } })?.localization?.enabled === true

const getConfiguredLocales = (ctx: GraphqlContext): string[] =>
  Object.keys(
    (ctx.discoveryConfig as { localization?: { locales?: Record<string, unknown> } })?.localization?.locales ?? {}
  )

Then use in both resolvers:

-    const isLocalizationEnabled =
-      (ctx.discoveryConfig as any)?.localization?.enabled === true
+    const localizationEnabled = isLocalizationEnabled(ctx)

This also narrows the as any assertions to more specific types.

Also applies to: 248-249

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/resolvers/product.ts` around lines 97 - 99,
Extract the duplicated localization check into a typed helper and replace the
inline checks: add a helper like isLocalizationEnabled(ctx: GraphqlContext):
boolean that returns (ctx.discoveryConfig as { localization?: { enabled?:
boolean } })?.localization?.enabled === true and a helper
getConfiguredLocales(ctx: GraphqlContext): string[] that returns
Object.keys((ctx.discoveryConfig as { localization?: { locales?: Record<string,
unknown> } })?.localization?.locales ?? {}); then replace the two inline
occurrences of (ctx.discoveryConfig as any)?.localization?.enabled === true and
any direct locale-keys access with calls to isLocalizationEnabled(ctx) and
getConfiguredLocales(ctx) in the product resolver (and the other occurrence
noted) to remove as any and improve readability/type-safety.

262-283: ⚖️ Poor tradeoff

Concurrent API calls scale with locale count.

Promise.all fires one API call per configured locale simultaneously. For stores with many locales, this could cause rate limiting or latency spikes. The architecture note mentions a future all-locales endpoint optimization which would address this.

Consider adding a note or TODO comment to track this optimization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/resolvers/product.ts` around lines 262 - 283,
The Promise.all over configuredLocales launches one
ctx.clients.catalog.getLocalizedProduct(productId, locale) call per locale which
can cause rate limiting/latency spikes for many locales; add a clear TODO
comment above the mapped Promise.all (referencing configuredLocales,
getLocalizedProduct, productTranslationsCache and productId) noting to replace
the per-locale calls with a single all-locales endpoint or batched/rate-limited
requests in a future optimization and include expected behavior and ticket/issue
ID if available.
packages/api/src/platforms/vtex/clients/catalog/index.ts (1)

35-48: ⚡ Quick win

URL-encode productId to prevent malformed URLs.

If productId contains unexpected characters, the URL could break or behave unexpectedly. Encoding ensures robustness.

♻️ Proposed fix
     getLocalizedProduct: (
       productId: string,
       locale: string
     ): Promise<LocalizedProductResponse> =>
       fetchAPI(
-        `${base}/api/catalog-dataplane/product/${productId}?an=${account}`,
+        `${base}/api/catalog-dataplane/product/${encodeURIComponent(productId)}?an=${encodeURIComponent(account)}`,
         {
           method: 'GET',
           headers: {
             'Accept-Language': locale,
-            'Content-Type': 'application/json',
           },
         }
       ),

Also removed Content-Type header since GET requests have no body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/clients/catalog/index.ts` around lines 35 -
48, The getLocalizedProduct implementation should URL-encode the productId
before interpolating into the fetch URL and omit the unnecessary 'Content-Type'
header for the GET request; update the call that constructs
`${base}/api/catalog-dataplane/product/${productId}?an=${account}` to encode
productId (e.g., via encodeURIComponent) and remove 'Content-Type' from the
headers passed to fetchAPI so only 'Accept-Language' is sent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/platforms/vtex/clients/catalog/index.ts`:
- Around line 24-28: The CatalogDataplane function references the Options type
but it isn't imported, causing TypeScript to fail; add a type-only import for
Options (e.g., add "import type { Options } from
'<module-that-exports-Options>';" at the top of the file) so the signature
"Pick<Options, 'account' | 'environment'>" is valid, ensure you point the import
to the module that actually exports Options and run the TypeScript build to
confirm compilation succeeds.

In `@packages/core/src/pages/`[slug]/p.tsx:
- Around line 153-161: The x-default hreflang currently uses the global
baseStoreUrl/storeConfig value which can point to the wrong host for
multi-binding setups; update the href when pushing into hreflangLinks to use the
default locale's binding URL from defaultEntry (e.g., use the defaultEntry's
binding/storeUrl field) instead of baseStoreUrl, so the x-default points to the
actual URL bound to defaultLocale; change the href construction in the
hreflangLinks.push call that references defaultEntry to use that defaultEntry
binding URL plus `/${defaultEntry.slug}/p`.

In `@packages/core/src/sdk/localization/useBindingSelector.ts`:
- Around line 203-211: The current redirect preserves the PDP slug when no
localized `entry` exists, which can produce invalid product URLs in the target
binding; update the redirect logic in useBindingSelector (the block handling
`entry` and the fallback that calls `buildRedirectUrl`) so that when `entry` is
undefined and the current path matches a PDP pattern (e.g., contains a product
slug or `/p` segment), you strip the PDP path and redirect to the binding's base
URL (preserving only search and hash) instead of preserving the full page path;
adjust the call sites around `entry`, `binding`, `baseUrl`, and
`buildRedirectUrl` to detect PDP paths and use the binding root URL
(`binding.url` normalized) plus `window.location.search`/`hash` for the
redirect.

---

Nitpick comments:
In `@packages/api/src/platforms/vtex/clients/catalog/index.ts`:
- Around line 35-48: The getLocalizedProduct implementation should URL-encode
the productId before interpolating into the fetch URL and omit the unnecessary
'Content-Type' header for the GET request; update the call that constructs
`${base}/api/catalog-dataplane/product/${productId}?an=${account}` to encode
productId (e.g., via encodeURIComponent) and remove 'Content-Type' from the
headers passed to fetchAPI so only 'Accept-Language' is sent.

In `@packages/api/src/platforms/vtex/resolvers/product.ts`:
- Around line 97-99: Extract the duplicated localization check into a typed
helper and replace the inline checks: add a helper like
isLocalizationEnabled(ctx: GraphqlContext): boolean that returns
(ctx.discoveryConfig as { localization?: { enabled?: boolean }
})?.localization?.enabled === true and a helper getConfiguredLocales(ctx:
GraphqlContext): string[] that returns Object.keys((ctx.discoveryConfig as {
localization?: { locales?: Record<string, unknown> } })?.localization?.locales
?? {}); then replace the two inline occurrences of (ctx.discoveryConfig as
any)?.localization?.enabled === true and any direct locale-keys access with
calls to isLocalizationEnabled(ctx) and getConfiguredLocales(ctx) in the product
resolver (and the other occurrence noted) to remove as any and improve
readability/type-safety.
- Around line 262-283: The Promise.all over configuredLocales launches one
ctx.clients.catalog.getLocalizedProduct(productId, locale) call per locale which
can cause rate limiting/latency spikes for many locales; add a clear TODO
comment above the mapped Promise.all (referencing configuredLocales,
getLocalizedProduct, productTranslationsCache and productId) noting to replace
the per-locale calls with a single all-locales endpoint or batched/rate-limited
requests in a future optimization and include expected behavior and ticket/issue
ID if available.

In `@packages/api/src/platforms/vtex/resolvers/query.ts`:
- Around line 101-129: Extract the duplicated cache lookup/populate logic into a
shared helper (e.g., getLocalizedProductEntry) and replace the inline blocks in
query.ts (the block around productGroupID/slugPrefix), breadcrumbList resolver,
and otherLocales resolver to call it; the helper should accept (ctx, productId,
locale), compute cacheKey `${productId}:${locale}`, read
ctx.storage.productTranslationsCache, call
ctx.clients.catalog.getLocalizedProduct(productId, locale) inside a try/catch to
build and store { linkId, category } on miss, return the entry or null on error,
and each caller should use the returned entry (null-safe) instead of duplicating
the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 384c81e1-2f4a-4063-8f0e-b634b7537049

📥 Commits

Reviewing files that changed from the base of the PR and between 2edbcfd and 04c2703.

📒 Files selected for processing (12)
  • packages/api/src/platforms/vtex/clients/catalog/index.ts
  • packages/api/src/platforms/vtex/clients/index.ts
  • packages/api/src/platforms/vtex/index.ts
  • packages/api/src/platforms/vtex/resolvers/product.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/api/src/platforms/vtex/typeDefs/product.graphql
  • packages/core/src/components/ui/Breadcrumb/Breadcrumb.tsx
  • packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx
  • packages/core/src/experimental/index.ts
  • packages/core/src/pages/[slug]/p.tsx
  • packages/core/src/sdk/localization/LocalizedProductContext.tsx
  • packages/core/src/sdk/localization/useBindingSelector.ts

Comment thread packages/api/src/platforms/vtex/clients/catalog/index.ts Outdated
Comment thread packages/core/src/pages/[slug]/p.tsx Outdated
Comment thread packages/core/src/sdk/localization/useBindingSelector.ts
@hellofanny hellofanny changed the title feat(i18n): localized product URLs, breadcrumbs and hreflang for PDP feat(core): localized product URLs, breadcrumbs and hreflang for PDP May 28, 2026
@pkg-pr-new

pkg-pr-new Bot commented May 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@faststore/api

npm i https://pkg.pr.new/vtex/faststore/@faststore/api@1775602

@faststore/cli

npm i https://pkg.pr.new/vtex/faststore/@faststore/cli@1775602

@faststore/components

npm i https://pkg.pr.new/vtex/faststore/@faststore/components@1775602

@faststore/core

npm i https://pkg.pr.new/vtex/faststore/@faststore/core@1775602

@faststore/diagnostics

npm i https://pkg.pr.new/vtex/faststore/@faststore/diagnostics@1775602

@faststore/lighthouse

npm i https://pkg.pr.new/vtex/faststore/@faststore/lighthouse@1775602

@faststore/sdk

npm i https://pkg.pr.new/vtex/faststore/@faststore/sdk@1775602

@faststore/ui

npm i https://pkg.pr.new/vtex/faststore/@faststore/ui@1775602

commit: 1775602

Comment thread packages/api/src/platforms/vtex/resolvers/product.ts Outdated
@hellofanny hellofanny marked this pull request as ready for review May 28, 2026 17:37

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/api/src/platforms/vtex/clients/catalog/index.ts (1)

32-32: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Unresolved TS type import blocker for Options

Line 32 references Options, but this file does not import that type, so this still breaks TypeScript compilation.

Suggested fix
 import { fetchAPI } from '../fetch'
+import type { Options } from '../..'
#!/bin/bash
# Verify whether Options is imported in this file and where it is exported.
set -euo pipefail

echo "1) Check usage/import in target file"
rg -n -C2 '\bOptions\b|import type \{ Options \}' packages/api/src/platforms/vtex/clients/catalog/index.ts

echo
echo "2) Locate exported Options type under VTEX platform module"
rg -n -C2 'export (type|interface) Options\b' packages/api/src/platforms/vtex

echo
echo "3) Confirm catalog client currently lacks Options import"
rg -n -C2 '^import' packages/api/src/platforms/vtex/clients/catalog/index.ts

As per coding guidelines, “TypeScript files: Ensure type safety and avoid type assertions when possible”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/clients/catalog/index.ts` at line 32, The
function signature for CatalogDataplane references the missing TypeScript type
Options but this file lacks an import; locate the module that exports the VTEX
Options type (search for "export type Options" or "export interface Options"
under the vtex platform) and add an explicit type-only import (import type {
Options } from '...') at the top of this file so CatalogDataplane({ account,
environment }: Options) compiles correctly; ensure the import uses the exact
exported name and update any tooling comments if necessary.
🧹 Nitpick comments (1)
packages/api/src/platforms/vtex/resolvers/product.ts (1)

129-135: ⚡ Quick win

Use a Set for category ID membership in breadcrumb filtering

Line 134 currently does repeated includes lookups on an array. Switching mainTreeIds to a Set is cleaner and faster in resolver hot paths.

Proposed refactor
-        const mainTreeIds = removeTrailingSlashes(categoriesIds[mainTreeIndex])
+        const mainTreeIds = new Set(
+          removeTrailingSlashes(categoriesIds[mainTreeIndex])
           .split('/')
           .filter(Boolean)
+        )

         const localizedCategories = entry.categories
-          .filter((category) => mainTreeIds.includes(category.id.toString()))
+          .filter((category) => mainTreeIds.has(category.id.toString()))

As per coding guidelines, “packages/api/src/**: Consider performance implications of resolvers”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/platforms/vtex/resolvers/product.ts` around lines 129 - 135,
Replace the array membership checks on mainTreeIds with a Set to avoid repeated
O(n) includes in the breadcrumb filter: after computing mainTreeIds (from
removeTrailingSlashes(...).split('/').filter(Boolean)) create a new Set (e.g.
mainTreeIdSet) and then use mainTreeIdSet.has(category.id.toString()) inside the
localizedCategories filter over entry.categories; update references to
mainTreeIds in this resolver (the localizedCategories filter and any subsequent
uses) to use the Set for faster lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/api/src/platforms/vtex/clients/catalog/index.ts`:
- Line 32: The function signature for CatalogDataplane references the missing
TypeScript type Options but this file lacks an import; locate the module that
exports the VTEX Options type (search for "export type Options" or "export
interface Options" under the vtex platform) and add an explicit type-only import
(import type { Options } from '...') at the top of this file so
CatalogDataplane({ account, environment }: Options) compiles correctly; ensure
the import uses the exact exported name and update any tooling comments if
necessary.

---

Nitpick comments:
In `@packages/api/src/platforms/vtex/resolvers/product.ts`:
- Around line 129-135: Replace the array membership checks on mainTreeIds with a
Set to avoid repeated O(n) includes in the breadcrumb filter: after computing
mainTreeIds (from removeTrailingSlashes(...).split('/').filter(Boolean)) create
a new Set (e.g. mainTreeIdSet) and then use
mainTreeIdSet.has(category.id.toString()) inside the localizedCategories filter
over entry.categories; update references to mainTreeIds in this resolver (the
localizedCategories filter and any subsequent uses) to use the Set for faster
lookups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28fbec94-8dbb-4cf4-b096-1d134824d741

📥 Commits

Reviewing files that changed from the base of the PR and between 04c2703 and 618aec5.

📒 Files selected for processing (4)
  • packages/api/src/platforms/vtex/clients/catalog/index.ts
  • packages/api/src/platforms/vtex/resolvers/product.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/core/src/sdk/localization/useBindingSelector.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/api/src/platforms/vtex/resolvers/query.ts

@hellofanny hellofanny marked this pull request as draft June 1, 2026 19:15
@hellofanny hellofanny force-pushed the feat/localized-product-links branch from 618aec5 to f2eee11 Compare June 11, 2026 13:48
@hellofanny hellofanny marked this pull request as ready for review June 11, 2026 13:49
@hellofanny hellofanny self-assigned this Jun 11, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/platforms/vtex/resolvers/product.ts`:
- Around line 292-307: The resolver forwards ctx.storage.locale directly to
ctx.clients.catalog.getLocalizedProduct (used in the otherLocales flow), which
can be missing/invalid and cause a catch returning null; before calling
getLocalizedProduct validate/guard locale (use (ctx.storage.locale) &&
isSupportedLocale(...) or fallback to
(ctx.discoveryConfig?.localization?.defaultLocale)) and only call
getLocalizedProduct when the resolved locale is truthy/valid; ensure the
cacheKey creation and ctx.storage.productTranslationsCache access use the
sanitized locale value so you don't populate the cache with invalid keys and
preserve alternates for PDP responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 06167fb8-8a61-43f1-b398-c5330a16dc60

📥 Commits

Reviewing files that changed from the base of the PR and between 618aec5 and f2eee11.

⛔ Files ignored due to path filters (1)
  • packages/api/src/__generated__/schema.ts is excluded by !**/__generated__/** and included by packages/**
📒 Files selected for processing (14)
  • packages/api/src/platforms/vtex/clients/catalog/index.ts
  • packages/api/src/platforms/vtex/clients/index.ts
  • packages/api/src/platforms/vtex/index.ts
  • packages/api/src/platforms/vtex/resolvers/product.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/api/src/platforms/vtex/typeDefs/product.graphql
  • packages/core/src/components/sections/EmptyState/EmptyState.tsx
  • packages/core/src/components/ui/Breadcrumb/Breadcrumb.tsx
  • packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx
  • packages/core/src/experimental/index.ts
  • packages/core/src/pages/[slug]/p.tsx
  • packages/core/src/sdk/localization/LocalizedProductContext.tsx
  • packages/core/src/sdk/localization/useBindingSelector.ts
  • packages/core/test/sdk/localization/useBindingSelector.test.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/core/src/experimental/index.ts
  • packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx
  • packages/api/src/platforms/vtex/clients/index.ts
  • packages/api/src/platforms/vtex/index.ts
  • packages/core/src/components/ui/Breadcrumb/Breadcrumb.tsx
  • packages/api/src/platforms/vtex/typeDefs/product.graphql
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/api/src/platforms/vtex/clients/catalog/index.ts
  • packages/core/src/sdk/localization/LocalizedProductContext.tsx
  • packages/core/src/pages/[slug]/p.tsx

Comment thread packages/api/src/platforms/vtex/resolvers/product.ts

@eduardoformiga eduardoformiga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

wow. Good job! Some complex logics.
I left some comments along with the review 😉

  • I miss the generated (gql/graphql) files from the core package in the PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

💯

Comment thread packages/api/src/platforms/vtex/clients/catalog/index.ts Outdated
Comment thread packages/api/src/platforms/vtex/clients/catalog/index.ts
Comment thread packages/api/src/platforms/vtex/resolvers/product.ts Outdated
Comment thread packages/api/src/platforms/vtex/resolvers/query.ts
Comment on lines +97 to +166
const isLocalizationEnabled =
(ctx.discoveryConfig as any)?.localization?.enabled === true
const locale = ctx.storage.locale

if (isLocalizationEnabled && locale) {
// productTranslationsCache is request-scoped and shared with the slug and otherLocales
// resolvers — if any of them already called getLocalizedProduct for this product+locale,
// we reuse the result here at zero extra cost.
const cacheKey = `${productId}:${locale}`
let entry = ctx.storage.productTranslationsCache?.get(cacheKey)

if (!entry) {
try {
const result = await ctx.clients.catalog.getLocalizedProduct(
productId,
locale
)
// Store both linkId (for the product item URL) and the full categories array
// (for per-level localized slugs). We intentionally keep categories[] rather than
// just the leaf category so we never need to reconstruct the hierarchy via split('/').
entry = {
linkId: result.linkId,
categories: result.categories ?? [],
availableLinkIds: result.availableLinkIds ?? {},
}
ctx.storage.productTranslationsCache ??= new Map()
ctx.storage.productTranslationsCache.set(cacheKey, entry)
} catch {
// Catalog Dataplane API unavailable — fall through to IS-based behavior below
}
}

if (entry) {
// Extract the category IDs that belong to the main tree (same tree chosen from IS above).
// A product can be registered in multiple trees; Catalog Dataplane returns all of them
// in categories[], so we filter to only the ones matching this tree's IDs.
const mainTreeIds = removeTrailingSlashes(categoriesIds[mainTreeIndex])
.split('/')
.filter(Boolean)

const localizedCategories = entry.categories
.filter((category) => mainTreeIds.includes(category.id.toString()))
.sort(
(a, b) =>
a.fullPath.split('/').length - b.fullPath.split('/').length
)

// Length guard: if Catalog Dataplane returns fewer categories than IS expects
// (e.g. data inconsistency or empty categories), fall through to the IS fallback.
if (localizedCategories.length === splittedCategories.length) {
return {
itemListElement: [
// Category items: both name and slug come from Catalog Dataplane, ensuring
// they are always consistent with each other for the requested locale.
...localizedCategories.map((category, index) => ({
name: category.name,
item: `/${category.fullPathUriName}/`,
position: index + 1,
})),
{
name: productName,
item: getPath(entry.linkId, itemId),
position: splittedCategories.length + 1,
},
],
numberOfItems: splittedCategories.length,
}
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you think it's worth moving this block of code into a new function to keep the resolver cleaner and more explicit in its intention?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This one I think it was a little bit tricker and confusing while extracting it, likely reduce readability. (AI said is better to keep this way hahah)

Comment thread packages/core/src/pages/[slug]/p.tsx
@hellofanny hellofanny changed the base branch from dev to feat/localized-slugs-links June 17, 2026 02:57
@hellofanny hellofanny added the enhancement New feature or request label Jun 17, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/pages/[slug]/p.tsx (1)

47-52: ⚠️ Potential issue | 🟡 Minor

Remove unsafe type assertions on localization config.

The as Record<string, any> and as | string | undefined casts drop type information from the SEO helper. Extend StoreConfig to include the localization shape so locales and defaultLocale flow with proper types.

♻️ Type the localization config
 type StoreConfig = typeof storeConfig & {
   experimental: {
     revalidate?: number
     enableClientOffer?: boolean
   }
+  localization?: {
+    enabled?: boolean
+    defaultLocale?: string
+    locales?: Record<
+      string,
+      { bindings?: Array<{ url?: string | null }> }
+    >
+  }
 }
@@
-  const locales = storeConfig.localization.locales as Record<string, any>
+  const locales = storeConfig.localization.locales
   const baseStoreUrl = storeConfig.storeUrl.replace(/\/$/, '')
-  const defaultLocale = storeConfig.localization.defaultLocale as
-    | string
-    | undefined
+  const defaultLocale = storeConfig.localization.defaultLocale

Applies to lines 96–100 where the casts are used. Per coding guidelines, avoid type assertions when possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/pages/`[slug]/p.tsx around lines 47 - 52, The StoreConfig
type definition is missing type information for the localization configuration
(locales and defaultLocale fields), which forces unsafe type assertions
elsewhere in the code. Extend the StoreConfig type at its definition to include
the localization shape with properly typed locales and defaultLocale properties,
rather than leaving them untyped and requiring as Record<string, any> and as
string | undefined assertions when these fields are used on lines 96-100. This
will allow the localization config to flow through with proper type safety
without needing unsafe type casts.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/api/src/platforms/vtex/resolvers/query.ts`:
- Around line 67-73: Add validation of the `locale` parameter against
configured/supported locales before the call to
`catalog.getLocalizedProduct(productGroupID, locale)` in this query resolver.
Implement a guard that checks if the locale value is in the list of supported
locales before proceeding with the platform call. If the locale is invalid,
either return an error or fall back to a default locale. This validation should
occur before or at the beginning of the try block to prevent unsupported user
input from triggering unnecessary platform calls to the Catalog Dataplane.

---

Outside diff comments:
In `@packages/core/src/pages/`[slug]/p.tsx:
- Around line 47-52: The StoreConfig type definition is missing type information
for the localization configuration (locales and defaultLocale fields), which
forces unsafe type assertions elsewhere in the code. Extend the StoreConfig type
at its definition to include the localization shape with properly typed locales
and defaultLocale properties, rather than leaving them untyped and requiring as
Record<string, any> and as string | undefined assertions when these fields are
used on lines 96-100. This will allow the localization config to flow through
with proper type safety without needing unsafe type casts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6803b9ef-0f56-4180-94ea-294cfe6cd6e9

📥 Commits

Reviewing files that changed from the base of the PR and between f2eee11 and 7c0e3dd.

⛔ Files ignored due to path filters (2)
  • packages/core/@generated/gql.ts is excluded by !**/@generated/**, !**/@generated/** and included by packages/**
  • packages/core/@generated/graphql.ts is excluded by !**/@generated/**, !**/@generated/** and included by packages/**
📒 Files selected for processing (4)
  • packages/api/src/platforms/vtex/clients/catalog/index.ts
  • packages/api/src/platforms/vtex/resolvers/product.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/core/src/pages/[slug]/p.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/api/src/platforms/vtex/resolvers/product.ts
  • packages/api/src/platforms/vtex/clients/catalog/index.ts

Comment thread packages/api/src/platforms/vtex/resolvers/query.ts
Comment thread packages/core/src/sdk/localization/useBindingSelector.ts

@eduardoformiga eduardoformiga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

  1. After changing to the Italian version, if I focused out of the browser and then focused in, the selector went back to the other locale:
Image
  1. hreflang - maybe I misconfigure it, but only brings 1 and the
<link rel="alternate" hreflang="x-default" href="https://homebrewqa.fast.store/roshe-tenis-80/p" data-next-head="">

edit: the 2) looks good now.

@hellofanny

Copy link
Copy Markdown
Contributor Author
  1. After changing to the Italian version, if I focused out of the browser and then focused in, the selector went back to the other locale:
Image 2. hreflang - maybe I misconfigure it, but only brings 1 and the
<link rel="alternate" hreflang="x-default" href="https://homebrewqa.fast.store/roshe-tenis-80/p" data-next-head="">

edit: the 2) looks good now.

The first 1 I couldn't reproduced it. Did you tried to to remove the browser data and tried again?

@hellofanny hellofanny force-pushed the feat/localized-product-links branch 2 times, most recently from 954989a to 1775602 Compare June 19, 2026 21:05
@sonar-workflows

Copy link
Copy Markdown

Failed Quality Gate failed

  • 52.90% Coverage on New Code (is less than 80.00%)

Project ID: vtex_faststore_f0a862d5-9557-49f9-8d09-de40caa76622

View in SonarQube

@hellofanny

Copy link
Copy Markdown
Contributor Author
  1. After changing to the Italian version, if I focused out of the browser and then focused in, the selector went back to the other locale:
Image
    [
      
        <img alt="Image" height="826" src="https://private-user-images.githubusercontent.com/11325562/610138299-2a299384-a21e-4985-a0fc-92bb1d9af41b.gif?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE4ODk3MzUsIm5iZiI6MTc4MTg4OTQzNSwicGF0aCI6Ii8xMTMyNTU2Mi82MTAxMzgyOTktMmEyOTkzODQtYTIxZS00OTg1LWEwZmMtOTJiYjFkOWFmNDFiLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE5VDE3MTcxNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE2NWZmZDc0Mjk4NGMzZWU0MDIwNTMzYzgzMzZlYjA4ODVmNDZkM2RiMzdhZWE0MTU2ZTY4NTNjZDhjZjQ2YmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmdpZiJ9.c713AYdZs3CTdoCNJ-NFdifrKQ-ZXIZD6sPQuYrbpko">
      
    ](https://private-user-images.githubusercontent.com/11325562/610138299-2a299384-a21e-4985-a0fc-92bb1d9af41b.gif?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE4ODk3MzUsIm5iZiI6MTc4MTg4OTQzNSwicGF0aCI6Ii8xMTMyNTU2Mi82MTAxMzgyOTktMmEyOTkzODQtYTIxZS00OTg1LWEwZmMtOTJiYjFkOWFmNDFiLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE5VDE3MTcxNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE2NWZmZDc0Mjk4NGMzZWU0MDIwNTMzYzgzMzZlYjA4ODVmNDZkM2RiMzdhZWE0MTU2ZTY4NTNjZDhjZjQ2YmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmdpZiJ9.c713AYdZs3CTdoCNJ-NFdifrKQ-ZXIZD6sPQuYrbpko)
    
    
      
        
          
        
        
          
          
        
      
      [
        
          
        
      ](https://private-user-images.githubusercontent.com/11325562/610138299-2a299384-a21e-4985-a0fc-92bb1d9af41b.gif?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE4ODk3MzUsIm5iZiI6MTc4MTg4OTQzNSwicGF0aCI6Ii8xMTMyNTU2Mi82MTAxMzgyOTktMmEyOTkzODQtYTIxZS00OTg1LWEwZmMtOTJiYjFkOWFmNDFiLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE5VDE3MTcxNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWE2NWZmZDc0Mjk4NGMzZWU0MDIwNTMzYzgzMzZlYjA4ODVmNDZkM2RiMzdhZWE0MTU2ZTY4NTNjZDhjZjQ2YmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmdpZiJ9.c713AYdZs3CTdoCNJ-NFdifrKQ-ZXIZD6sPQuYrbpko)
  1. hreflang - maybe I misconfigure it, but only brings 1 and the
<link rel="alternate" hreflang="x-default" href="https://homebrewqa.fast.store/roshe-tenis-80/p" data-next-head="">

edit: the 2) looks good now.

The first 1 I couldn't reproduced it. Did you tried to to remove the browser data and tried again?

I believe it's an existing bug, I will treat this bug later, ok?
Since I couldn't reproduce I asked AI: Não — o PT/USD no selector não é uma regressão direta de feat/localized-product-links.
Com sessão em pt-BR numa URL it-IT, o conteúdo do produto fica em português mesmo com slug italiano — comportamento esperado com sessão errada, não bug novo da branch. - O fix continua sendo alinhar sessão com o binding da URL. task

@hellofanny hellofanny merged commit 92bc71f into feat/localized-slugs-links Jun 19, 2026
19 of 21 checks passed
@hellofanny hellofanny deleted the feat/localized-product-links branch June 19, 2026 21:19
hellofanny added a commit that referenced this pull request Jun 19, 2026
…3352)

## Summary

Implements localized product links for FastStore stores with
localization.enabled: true (PDP only — PLP localized slugs are handled
separately).

**it-IT**
<img width="1907" height="1001" alt="image"
src="https://github.com/user-attachments/assets/b78831db-edbd-4db3-96bb-0c67b4b6c20b"
/>

**pt-BR**
<img width="1918" height="992" alt="image"
src="https://github.com/user-attachments/assets/f357c607-a37d-41e2-b13a-abf25de2629d"
/>


### Problem

FastStore stores with localization enabled had three major issues on the
PDP:
1. Localized product URLs (e.g. `/it-IT/adidas-polo-uomo-65/p`) returned
404 because the `Query.product` resolver's `startsWith` guard rejected
slugs whose prefix didn't match the Intelligent Search `linkText`
(always in the default locale).
2. Breadcrumb items were rendered as non-clickable `<span>` elements and
`BreadcrumbJsonLd` was suppressed entirely.
3. The `LocalizationSelector` had no localized product URL to navigate
to when the shopper switched locale.
4. After landing on a 404 from a locale switch, the LocalizationSelector
lost the product context and couldn't recover the correct localized URL
— and the 404 page showed the bare asPath instead of the full
locale-prefixed URL.

### Changes

@faststore/api

- New clients/catalog/ client — uses the public
catalog-dataplane/product/{id} endpoint (Accept-Language header, no
AppKey/AppToken required) to fetch localized product data (linkId,
category.fullPathUriName) per locale
- Query.product resolver: when startsWith fails and localization is
enabled, validates the slug prefix against the - - Catalog Dataplane
linkId for the current locale before rejecting
- New StoreProduct.otherLocales: [StoreProductLocale!] field — fires
Promise.all over all configured locales to build localized slug entries;
zero overhead for non-localized stores
- StoreProduct.breadcrumbList resolver: now async; uses
category.fullPathUriName from the Catalog Dataplane for localized
category URL paths and linkId for the product URL; falls back to
slugify(IS name) when no translation exists
- productTranslationsCache in GraphqlContext.storage — request-scoped
cache keyed by productId:locale shared across all three resolvers to
avoid duplicate API calls

@faststore/core

- New `LocalizedProductContext` — React context that provides
otherLocales from the PDP GraphQL response to - any component in the
tree (including global components like LocalizationButton)
- `useLocalizedProduct_unstable` exported from experimental/index.ts for
store customization
- `LocalizationButton` / `useBindingSelector`: uses otherLocales to
redirect to the correct localized product URL on locale switch; falls
back to the defaultLocale slug when no translation exists for the target
locale
- `useBindingSelector`: persists otherLocales to sessionStorage keyed by
SKU id (fs:otherLocales:{skuId}) so the locale selector can recover the
correct product URL even after the user lands on a 404 (where the page
no longer provides otherLocales)
- `Breadcrumb.tsx`: removed localization.enabled workaround — always
renders <Link> now that localized URLs are available
-`p.tsx`: restored BreadcrumbJsonLd for all locales + added hreflang
alternate tags via NextSeo.additionalLinkTags (including x-default
pointing to the root store URL)
- `EmptyState`: the fromUrl shown on the 404 page now includes the
active Next.js locale prefix (e.g. /it-IT/...) so shoppers see the full
URL they attempted to visit

### Architecture note

The Catalog Dataplane is called once per product+locale combination. For
otherLocales (all locales) and breadcrumbList (current locale only),
results are shared via productTranslationsCache. A future optimization —
replacing the Promise.all with a single "all-locales" endpoint from the
Catalog team — would be a contained 2-file change with no behavior
impact.

The sessionStorage persistence in `useBindingSelector` uses a
fs:otherLocales:{skuId} key and is intentionally session-scoped (cleared
on tab close). It is a best-effort fallback; the locale selector always
prefers live otherLocales from context when available.

## Test plan

try using `brandless` acc

- [ ] Visit a localized PDP (e.g. `/it-IT/{localized-slug}-{skuId}/p`) —
page renders correctly
eg. /adidas-mens-performance-polo-blast-blue-65/p -> switch to it-IT
locale! You will be able to see the slug localized.
- [ ] Visit a non-localized locale PDP (e.g.
`/pt-BR/{default-slug}-{skuId}/p`) — page renders correctly (fallback
path)
eg. select en-CA
- [ ] PDP breadcrumb items are clickable `<Link>` elements with
localized category URLs
- [ ] 2-level category hierarchy breadcrumb builds correct intermediate
paths
- [ ] `LocalizationSelector` navigates to the correct localized product
URL for each locale
- [ ] `LocalizationSelector` falls back to the default locale slug for
locales without a translation
- [ ] `hreflang` tags appear in `<head>` for all configured locales +
`x-default` (lists only the `availableLinkIds`)
<img width="1086" height="181" alt="Screenshot 2026-06-11 at 16 33 23"
src="https://github.com/user-attachments/assets/e1dc0ed0-3332-492b-9a1d-84a33c3a1eb4"
/>

<img width="498" height="178" alt="Screenshot 2026-06-11 at 16 37 36"
src="https://github.com/user-attachments/assets/6f52bb39-9211-4ece-8462-14e0841d6115"
/>

- [ ] `BreadcrumbJsonLd` appears in page source for all locales
- [ ] Non-localized stores (`localization.enabled: false`) — no
regression, no extra API calls
- [ ] Switch locale on a PDP → land on a 404 → LocalizationSelector
still shows/navigates to correct localized URLs for all locales
- [ ] 404 page fromUrl displays the full locale-prefixed path (e.g.
/it-IT/some-product/p)


[Spec file for more
information](vtex/faststore-dx-spec-kit#15)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Product pages now expose `otherLocales` via a dedicated localization
context/provider.
  * Product pages generate multi-locale SEO `hreflang` alternate links.
* Added localized VTEX catalog support to fetch locale-specific
product/category details.

* **Improvements**
* Breadcrumbs now use localized category/product path segments when
available and consistently render as links.
* Locale-aware product slug validation and PDP redirects that use cached
or persisted `otherLocales`.
  * Added request-scoped caching for localized product lookups.

* **Tests**
* Expanded coverage for PDP locale parsing plus `otherLocales`
persistence/recovery behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants