Skip to content

feat: replace pagetype with by-linkid cascade + localized PLP/collection pages#3401

Open
hellofanny wants to merge 29 commits into
feat/localized-slugs-linksfrom
feat/localized-collection-pages
Open

feat: replace pagetype with by-linkid cascade + localized PLP/collection pages#3401
hellofanny wants to merge 29 commits into
feat/localized-slugs-linksfrom
feat/localized-collection-pages

Conversation

@hellofanny

@hellofanny hellofanny commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces pagetype with by-linkid cascade for all stores in Query.collection and collectionLoader. The cascade tries category/by-linkidbrand/by-linkidcollection/by-linkid in sequence; 404 advances to the next step, 5xx is a hard failure. pagetype is retired on this code path.
  • Adds StoreCollection.otherLocales — a new lazy GraphQL field built from availableLinkIds in the by-linkid response, enabling the LocalizationSelector to navigate to the same collection in another locale.
  • Adds StoreProduct.otherLocales and uses the Catalog Dataplane API to resolve localized product slugs and breadcrumbs for PDP pages (localization.enabled stores only).
  • Slug normalization: all slugs are lowercased before by-linkid calls, preserving parity with pagetype's case-insensitive behavior (merchants can register mixed-case linkId values via the Catalog multilanguage API).
  • Canonical IS facets: StoreCollection.meta for categories now fetches the category tree to derive canonical (default-locale) slugs for selectedFacets, since category/by-linkid echoes the queried (potentially localized) slug in linkId rather than the canonical slug.

What changed

@faststore/api

  • New catalog.byLinkId.{category,brand,collection} client methods
  • collectionLoader: replaced pagetype with the by-linkid cascade; added entityType discriminator; slug normalization (toLowerCase)
  • StoreCollection resolvers: meta (async, canonical slugs via category tree), breadcrumbList, seo, type, otherLocales (new)
  • StoreProduct resolvers: otherLocales (new), breadcrumbList (localized via Catalog Dataplane)
  • Query.product: localized slug validation via Catalog Dataplane when localization.enabled
  • Query.collection: accepts locale arg, calls mutateLocaleContext for correct SSG locale propagation
  • GraphQL schema: StoreCollectionLocale, StoreProductLocale types; otherLocales on both StoreCollection and StoreProduct
  • Search client: prefer ctx.storage.locale over vtex_segment for IS locale

@faststore/core

  • [...slug].tsx: passes locale to collection query;
  • LocalizedProductContext + useBindingSelector: page-agnostic locale switching with urlSuffix (empty for PLP, /p for PDP)
  • LocalizationButton: reads urlSuffix from context

Test plan

breadbrum-go-plp
  • Brand PLP (/adidas) — selectedFacets: [{key:"brand", value:"adidas"}]
  • Collection PLP (/computer---software) — selectedFacets: [{key:"productclusterids", value:"..."}]
  • Localized category PLP (/it-IT/Abbigliamento) — resolves, no IS 500s, canonical facets sent
  • Locale switch on PLP (IT → PT-BR) → navigates to /pt-BR/Vestuário
  • Locale switch on PLP (IT → EN) → navigates to /apparel
  • Non-existent slug → clean 404
  • PDP locale switch — otherLocales drives LocalizationSelector

Known limitations / follow-ups

  • Breadcrumb names are not localized: by-linkid name field always returns the default-locale value. Feedback filed with the Catalog team to add availableNames or Accept-Language support.
  • availableLinkIds excludes the default locale: worked around via category tree lookup. Feedback filed with Catalog team to include the default locale in the map.
  • metaTagDescription not yet in by-linkid response: resolver already reads root.metaTagDescription; will work once Catalog team ships the field. SEO description is currently empty for collection pages (pre-existing gap, not a regression from this PR).

Related

Summary by CodeRabbit

  • New Features

    • Added locale-aware collection support via an optional locale argument, including otherLocales with localized slugs.
    • Introduced VTEX catalog lookups by link ID for category, brand, and collection to power resolution.
    • Improved PDP/PLP localization navigation with locale-aware URL suffix handling.
  • Enhancements

    • Updated PLP collection routing to resolve facets and breadcrumbs from link-based category roots.
    • Improved localized product slug validation when localization is enabled.
  • Tests

    • Added unit tests for link-based catalog lookups, collection resolution cascade, and locale selection priority.

@hellofanny hellofanny requested a review from a team as a code owner June 19, 2026 15:53
@hellofanny hellofanny requested review from lemagnetic and lucasfp13 and removed request for a team June 19, 2026 15:53
@coderabbitai

coderabbitai Bot commented Jun 19, 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 byLinkId catalog lookups, rewrites collection resolution and locale-aware GraphQL plumbing, and threads localized slug and urlSuffix data through the PLP and redirect flow.

Changes

Localization and collection resolution

Layer / File(s) Summary
ByLinkId catalog client
packages/api/src/platforms/vtex/clients/commerce/types/ByLinkId.ts, packages/api/src/platforms/vtex/clients/commerce/index.ts, packages/api/test/unit/platforms/vtex/clients/commerce.test.ts
Adds byLinkId response types for category, brand, and collection, plus catalog.byLinkId methods that encode linkIds, return null on not-found errors, and are covered by tests for endpoint selection and 404 handling.
Collection loader cascade
packages/api/src/platforms/vtex/loaders/collection.ts, packages/api/test/unit/platforms/vtex/loaders/collection.test.ts
Replaces the collection loader’s page-type lookup with a normalized byLinkId cascade across category, brand, and collection, and updates the loader tests for cascade order and slug handling.
Collection schema fields
packages/api/src/platforms/vtex/typeDefs/collection.graphql, packages/api/src/platforms/vtex/typeDefs/query.graphql
Adds StoreCollection.otherLocales, introduces StoreCollectionLocale, and extends Query.collection with an optional locale argument.
Collection resolver localization
packages/api/src/platforms/vtex/resolvers/collection.ts
Switches collection roots to byLinkId shapes, adds canonical category slug lookup, updates meta and breadcrumb generation, and rebuilds otherLocales from localized linkId data.
Query resolver locale handling
packages/api/src/platforms/vtex/resolvers/query.ts
Extends product slug validation for localized matches, applies locale to collection requests, and rewrites allCollections to build byLinkId category and brand roots.
Localization context and redirects
packages/core/src/sdk/localization/LocalizedProductContext.tsx, packages/core/src/sdk/localization/useBindingSelector.ts
Adds urlSuffix to localization context data and provider props, and updates redirect construction to append the suffix and handle PDP stale-slug redirects.
Locale sources and PLP wiring
packages/api/src/platforms/vtex/clients/search/index.ts, packages/core/src/sdk/product/useLocalizedVariables.ts, packages/core/src/pages/[...slug].tsx, packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx, packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx, packages/core/src/customizations/src/fragments/ServerCollectionPage.ts, packages/api/test/unit/platforms/vtex/clients/search.test.ts
Changes locale priority in the search client, derives product locale from router state, passes locale through the PLP query and fragment, wraps the PLP in LocalizedProductProvider, and feeds urlSuffix into LocalizationButton; search client locale priority is covered by unit tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • vtex/faststore#3237: Changes useBindingSelector redirect behavior in the same area that this PR extends with urlSuffix and PDP stale-slug handling.
  • vtex/faststore#3352: Touches the same Query.product localized slug validation path and catalog.getLocalizedProduct flow.
  • vtex/faststore#3385: Modifies the VTEX product resolver and slug-handling logic adjacent to the resolver changes in this PR.

Suggested reviewers

  • lariciamota
  • renatomaurovtex
  • lucasfp13

Poem

A slug went wandering, lowercase and bright,
It found its brand and category by night.
The PLP learned locales, soft and true,
With urlSuffix stitched into the view.
Canonical paths now hum in tune,
Like breadcrumbs under a careful moon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing pagetype with a by-linkid cascade and adding localized PLP/collection pages.
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-collection-pages

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

@codesandbox-ci

codesandbox-ci Bot commented Jun 19, 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: 5

🧹 Nitpick comments (7)
packages/api/src/platforms/vtex/resolvers/collection.ts (3)

112-115: ⚖️ Poor tradeoff

Performance: category tree fetched on every category meta resolution.

commerce.catalog.category.tree(10) is called for each category-based collection's meta resolver. If the tree is large, this could add latency. The DataLoader caches collection roots, but the tree itself isn't cached request-scoped.

Consider caching the tree in ctx.storage similar to productTranslationsCache, or verify that upstream HTTP caching mitigates this.

🤖 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/collection.ts` around lines 112 -
115, The category tree is being fetched from
`commerce.catalog.category.tree(10)` on every resolution of the collection meta
resolver without request-scoped caching, which can cause performance issues for
large trees. Implement request-scoped caching for the category tree by checking
if it exists in `ctx.storage` before calling
`commerce.catalog.category.tree(10)`, and if it doesn't exist, fetch it and
store the result in `ctx.storage` for subsequent uses within the same request.
Follow the same pattern used for `productTranslationsCache` to maintain
consistency with existing caching patterns in the codebase.

191-193: ⚡ Quick win

Silent error swallowing may hide legitimate failures.

The catch block returns null for any error, including 5xx server errors or network issues. Consider logging the error for observability, or only swallowing expected errors (e.g., NotFoundError).

     } catch (err) {
+      // Log unexpected errors for debugging while gracefully degrading
+      console.warn('Failed to load collection entities for otherLocales:', err)
       return null
     }
🤖 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/collection.ts` around lines 191 -
193, The catch block in the collection resolver is silently swallowing all
errors without any logging or visibility into what went wrong. Add logging for
the caught error before returning null to improve observability, or
alternatively, make the catch block more specific by only catching expected
errors like NotFoundError and allowing other errors (such as server errors or
network issues) to propagate up. This ensures that unexpected failures are not
hidden and can be properly debugged.

157-162: ⚡ Quick win

Type safety: repeated as any casts for discoveryConfig.

Multiple (ctx.discoveryConfig as any) casts bypass TypeScript's type checking. Consider defining a proper interface for the localization config shape and using a type guard or helper function.

interface LocalizationConfig {
  enabled?: boolean
  defaultLocale?: string
  locales?: Record<string, unknown>
}

function getLocalizationConfig(ctx: GraphqlContext): LocalizationConfig | null {
  return (ctx.discoveryConfig as { localization?: LocalizationConfig })?.localization ?? null
}
🤖 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/collection.ts` around lines 157 -
162, The code contains repeated `as any` type casts for accessing
ctx.discoveryConfig properties (specifically checking isLocalizationEnabled and
accessing configuredLocales), which bypasses TypeScript type safety. Define a
proper LocalizationConfig interface with typed properties for enabled,
defaultLocale, and locales, then create a helper function (such as
getLocalizationConfig) that safely extracts and returns the typed localization
configuration from ctx.discoveryConfig. Replace all instances of the unsafe
`(ctx.discoveryConfig as any)?.localization` casts throughout this resolver with
calls to the new helper function or properly typed property access to restore
type safety.
packages/api/src/platforms/vtex/loaders/collection.ts (1)

63-63: 💤 Low value

Consider defensive handling for edge case.

The .at(-1)! assertion is technically safe since split('/') always returns at least one element, but an empty slug would produce an empty lastSegment, which then queries the API with an empty string. Consider adding an early guard if this edge case should fail fast.

+          if (!normalizedSlug) {
+            throw new NotFoundError(`Empty slug provided.`)
+          }
+
           const lastSegment = normalizedSlug.split('/').at(-1)!
🤖 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/loaders/collection.ts` at line 63, The
lastSegment variable is extracted from normalizedSlug using split('/').at(-1)!
without validating that the resulting segment is not empty, which could cause an
API query with an empty string if the slug is empty or ends with a slash. Add an
early guard check after extracting lastSegment to validate that it is not an
empty string, and return or throw an error appropriately if it is empty. This
ensures the function fails fast rather than proceeding with invalid data to the
API.
packages/api/src/platforms/vtex/resolvers/product.ts (1)

108-127: ⚡ Quick win

DRY: Extract shared cache-or-fetch logic.

The pattern for fetching and caching localized product data is duplicated between breadcrumbList (lines 108-127) and otherLocales (lines 302-318). Extract to a helper function to reduce duplication and ensure consistent behavior.

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,
        categories: result.categories ?? [],
        availableLinkIds: result.availableLinkIds ?? {},
      }
      ctx.storage.productTranslationsCache ??= new Map()
      ctx.storage.productTranslationsCache.set(cacheKey, entry)
    } catch {
      return null
    }
  }
  return entry
}

Also applies to: 302-318

🤖 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 108 - 127,
Extract the duplicated cache-or-fetch logic used in both the breadcrumbList and
otherLocales resolvers into a new helper function called
getLocalizedProductEntry that accepts ctx, productId, and locale parameters.
This function should handle cache lookup, call
ctx.clients.catalog.getLocalizedProduct, cache the result with linkId,
categories, and availableLinkIds properties, and return either the
cached/fetched entry or null on error. Replace the duplicated try-catch blocks
in both resolvers with calls to this new helper function to ensure consistent
behavior and reduce code duplication.
packages/api/src/platforms/vtex/resolvers/query.ts (1)

100-140: 💤 Low value

Complex but well-documented localized slug validation.

The logic for validating localized slugs against the Catalog Dataplane is thorough. The cache-or-fetch pattern here would also benefit from the helper extraction suggested for the product resolver.

🤖 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 100 - 140,
The cache-or-fetch pattern for retrieving localized product data is complex and
would benefit from extraction into a reusable helper function. Extract the logic
that checks ctx.storage.productTranslationsCache, retrieves or creates an entry
using catalog.getLocalizedProduct, and sets the cache into a separate helper
function (similar to the pattern suggested for the product resolver). This
helper should encapsulate the cacheKey generation, cache lookup, the conditional
call to getLocalizedProduct when entry is not found, and the cache storage logic
to improve code reusability and maintainability.
packages/core/src/sdk/localization/useBindingSelector.ts (1)

69-70: ⚡ Quick win

Validate recovered session payload before using it

Line 69 asserts parsed JSON as LocalizedProductLocale[]. Since sessionStorage is untyped, add a runtime guard and return null for invalid shapes instead of asserting.

Suggested patch
+function isLocalizedProductLocaleArray(
+  value: unknown
+): value is LocalizedProductLocale[] {
+  return (
+    Array.isArray(value) &&
+    value.every((item) => {
+      if (typeof item !== 'object' || item === null) return false
+      return (
+        typeof Reflect.get(item, 'locale') === 'string' &&
+        typeof Reflect.get(item, 'slug') === 'string'
+      )
+    })
+  )
+}
+
 export function recoverOtherLocales(): LocalizedProductLocale[] | null {
   if (typeof window === 'undefined') return null
@@
-    return raw ? (JSON.parse(raw) as LocalizedProductLocale[]) : null
+    if (!raw) return null
+    const parsed: unknown = JSON.parse(raw)
+    return isLocalizedProductLocaleArray(parsed) ? parsed : null
   } catch {
     return null
   }
 }

As per coding guidelines, "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/core/src/sdk/localization/useBindingSelector.ts` around lines 69 -
70, The code at the JSON.parse line uses a type assertion to cast the parsed
JSON as LocalizedProductLocale[] without validating the actual shape of the
data, which is unsafe since sessionStorage is untyped. Add a runtime validation
function that checks whether the parsed object actually matches the
LocalizedProductLocale[] structure before returning it. If the parsed data fails
validation, return null instead of relying on the type assertion. This ensures
type safety by catching invalid data shapes at runtime rather than blindly
trusting the assertion.

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/loaders/collection.ts`:
- Around line 66-77: The code in the collection.ts file defers implementing
fatherCategoryId-based disambiguation when multiple categories share the same
linkId, simply taking the first match. Create a tracking issue in your project
management system to ensure this edge case is addressed in a follow-up
implementation. The issue should reference that fatherCategoryId (available in
the VTEX catalog response) can disambiguate categories at different tree levels,
and note that similar disambiguation logic is already implemented elsewhere in
resolvers/collection.ts. This prevents the defer from being forgotten and
provides context for future work.

In `@packages/api/src/platforms/vtex/resolvers/query.ts`:
- Line 352: The URL parsing operation using new URL(node.url) in the slug
assignment can throw an exception if the URL is malformed, which would crash the
entire allCollections resolver. Wrap the URL parsing logic (where slug is being
assigned with new URL(node.url).pathname.slice(1).toLowerCase()) in a try/catch
block to handle malformed URLs gracefully. When a URL parsing error occurs,
either skip that node/entry, provide a fallback slug value, or log the error
appropriately so the resolver can continue processing other collection entries
without crashing.

In `@packages/api/src/platforms/vtex/typeDefs/query.graphql`:
- Around line 233-239: The locale argument in the query parameters is accepted
and propagated to platform APIs without validation, which violates input
validation guidelines. Before the resolver uses the locale parameter in
mutateLocaleContext() or propagates it to downstream clients (Search API,
Catalog API, OrderForm), validate it against the
discoveryConfig?.localization?.locales array to ensure it is a configured
locale. Apply the same validation pattern that is already implemented in the
otherLocales resolvers to maintain consistency across the codebase and prevent
invalid locale values from reaching platform APIs.

In `@packages/core/src/pages/`[slug]/p.tsx:
- Around line 134-143: Remove the unnecessary type assertions from the hreflang
configuration extraction in the section extracting locales and binding URLs.
Specifically, delete the `as Record<string, any>` assertion from the locales
variable assignment, the `as | string | undefined` assertion from the
defaultLocale variable assignment, and the `as | string | undefined` assertion
from the bindingUrl variable assignment. These properties are already properly
typed in the codebase and the assertions only mask type information, preventing
compile-time detection of type changes.

In `@packages/core/src/sdk/localization/useBindingSelector.ts`:
- Around line 39-42: The storage key derivation at line 41 depends on array
ordering of otherLocales and the persistence logic at lines 171-175 applies to
both PLP and PDP flows. Instead of extracting skuId from
otherLocales[0]?.slug.split('-').pop(), extract it from window.location.pathname
to make it stable and independent of array ordering. Additionally, modify the
persistence condition around lines 171-175 to only persist when urlSuffix equals
'/p' (PDP flow) rather than persisting whenever otherLocales exists, ensuring
the binding selector state is only cached for product detail pages.

---

Nitpick comments:
In `@packages/api/src/platforms/vtex/loaders/collection.ts`:
- Line 63: The lastSegment variable is extracted from normalizedSlug using
split('/').at(-1)! without validating that the resulting segment is not empty,
which could cause an API query with an empty string if the slug is empty or ends
with a slash. Add an early guard check after extracting lastSegment to validate
that it is not an empty string, and return or throw an error appropriately if it
is empty. This ensures the function fails fast rather than proceeding with
invalid data to the API.

In `@packages/api/src/platforms/vtex/resolvers/collection.ts`:
- Around line 112-115: The category tree is being fetched from
`commerce.catalog.category.tree(10)` on every resolution of the collection meta
resolver without request-scoped caching, which can cause performance issues for
large trees. Implement request-scoped caching for the category tree by checking
if it exists in `ctx.storage` before calling
`commerce.catalog.category.tree(10)`, and if it doesn't exist, fetch it and
store the result in `ctx.storage` for subsequent uses within the same request.
Follow the same pattern used for `productTranslationsCache` to maintain
consistency with existing caching patterns in the codebase.
- Around line 191-193: The catch block in the collection resolver is silently
swallowing all errors without any logging or visibility into what went wrong.
Add logging for the caught error before returning null to improve observability,
or alternatively, make the catch block more specific by only catching expected
errors like NotFoundError and allowing other errors (such as server errors or
network issues) to propagate up. This ensures that unexpected failures are not
hidden and can be properly debugged.
- Around line 157-162: The code contains repeated `as any` type casts for
accessing ctx.discoveryConfig properties (specifically checking
isLocalizationEnabled and accessing configuredLocales), which bypasses
TypeScript type safety. Define a proper LocalizationConfig interface with typed
properties for enabled, defaultLocale, and locales, then create a helper
function (such as getLocalizationConfig) that safely extracts and returns the
typed localization configuration from ctx.discoveryConfig. Replace all instances
of the unsafe `(ctx.discoveryConfig as any)?.localization` casts throughout this
resolver with calls to the new helper function or properly typed property access
to restore type safety.

In `@packages/api/src/platforms/vtex/resolvers/product.ts`:
- Around line 108-127: Extract the duplicated cache-or-fetch logic used in both
the breadcrumbList and otherLocales resolvers into a new helper function called
getLocalizedProductEntry that accepts ctx, productId, and locale parameters.
This function should handle cache lookup, call
ctx.clients.catalog.getLocalizedProduct, cache the result with linkId,
categories, and availableLinkIds properties, and return either the
cached/fetched entry or null on error. Replace the duplicated try-catch blocks
in both resolvers with calls to this new helper function to ensure consistent
behavior and reduce code duplication.

In `@packages/api/src/platforms/vtex/resolvers/query.ts`:
- Around line 100-140: The cache-or-fetch pattern for retrieving localized
product data is complex and would benefit from extraction into a reusable helper
function. Extract the logic that checks ctx.storage.productTranslationsCache,
retrieves or creates an entry using catalog.getLocalizedProduct, and sets the
cache into a separate helper function (similar to the pattern suggested for the
product resolver). This helper should encapsulate the cacheKey generation, cache
lookup, the conditional call to getLocalizedProduct when entry is not found, and
the cache storage logic to improve code reusability and maintainability.

In `@packages/core/src/sdk/localization/useBindingSelector.ts`:
- Around line 69-70: The code at the JSON.parse line uses a type assertion to
cast the parsed JSON as LocalizedProductLocale[] without validating the actual
shape of the data, which is unsafe since sessionStorage is untyped. Add a
runtime validation function that checks whether the parsed object actually
matches the LocalizedProductLocale[] structure before returning it. If the
parsed data fails validation, return null instead of relying on the type
assertion. This ensures type safety by catching invalid data shapes at runtime
rather than blindly trusting the assertion.
🪄 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: 95c0a81b-a9e4-4fe6-bd9f-8b9d23c16492

📥 Commits

Reviewing files that changed from the base of the PR and between b7819e7 and 0424a04.

⛔ Files ignored due to path filters (3)
  • packages/api/src/__generated__/schema.ts is excluded by !**/__generated__/** and included by packages/**
  • 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 (23)
  • packages/api/src/platforms/vtex/clients/catalog/index.ts
  • packages/api/src/platforms/vtex/clients/commerce/index.ts
  • packages/api/src/platforms/vtex/clients/commerce/types/ByLinkId.ts
  • packages/api/src/platforms/vtex/clients/index.ts
  • packages/api/src/platforms/vtex/index.ts
  • packages/api/src/platforms/vtex/loaders/collection.ts
  • packages/api/src/platforms/vtex/resolvers/collection.ts
  • packages/api/src/platforms/vtex/resolvers/product.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/api/src/platforms/vtex/typeDefs/collection.graphql
  • packages/api/src/platforms/vtex/typeDefs/product.graphql
  • packages/api/src/platforms/vtex/typeDefs/query.graphql
  • packages/core/src/components/sections/EmptyState/EmptyState.tsx
  • packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx
  • packages/core/src/components/ui/Breadcrumb/Breadcrumb.tsx
  • packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx
  • packages/core/src/customizations/src/fragments/ServerCollectionPage.ts
  • packages/core/src/experimental/index.ts
  • packages/core/src/pages/[...slug].tsx
  • 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

Comment thread packages/api/src/platforms/vtex/loaders/collection.ts
Comment thread packages/api/src/platforms/vtex/resolvers/query.ts
Comment thread packages/api/src/platforms/vtex/typeDefs/query.graphql
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(core,api): replace pagetype with by-linkid cascade + localized PLP/collection pages feat: replace pagetype with by-linkid cascade + localized PLP/collection pages Jun 19, 2026
@hellofanny hellofanny changed the base branch from dev to feat/localized-slugs-links June 19, 2026 20:18
@hellofanny hellofanny force-pushed the feat/localized-collection-pages branch from 0424a04 to 7f6d686 Compare June 20, 2026 00:16
@pkg-pr-new

pkg-pr-new Bot commented Jun 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@faststore/api

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

@faststore/cli

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

@faststore/components

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

@faststore/core

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

@faststore/diagnostics

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

@faststore/lighthouse

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

@faststore/sdk

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

@faststore/ui

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

commit: c12e1f8

@vtex-pr-sentinel

vtex-pr-sentinel Bot commented Jun 24, 2026

Copy link
Copy Markdown

🛡️ SDD Check — action required

I couldn't detect an SDD in this PR. Please check one option below (requires write access to the repo):

@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/core/src/sdk/product/useLocalizedVariables.ts`:
- Around line 29-40: The useMemo hook depends on router.asPath to trigger
recalculation, but getSettings() internally uses window.location.href, which can
diverge briefly on navigation. Modify the getSettings() call within the useMemo
to accept an explicit URL parameter and pass it the current URL (you can use
router.asPath or construct from window.location.href) so that getSettings() uses
the same URL that triggered the memo recomputation, keeping the dependency
trigger and the locale resolution in sync.
🪄 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: cf820564-973c-4fd2-affe-1344cde70d4a

📥 Commits

Reviewing files that changed from the base of the PR and between 7f6d686 and a85d024.

📒 Files selected for processing (2)
  • packages/api/src/platforms/vtex/clients/search/index.ts
  • packages/core/src/sdk/product/useLocalizedVariables.ts

Comment thread packages/core/src/sdk/product/useLocalizedVariables.ts Outdated
@hellofanny hellofanny self-assigned this Jun 24, 2026
@hellofanny hellofanny added the enhancement New feature or request label Jun 24, 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: 2

🧹 Nitpick comments (1)
packages/api/test/unit/platforms/vtex/loaders/collection.test.ts (1)

64-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid double type assertion in test client stub.

as unknown as Clients masks type drift; prefer a narrowly typed stub type that matches only what getCollectionLoader consumes. As per path instructions, packages/**/*.{ts,tsx}: “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/test/unit/platforms/vtex/loaders/collection.test.ts` around
lines 64 - 76, The test stub in makeClients() is using a double type assertion
that hides type mismatches; replace it with a narrowly typed object or helper
type that only includes the properties consumed by getCollectionLoader and
matches the Clients shape without forcing cast-through-unknown. Keep the stub
focused on commerce.catalog.byLinkId with the mocked category, brand, and
collection members, and remove the as unknown as Clients assertion so the test
remains type-safe.

Source: Path instructions

🤖 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/test/unit/platforms/vtex/clients/commerce.test.ts`:
- Around line 257-265: The URL-encoding test for commerce catalog byLinkId does
not actually cover encoding because the current linkId value has no special
characters. Update the test in the commerce.test suite to use a linkId
containing characters that must be encoded (for example spaces or reserved URL
characters), then assert the generated URL contains the encoded form from
commerce.catalog.byLinkId.category so the test truly verifies encoding behavior.

In `@packages/api/test/unit/platforms/vtex/clients/search.test.ts`:
- Around line 6-13: The fetch mock used in the VTEX search test is being
referenced inside a hoisted vi.mock factory before it is safely initialized.
Update the search.test.ts setup so fetchAPIMocked is created via vi.hoisted (or
another hoisted binding) before the vi.mock call that uses it, and keep the rest
of the test wiring in place around the searchOptions and mocked API client
setup.

---

Nitpick comments:
In `@packages/api/test/unit/platforms/vtex/loaders/collection.test.ts`:
- Around line 64-76: The test stub in makeClients() is using a double type
assertion that hides type mismatches; replace it with a narrowly typed object or
helper type that only includes the properties consumed by getCollectionLoader
and matches the Clients shape without forcing cast-through-unknown. Keep the
stub focused on commerce.catalog.byLinkId with the mocked category, brand, and
collection members, and remove the as unknown as Clients assertion so the test
remains type-safe.
🪄 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: 48d38b3b-4ac3-4598-8a27-8a59871f5655

📥 Commits

Reviewing files that changed from the base of the PR and between a85d024 and 3857e59.

📒 Files selected for processing (4)
  • packages/api/test/unit/platforms/vtex/clients/commerce.test.ts
  • packages/api/test/unit/platforms/vtex/clients/search.test.ts
  • packages/api/test/unit/platforms/vtex/loaders/collection.test.ts
  • packages/core/src/sdk/product/useLocalizedVariables.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/sdk/product/useLocalizedVariables.ts

