Skip to content

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

Open
hellofanny wants to merge 18 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 18 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

@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

  • Category PLP (/apparel) — products load, selectedFacets: [{key:"category-1", value:"apparel"}]
  • Sub-category PLP (/apparel/shirts) — multi-level breadcrumb correct
  • 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 support for viewing collections and products in multiple locales with localized URLs.
    • Enabled locale-specific collection queries via optional locale parameter.
    • Added localized collection variants (otherLocales) with per-locale slugs.
    • Improved collection and category lookup using the byLinkId API for better reliability.

@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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5e283ed8-6091-43c0-96f6-a961151c8a31

📥 Commits

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

⛔ 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 (13)
  • packages/api/src/platforms/vtex/clients/commerce/index.ts
  • packages/api/src/platforms/vtex/clients/commerce/types/ByLinkId.ts
  • packages/api/src/platforms/vtex/loaders/collection.ts
  • packages/api/src/platforms/vtex/resolvers/collection.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts
  • packages/api/src/platforms/vtex/typeDefs/collection.graphql
  • packages/api/src/platforms/vtex/typeDefs/query.graphql
  • 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/core/src/pages/[...slug].tsx
  • packages/core/src/sdk/localization/LocalizedProductContext.tsx
  • packages/core/src/sdk/localization/useBindingSelector.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/customizations/src/fragments/ServerCollectionPage.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • packages/api/src/platforms/vtex/clients/commerce/types/ByLinkId.ts
  • packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx
  • packages/core/src/components/ui/LocalizationButton/LocalizationButton.tsx
  • packages/core/src/pages/[...slug].tsx
  • packages/api/src/platforms/vtex/typeDefs/query.graphql
  • packages/core/src/sdk/localization/LocalizedProductContext.tsx
  • packages/api/src/platforms/vtex/typeDefs/collection.graphql
  • packages/api/src/platforms/vtex/clients/commerce/index.ts
  • packages/api/src/platforms/vtex/resolvers/collection.ts
  • packages/core/src/sdk/localization/useBindingSelector.ts
  • packages/api/src/platforms/vtex/loaders/collection.ts
  • packages/api/src/platforms/vtex/resolvers/query.ts

Walkthrough

Rewrites the VTEX collection resolution pipeline from portal.pagetype to a sequential byLinkId cascade (category → brand → collection) using new API client methods. Extends the GraphQL schema with otherLocales on StoreCollection and a locale arg on Query.collection. Updates collection/query resolvers for canonical slugs and localized hreflang alternates, and wires LocalizedProductContext with a configurable urlSuffix into the PLP page and LocalizationButton.

Changes

Localization for Collections via ByLinkId API