Comment thread packages/api/test/unit/platforms/vtex/clients/commerce.test.ts
Comment thread packages/api/test/unit/platforms/vtex/clients/search.test.ts
* Recursively searches the category tree for the item with the given numeric ID
* and returns the canonical (default-locale) slug extracted from its URL.
*
* The Catalog `by-linkid` endpoint echoes back the queried slug in `linkId`

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.

findCanonicalSlug is current a workaround because we don't have the default locale listed in the availableLinkIds array.
I have reported to the catalog team here

If they implement this part we won't need to do it.

hellofanny added a commit that referenced this pull request Jun 29, 2026
## Summary

- **`@faststore/core`**: when `localization.enabled`, derive the locale
facet for client-side IS queries from `router.locale` (via
`getSettings`) instead of `session.locale`, which can lag after a hard
locale switch or back/forward navigation.
- **`@faststore/api`**: prefer `ctx.storage.locale` (set from trusted
`selectedFacets`) over `vtex_segment` cookie `cultureInfo` when building
Intelligent Search requests — the cookie can remain stale while the URL
and facets already reflect the new locale.

These two changes work together: core sends the correct locale in the
GraphQL request; API ensures IS does not override it with a stale
segment cookie.

## Context

Introduced while working on localized PLP pages (#3401). The bug also
affects stores with localization enabled on `dev` today (locale-prefixed
PLPs, search, shelves) even without localized collection slug
resolution.

Does **not** include PLP-specific work (`by-linkid`,
`StoreCollection.otherLocales`, etc.) — that remains in #3401.

## Test plan

- [ ] `cd packages/api && pnpm vitest run
test/unit/platforms/vtex/clients/search.test.ts`
- [ ] With `localization.enabled: true`, open a locale-prefixed PLP
(e.g. `/pt-BR/apparel`)
- [ ] Switch locale via LocalizationSelector → product grid shows
correct translated names immediately
- [ ] Browser back/forward on a localized PLP → product slugs/names stay
correct
- [ ] With `localization.enabled: false`, verify no behavior change on
PLP/search pages


PR:
https://vtex-dev.atlassian.net/jira/software/c/projects/SFS/boards/1051?selectedIssue=SFS-3242
Made with [Cursor](https://cursor.com)

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

## Summary by CodeRabbit

* **New Features**
* Localized stores now derive the active locale from the current page
URL, enabling immediate updates to language-aware selections during
navigation.

* **Bug Fixes**
* Search requests now prioritize the current stored locale over stale
cookie data, with a safe fallback when locale is unavailable.
* Search requests omit the locale parameter when no locale can be
determined, preventing invalid locale values.
  
* **Tests**
* Added unit coverage to verify locale query parameter behavior across
precedence and fallback scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Cursor <cursoragent@cursor.com>

@renatomaurovtex renatomaurovtex 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.

Review — feat: replace pagetype with by-linkid cascade + localized PLP/collection pages

Reviewed the API logic (byLinkId client, collection loader/resolvers, query.ts) and the core wiring (LocalizedProductContext/useBindingSelector/PLP). This PR targets the feat/localized-slugs-links stack branch (not dev), which lowers immediate blast radius, but the pagetype → by-linkid swap is a real behavior change for the PLP/collection path that warrants maintainer eyes before the stack lands on dev. Strong unit coverage added (commerce/search clients + loader cascade). Boundaries clean: 404→advance, 5xx→hard fail via isNotFoundError.

🟠 Multi-segment category disambiguation — correctness regression risk
loaders/collection.ts resolves a multi-segment slug (vestuario/camisetas) by querying category/by-linkid/<lastSegment> only (camisetas) and, per the inline TODO, "take the first match" when several categories share that linkId at different tree levels. pagetype resolved by the full URL path, so it didn't have this ambiguity. For stores with a repeated leaf linkId under multiple parents this can resolve the wrong category → wrong selectedFacets/products and wrong breadcrumb. The fatherCategoryId discriminator is already on the response — consider walking the parent chain against the preceding segment now rather than deferring, or at least gate the feature off for affected catalogs. Please confirm this is acceptable for GA stores.

🟠 Search locale priority flip is NOT gated by localization.enabled
clients/search/index.ts getSegmentLocale changes precedence from cultureInfo ?? storage.locale to storage.locale || cultureInfo || '' for all stores. Any existing multi-culture store where the vtex_segment cookie's cultureInfo intentionally differs from ctx.storage.locale now gets a different IS locale. The PLP fix needs this, but the change is global — recommend scoping it behind localization.enabled (or confirm storage.locale is always the authoritative value for legacy stores).

🟠 Added category.tree(10) to the hot PLP path
meta (category branch) and otherLocales now fetch commerce.catalog.category.tree(10) to recover canonical slugs, on top of N per-segment byLinkId calls. Combined with the cascade (up to 3 sequential catalog calls per uncached slug vs. the single pagetype call), this multiplies upstream catalog traffic and latency for every category PLP render. DataLoader caches within a request, but tree(10) is a large payload. Worth a perf check / shallower depth / caching note before GA.

🟠 SEO description regression (acknowledged)
seo now reads root.metaTagDescription, which the by-linkid response doesn't yet return (TODO Catalog team), so category/brand/collection meta descriptions go empty where pagetype previously populated them. You flagged this in the PR body as a pre-existing gap — calling it out so it makes the release notes; it's a visible SEO change for existing pages, not just new ones.

🟡 Minor

  • Query.collection calls mutateLocaleContext(ctx, locale) with the raw client-supplied locale arg — no validation against configured locales. Low risk (only sets IS locale) but worth bounding to localization.locales keys.
  • Repeated (ctx.discoveryConfig as any)?.localization casts in otherLocales — a typed accessor would be safer/clearer.

🟢 Positives

  • useBindingSelector.recoverOtherLocales now validates the persisted JSON shape (isLocalizedProductLocaleArray) instead of a blind JSON.parse cast — good hardening against poisoned/legacy localStorage.
  • urlSuffix abstraction ('' for PLP, '/p' for PDP) is clean; the persistOtherLocales effect correctly stays PDP-only (urlSuffix === '/p').
  • useLocalizedVariables preferring router.locale over session.locale is gated by localization.enabled — correct, no regression for non-localized stores.
  • slugify-based linkId synthesis in the allCollections flattening keeps that route working without by-linkid round-trips.

CI: FastStore build/test, codesandbox, node-ci all green. SonarQube red — likely new-code coverage/quality gate on the new client+resolver files; please check the dashboard. UI Tests pending (Chromatic baselines to accept).

Verdict: Approved with comments. No hard blockers for a stack branch, but please resolve the multi-segment disambiguation and the un-gated search-locale flip (or get explicit maintainer sign-off) before this stack merges to dev.

…ution

Adds catalog.byLinkId.{category,brand,collection} methods to the
VtexCommerce client
Replaces the pagetype API with the typed by-linkid cascade
(category → brand → collection). Injects entityType
discriminator and slug into loader results
Next.js i18n strips the locale prefix from router.asPath, so
getSettings({ url: new URL(router.asPath, ...) }) always fell back to the default locale — breaking product names and slugs on non-default locale PLPs and search pages. router.locale is set by Next.js i18n directly from the URL prefix and updates synchronously on navigation.
@hellofanny hellofanny force-pushed the feat/localized-collection-pages branch from 783b521 to 9360e5b Compare June 30, 2026 17:17
@hellofanny

Copy link
Copy Markdown
Contributor Author

Thanks for the review!
🟠 Multi-segment category disambiguation — correctness regression risk
I'm confirming this point with catalog team - thread They've fixed but still in beta. I'll comment in the code so we can change this logic when this fix is available.

🟠 Search locale priority flip is NOT gated by localization.enabled
done here

🟠 Added category.tree(10) to the hot PLP path
meta (category branch) and otherLocales now fetch commerce.catalog.category.tree(10) to recover canonical slugs, on top of N per-segment byLinkId calls. Combined with the cascade (up to 3 sequential catalog calls per uncached slug vs. the single pagetype call), this multiplies upstream catalog traffic and latency for every category PLP render. DataLoader caches within a request, but tree(10) is a large payload. Worth a perf check / shallower depth / caching note before GA. ✅ here

🟠 SEO description regression (acknowledged)
I'm waiting for catalog team to add it. It might be a block for now - acknowledge 👍 thread

@sonar-workflows

Copy link
Copy Markdown

Failed Quality Gate failed

  • 11 New Issues (is greater than 0)
  • 55.90% Coverage on New Code (is less than 75.00%)

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

View in SonarQube

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