Layer / File(s) Summary
ByLinkId response types and commerce client methods
packages/api/src/platforms/vtex/clients/commerce/types/ByLinkId.ts, packages/api/src/platforms/vtex/clients/commerce/index.ts
Three new response interfaces (ByLinkIdCategoryResponse, ByLinkIdBrandResponse, ByLinkIdCollectionResponse) with nullable SEO/locale fields. catalog.byLinkId gains category, brand, and collection async methods that URL-encode linkIds and return null on not-found errors.
Collection loader: discriminated Root union and byLinkId cascade
packages/api/src/platforms/vtex/loaders/collection.ts
Replaces CollectionPageType with a discriminated Root union (`ByLinkIdCategoryRoot
GraphQL schema: otherLocales and locale arg
packages/api/src/platforms/vtex/typeDefs/collection.graphql, packages/api/src/platforms/vtex/typeDefs/query.graphql
StoreCollection gains an otherLocales: [StoreCollectionLocale!] field and a new StoreCollectionLocale type. Query.collection gains an optional locale: String argument.
Collection resolver: slugifyRoot, findCanonicalSlug, async meta/breadcrumb/otherLocales
packages/api/src/platforms/vtex/resolvers/collection.ts
Root type and slugifyRoot rewritten for byLinkId roots. Adds findCanonicalSlug tree-walk. meta becomes async, loading entities per segment with canonical facet slugs. breadcrumbList uses slugifyRoot. otherLocales gates on discoveryConfig.localization.enabled and joins per-segment availableLinkIds.
Query resolver: localized slug validation, collection locale, allCollections refactor
packages/api/src/platforms/vtex/resolvers/query.ts
Adds isLocalizedSlugMatch helper with productTranslationsCache. Product resolver slug-mismatch path validates against localized linkId. Collection resolver applies mutateLocaleContext when locale is provided. allCollections rebuilds via root-based DFS producing ByLinkIdCategoryRoot/ByLinkIdBrandRoot entries.
LocalizedProductContext and useBindingSelector urlSuffix support
packages/core/src/sdk/localization/LocalizedProductContext.tsx, packages/core/src/sdk/localization/useBindingSelector.ts
LocalizedProductData and LocalizedProductProvider gain a urlSuffix field/prop (default '/p'). useBindingSelector accepts urlSuffix, appends it to localized slug redirects, and adds a PDP stale-slug redirect branch.
PLP page and component wiring
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
PLP query adds $locale variable and otherLocales sub-selection; getStaticProps passes locale. ProductListingPage wraps with LocalizedProductProvider (urlSuffix ""). LocalizationButton reads urlSuffix from context and passes it to useBindingSelector. Fragment forwards locale variable.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant PLPPage as [...slug].tsx
  participant GraphQL
  participant CollectionLoader
  participant VtexCommerce as catalog.byLinkId
  participant CollectionResolver

  Browser->>PLPPage: getStaticProps(slug, locale)
  PLPPage->>GraphQL: Query.collection(slug, locale)
  GraphQL->>CollectionResolver: mutateLocaleContext(locale)
  GraphQL->>CollectionLoader: load(slug)
  CollectionLoader->>VtexCommerce: byLinkId.category(leaf)
  VtexCommerce-->>CollectionLoader: ByLinkIdCategoryRoot | null
  CollectionLoader->>VtexCommerce: byLinkId.brand(leaf) [if null]
  VtexCommerce-->>CollectionLoader: ByLinkIdBrandRoot | null
  CollectionLoader-->>GraphQL: Root
  GraphQL->>CollectionResolver: otherLocales(Root)
  CollectionResolver->>CollectionLoader: load(each segment)
  CollectionResolver-->>GraphQL: [{locale, slug}]
  GraphQL-->>PLPPage: {collection: {otherLocales: [...]}}
  PLPPage-->>Browser: LocalizedProductProvider(otherLocales, urlSuffix="")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • vtex/faststore#3176: Refactors useBindingSelector naming (i18n→localization), directly adjacent to this PR's urlSuffix and redirect logic changes in the same hook.
  • vtex/faststore#3237: Modifies useBindingSelector's save redirect to use buildRedirectUrl for preserving path/query/hash — the same redirect path this PR extends with urlSuffix and PDP stale-slug handling.
  • vtex/faststore#3352: Introduces catalog.getLocalizedProduct and productTranslationsCache slug-validation logic in query.ts, which this PR builds on for the isLocalizedSlugMatch helper and product resolver localized match path.

Suggested labels

enhancement

Suggested reviewers

  • lucasfp13
  • ommeirelles
  • renatomaurovtex

Poem

🌍 The slug cascade flows — category, brand, then collection found,
Leaf segments normalized, canonical paths abound.
byLinkId whispers back the locale-aware truth,
otherLocales blooms in hreflang proof.
PLP now speaks in every tongue with grace,
urlSuffix trailing softly into place. 🏷️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main architectural change: replacing pagetype API with by-linkid cascade and adding localization to 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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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 and usage tips.

@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 on lines +66 to +77
const categories =
await clients.commerce.catalog.byLinkId.category(lastSegment)
if (categories !== null && categories.length > 0) {
// When multiple categories share the same linkId at different tree levels
// (e.g. "bolas" under both "esportes" and "infantil"), fatherCategoryId-based
// disambiguation can be added here in a follow-up. For now, take the first match.
return {
...categories[0],
entityType: 'category' as const,
slug: normalizedSlug,
}
}

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a tracking issue for this disambiguation work
rg -n "fatherCategoryId" --type=ts -C2

Repository: vtex/faststore

Length of output: 2818


Consider tracking fatherCategoryId-based disambiguation for follow-up.

The code currently selects the first category when multiple share the same linkId at different tree levels. The fatherCategoryId field (available in the response) could disambiguate these cases—it's already used elsewhere for similar category level distinction (see resolvers/collection.ts). While the comment correctly flags this as deferred, verify whether this edge case warrants a tracking issue to prevent it from being overlooked.

🤖 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` around lines 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.

metaTagDescription: node.MetaTagDescription,
availableLinkIds: null,
entityType: 'category' as const,
slug: new URL(node.url).pathname.slice(1).toLowerCase(),

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Defensive: wrap URL parsing in try/catch.

new URL(node.url) can throw if the URL is malformed. While VTEX API data should be valid, a single corrupted entry would crash the entire allCollections resolver.

-        slug: new URL(node.url).pathname.slice(1).toLowerCase(),
+        slug: (() => {
+          try {
+            return new URL(node.url).pathname.slice(1).toLowerCase()
+          } catch {
+            return slugify(node.name)
+          }
+        })(),
🤖 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` at 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.

Comment on lines +233 to 239
"""
Locale code (e.g. "pt-BR"). When provided, the resolver sets the request
locale so that localization-aware fields (e.g. otherLocales) return data
for the correct locale instead of the store default.
"""
locale: String
): StoreCollection!

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect Query.collection resolver and locale handling:"
sed -n '1,260p' packages/api/src/platforms/vtex/resolvers/query.ts

echo
echo "Find locale usage in VTEX resolvers and clients:"
rg -n -C3 '\blocale\b|ctx\.storage\.locale|getLocalizedProduct|Accept-Language' packages/api/src/platforms/vtex/resolvers packages/api/src/platforms/vtex/clients

echo
echo "Look for locale validation/allowlist patterns:"
rg -n -C2 'supportedLocales|defaultLocale|localization|validate|isValidLocale|Intl\.Locale|BCP|^[a-z]{2}-[A-Z]{2}$' packages/api/src/platforms/vtex/resolvers/query.ts packages/api/src/platforms/vtex/index.ts

Repository: vtex/faststore

Length of output: 40014


🏁 Script executed:

find packages/api/src/platforms/vtex -name "*contex*" -o -name "*context*" | head -20

Repository: vtex/faststore

Length of output: 106


🏁 Script executed:

rg -n "mutateLocaleContext|findLocale" packages/api/src/platforms/vtex/utils/ -A 15

Repository: vtex/faststore

Length of output: 831


🏁 Script executed:

cat -n packages/api/src/platforms/vtex/utils/facets.ts | sed -n '1,100p'

Repository: vtex/faststore

Length of output: 3402


🏁 Script executed:

cat -n packages/api/src/platforms/vtex/utils/contex.ts

Repository: vtex/faststore

Length of output: 495


🏁 Script executed:

rg -n "discoveryConfig.*locale|configuredLocales|supportedLocales" packages/api/src/platforms/vtex/ -A 2 -B 2

Repository: vtex/faststore

Length of output: 2540


Validate locale argument against configured locales before propagating to platform APIs.

The locale: String argument (line 238) is accepted directly and passed to mutateLocaleContext() without server-side validation. The resolver then propagates it unchecked to downstream clients (Search API as query parameter, Catalog API as Accept-Language header, and OrderForm as clientPreferencesData.locale).

This violates the coding guideline requiring validation of customer input before platform propagation. Validate the input against discoveryConfig?.localization?.locales (available in discovery config) before setting ctx.storage.locale, matching the validation pattern already used in otherLocales 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/typeDefs/query.graphql` around lines 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.

Source: Coding guidelines

Comment thread packages/core/src/pages/[slug]/p.tsx Outdated
Comment on lines +134 to +143
const locales = storeConfig.localization.locales as Record<string, any>
const baseStoreUrl = storeConfig.storeUrl.replace(/\/$/, '')
const defaultLocale = storeConfig.localization.defaultLocale as
| string
| undefined

for (const { locale, slug } of server.product.otherLocales) {
const bindingUrl = locales?.[locale]?.bindings?.[0]?.url as
| string
| undefined

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find discovery config declarations/typing sources
fd -i "discovery.config*"

# Inspect localization-related typing across TS files
rg -n --type=ts "declare module ['\"]discovery.config['\"]|localization|defaultLocale|bindings|storeUrl"

# Confirm assertion usage in the changed PDP file
rg -n --type=ts "as Record<string, any>|as\\s*\\|\\s*string\\s*\\|\\s*undefined" "packages/core/src/pages/[slug]/p.tsx"

Repository: vtex/faststore

Length of output: 44239


🏁 Script executed:

cat -n packages/core/src/typings/locales.ts

Repository: vtex/faststore

Length of output: 1084


🏁 Script executed:

cat -n packages/core/src/sdk/localization/types.ts

Repository: vtex/faststore

Length of output: 1242


🏁 Script executed:

head -200 packages/core/src/pages/\[slug\]/p.tsx | tail -100

Repository: vtex/faststore

Length of output: 3306


🏁 Script executed:

rg -n "storeConfig.*:" packages/core/src -A 5 -B 2 | head -50

Repository: vtex/faststore

Length of output: 3845


🏁 Script executed:

rg -n "type StoreConfig|interface StoreConfig" packages/core/src -A 20 | head -80

Repository: vtex/faststore

Length of output: 5153


🏁 Script executed:

grep -n "storeConfig" packages/core/src/pages/\[slug\]/p.tsx | head -20

Repository: vtex/faststore

Length of output: 650


🏁 Script executed:

cat -n packages/core/discovery.config.default.js | head -100

Repository: vtex/faststore

Length of output: 3160


🏁 Script executed:

rg -n "localization|locales" packages/core/src/sdk/localization/useLocalizationConfig.tsx -A 3 -B 2

Repository: vtex/faststore

Length of output: 668


Remove unnecessary type assertions in hreflang config extraction.

Lines 134, 136–138, and 141–142 use as casts that obscure properly typed configuration. The locales, defaultLocale, and bindingUrl properties are already strongly typed in the codebase and do not require assertions. Removing them exposes type changes at compile time instead of masking them.

Suggested refactor
-    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
@@
-      const bindingUrl = locales?.[locale]?.bindings?.[0]?.url as
-        | string
-        | undefined
-      if (bindingUrl) {
+      const bindingUrl = locales?.[locale]?.bindings?.[0]?.url
+      if (typeof bindingUrl === 'string' && bindingUrl.length > 0) {
         hreflangLinks.push({
           rel: 'alternate',
           hrefLang: locale,
           href: `${bindingUrl.replace(/\/$/, '')}/${slug}/p`,
         })
       }
🤖 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 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.

Source: Coding guidelines

Comment on lines +39 to +42
if (typeof window === 'undefined' || !otherLocales.length) return

const skuId = otherLocales[0]?.slug.split('-').pop()
if (!skuId) return

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scope session persistence to PDPs and key by current pathname SKU

Line 41 derives the storage key from the first localized slug, and Lines 171-175 persist whenever otherLocales exists (including PLP). This can create irrelevant keys and makes keying depend on array ordering. Derive the key from window.location.pathname and persist only for PDP flow (urlSuffix === '/p').

Suggested patch
 export function persistOtherLocales(
   otherLocales: LocalizedProductLocale[]
 ): void {
   if (typeof window === 'undefined' || !otherLocales.length) return
 
-  const skuId = otherLocales[0]?.slug.split('-').pop()
+  const skuId = getSkuIdFromPdpPath(window.location.pathname)
   if (!skuId) return
 
   try {
@@
   useEffect(() => {
-    if (otherLocales?.length) {
+    if (urlSuffix === '/p' && otherLocales?.length) {
       persistOtherLocales(otherLocales)
     }
-  }, [otherLocales])
+  }, [otherLocales, urlSuffix])

Also applies to: 168-175

🤖 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 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.

@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
LocalizationButton lives in the Navbar (global section) uses useSafePDP to have product (localized slugs) on non-PDP pages
…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
@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@7f6d686

@faststore/cli

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

@faststore/components

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

@faststore/core

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

@faststore/diagnostics

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

@faststore/lighthouse

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

@faststore/sdk

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

@faststore/ui

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

commit: 7f6d686

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant