Skip to content

Conversation

@lemagnetic
Copy link
Contributor

@lemagnetic lemagnetic commented Jan 22, 2026

What's the purpose of this pull request?

Add custom path support for i18n in FastStore stores: in addition to Next.js canonical paths (/pt-BR, /en-US), stores can now support URLs like /america/fr/produto/p and /europe/it/apparel, defined by localization bindings in discovery.config. The middleware rewrites these URLs to the format Next.js i18n expects, and internal links (Link, ProductCard, Hero, etc.) automatically get the custom path prefix when the user is on a customized route.

How it works?

Middleware (packages/core/src/middleware.ts)

  • Runs only when storeConfig.localization?.enabled is true.
  • Rewrite rules are generated dynamically from bindings in discovery.config: for each binding whose URL has a custom path (e.g. https://store.com/america/fr), a rule is created that:
    • matches the request path (e.g. /america/fr, /america/fr/produto/p);
    • rewrites internally to the canonical locale path (e.g. /fr-CA, /fr-CA/produto/p).
  • Paths are ordered by length (longest first) to avoid incorrect matches.
  • Hostname validation per rule exists in the code but is disabled (TODO) to ease testing on localhost; it can be re-enabled when needed.
  • The matcher excludes api, _next/static, _next/image, favicon.ico, and static assets.

Custom path utilities (packages/core/src/utils/customPaths.ts)

  • isCustomPath(url) – Returns whether the URL uses a custom path (e.g. /america/fr) instead of a canonical one (/pt-BR).
  • getCustomPathsFromBindings() – Reads custom paths from localization bindings; the result is cached so the loop is not repeated on every link.
  • addCustomPathPrefix(link, currentPathname) – For internal links (/apparel), adds the current custom path prefix when the user is on a customized route (e.g. on /america/fr/apparel becomes /america/fr/apparel). Does not change external links or links that already have the prefix.

Where the prefix is applied to links

  • Link (core) – Uses addCustomPathPrefix(href, router.asPath) for internal links, so all links that go through the core Link component get the prefix when applicable.
  • useProductLink – Applies the prefix to the product href (/${slug}/p), fixing ProductCard links that did not use the core Link.
  • Hero – Applies the prefix to the CTA link?.url before passing it to HeroHeader, fixing the “See Offers” button (fs-link-button) on customized routes.

Middleware toggle by localization feature flag (CLI)

When the localization feature flag (localization.enabled) is false, the CLI disables the i18n middleware

  • toggleI18nMiddlewareByLocalizationFlag (packages/cli/src/utils/generate.ts) – Called from build and dev after the existing localization check and (optional) generate-i18n step:
    • Flag on: Ensures middleware.ts is present (renames middleware__I18N_DISABLED.tsmiddleware.ts if needed).
    • Flag off: Renames middleware.tsmiddleware__I18N_DISABLED.ts so Next.js does not load or run the i18n middleware.

Binding URL format in the admin

Binding URLs must be only the path base, without product or page path. E.g.:
https://store.com/america/fr
https://store.com/america/fr/produto/p

If the binding URL includes a product/page path, the middleware treats that entire path as the custom path for that locale and rewrite rules become incorrect.

Outstanding items (TODOs in code)

  • Re-enable hostname validation per rule in the middleware when it is no longer necessary to skip it on localhost.
  • Re-enable or adjust locale validation for subdomain bindings (useLocaleValidation); the current validation does not account for custom paths (only hostname + locale).

How to test it?

  1. Ensure localization.enabled is true in discovery.config and that there are bindings with a custom path (e.g. https://yoursite.com/america/fr for locale fr-CA). To generate or refresh locales/bindings from the SDK, run from the repo root:
    node packages/cli/bin/run.js generate-i18n ./packages/core
    Requires VTEX credentials: VTEX_ACCOUNT, FS_DISCOVERY_APP_KEY, and FS_DISCOVERY_APP_TOKEN.
  2. Start the store (pnpm dev in core) and open the custom path, e.g. http://localhost:3000/america/fr.
  3. Confirm the page loads with the correct locale (e.g. lang="fr-CA" on <html>).
  4. Navigate via header links, hero (“See Offers”), product cards, and other internal links and confirm they keep the prefix (e.g. /america/fr/technology, /america/fr/produto-x/p).
  5. On a canonical route (e.g. /pt-BR), confirm links remain without a custom prefix (e.g. /apparel, /produto/p).
  6. Turn off localization.enabled, run pnpm dev or pnpm build again, and confirm the CLI renames middleware.ts to middleware__I18N_DISABLED.ts (so Next.js does not run the i18n middleware and links do not get a prefix).

Starters Deploy Preview

Preview custom path ->
Locale: fr-CA
Home with custom path: https://brandless-cma5xay4001f6dn4xjwato8b4-qqih78fto.b.vtex.app/america/fr

References

Jira task

Summary by CodeRabbit

  • New Features

    • Server-side middleware for automatic locale-based URL rewriting
    • i18n-aware link resolution applied across UI (links, hero, alerts, product links)
    • Custom localization path management and prefixing utilities
    • Build/dev flows now toggle middleware based on the localization setting
  • Removed

    • Legacy redirects middleware and its redirect lookups
  • Refactor

    • Removed client-side locale validation in favor of server-side handling
  • Tests

    • Added comprehensive tests for custom localization path utilities

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Walkthrough

Adds localization-aware server middleware and client link-resolution utilities, removes legacy redirect middleware, updates components to use the new resolver, and toggles middleware files from the CLI based on localization configuration.

Changes

Cohort / File(s) Summary
CLI Middleware Toggle
packages/cli/src/commands/build.ts, packages/cli/src/commands/dev.ts
Call toggleMiddlewareByLocalizationFlag(basePath, localizationEnabled) after localization checks to enable/disable middleware files prior to build/dev.
Generate Utility
packages/cli/src/utils/generate.ts
New exported toggleMiddlewareByLocalizationFlag(basePath, localizationEnabled) that swaps middleware.ts and middleware__DISABLED.ts via fs-extra.moveSync; replaces prior env-based enablement.
New Server Middleware
packages/core/src/middleware.ts
Adds Next.js middleware that builds rewrite rules from custom path bindings and rewrites requests to locale-prefixed paths (preserves query and matches optional hostnames); exports middleware and config.
Removed Legacy Middleware
packages/core/src/middleware__DISABLED.ts
Deleted previous redirect middleware (redirect lookup and redirect responses removed).
Client Link Resolution Hook
packages/core/src/sdk/ui/useLink.ts
New useLink hook exposing resolveLink(href) which applies custom path prefixes using current router asPath.
Localization Path Utilities
packages/core/src/utils/localization/customPaths.ts
New utilities: getCustomPathsFromBindings, addCustomPathPrefix, and CustomPathInfo with parsing, caching, and prefix extraction logic.
Component Integrations
packages/core/src/components/sections/Hero/Hero.tsx, packages/core/src/components/ui/Link/Link.tsx, packages/core/src/components/sections/Alert/Alert.tsx, packages/core/src/sdk/product/useProductLink.ts
Components updated to import/use useLink and call resolveLink(...) for internal links; internal link rendering now uses resolved hrefs.
App Cleanup
packages/core/src/pages/_app.tsx
Removed client-side useLocaleValidation import and invocation.
Tests
packages/core/test/utils/localization/customPaths.test.ts
New tests covering getCustomPathsFromBindings and addCustomPathPrefix (sorting, prefixing, queries/hashes, edge cases).

Sequence Diagrams

sequenceDiagram
    participant Client as Client (Browser)
    participant Middleware as middleware.ts
    participant Config as discovery.config / bindings
    participant NextApp as Next.js App

    Client->>Middleware: Request /products
    activate Middleware
    Middleware->>Config: Read custom path bindings
    activate Config
    Config-->>Middleware: Return bindings
    deactivate Config
    Middleware->>Middleware: Build/Match rewrite rules
    alt match found and valid locale
        Middleware-->>Client: Rewrite to /<locale>/products (preserve query)
    else no match
        Middleware-->>Client: Pass through (no rewrite)
    end
    deactivate Middleware
    Client->>NextApp: Request (possibly locale-prefixed)
    NextApp-->>Client: Response
Loading
sequenceDiagram
    participant Component as UI Component
    participant useLink as useLink Hook
    participant Router as Next.js Router
    participant CustomPaths as customPaths utils

    Component->>useLink: Call resolveLink('/product')
    activate useLink
    useLink->>Router: read asPath
    activate Router
    Router-->>useLink: current asPath
    deactivate Router
    useLink->>CustomPaths: addCustomPathPrefix('/product', asPath)
    activate CustomPaths
    CustomPaths-->>useLink: maybe-prefixed '/<locale>/product'
    deactivate CustomPaths
    useLink-->>Component: return resolved href
    deactivate useLink
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • hellofanny

Poem

Middleware toggled, locales take flight,
Links learn the paths and steer just right,
Server rewrites while clients resolve true,
Old redirects fade — new maps come into view,
Tiny file moves, big routes anew. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: localization middleware' accurately reflects the main change: implementing middleware for custom path localization based on the localization flag.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/i18n-custom-paths-middleware

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 22, 2026

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.

@lemagnetic lemagnetic marked this pull request as ready for review January 22, 2026 22:10
@lemagnetic lemagnetic requested a review from a team as a code owner January 22, 2026 22:10
@lemagnetic lemagnetic requested review from emersonlaurentino and renatomaurovtex and removed request for a team January 22, 2026 22:10
@lemagnetic
Copy link
Contributor Author

@coderabbitai configuration

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Configuration used: defaults

CodeRabbit configuration
language: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
inheritance: false
reviews:
  profile: chill
  request_changes_workflow: false
  high_level_summary: true
  high_level_summary_instructions: ''
  high_level_summary_placeholder: '@coderabbitai summary'
  high_level_summary_in_walkthrough: false
  auto_title_placeholder: '@coderabbitai'
  auto_title_instructions: ''
  review_status: true
  review_details: false
  commit_status: true
  fail_commit_status: false
  collapse_walkthrough: true
  changed_files_summary: true
  sequence_diagrams: true
  estimate_code_review_effort: true
  assess_linked_issues: true
  related_issues: true
  related_prs: true
  suggested_labels: true
  labeling_instructions: []
  auto_apply_labels: false
  suggested_reviewers: true
  auto_assign_reviewers: false
  in_progress_fortune: true
  poem: true
  enable_prompt_for_ai_agents: true
  path_filters: []
  path_instructions: []
  abort_on_close: true
  disable_cache: false
  auto_review:
    enabled: true
    auto_incremental_review: true
    ignore_title_keywords: []
    labels: []
    drafts: false
    base_branches: []
    ignore_usernames: []
  finishing_touches:
    docstrings:
      enabled: true
    unit_tests:
      enabled: true
  pre_merge_checks:
    docstrings:
      mode: warning
      threshold: 80
    title:
      mode: warning
      requirements: ''
    description:
      mode: warning
    issue_assessment:
      mode: warning
    custom_checks: []
  tools:
    ast-grep:
      rule_dirs: []
      util_dirs: []
      essential_rules: true
      packages: []
    shellcheck:
      enabled: true
    ruff:
      enabled: true
    markdownlint:
      enabled: true
    github-checks:
      enabled: true
      timeout_ms: 90000
    languagetool:
      enabled: true
      enabled_rules: []
      disabled_rules: []
      enabled_categories: []
      disabled_categories: []
      enabled_only: false
      level: default
    biome:
      enabled: true
    hadolint:
      enabled: true
    swiftlint:
      enabled: true
    phpstan:
      enabled: true
      level: default
    phpmd:
      enabled: true
    phpcs:
      enabled: true
    golangci-lint:
      enabled: true
    yamllint:
      enabled: true
    gitleaks:
      enabled: true
    checkov:
      enabled: true
    detekt:
      enabled: true
    eslint:
      enabled: true
    flake8:
      enabled: true
    fortitudeLint:
      enabled: true
    rubocop:
      enabled: true
    buf:
      enabled: true
    regal:
      enabled: true
    actionlint:
      enabled: true
    pmd:
      enabled: true
    clang:
      enabled: true
    cppcheck:
      enabled: true
    semgrep:
      enabled: true
    circleci:
      enabled: true
    clippy:
      enabled: true
    sqlfluff:
      enabled: true
    prismaLint:
      enabled: true
    pylint:
      enabled: true
    oxc:
      enabled: true
    shopifyThemeCheck:
      enabled: true
    luacheck:
      enabled: true
    brakeman:
      enabled: true
    dotenvLint:
      enabled: true
    htmlhint:
      enabled: true
    checkmake:
      enabled: true
    osvScanner:
      enabled: true
chat:
  art: true
  auto_reply: true
  integrations:
    jira:
      usage: auto
    linear:
      usage: auto
knowledge_base:
  opt_out: false
  web_search:
    enabled: true
  code_guidelines:
    enabled: true
    filePatterns: []
  learnings:
    scope: auto
  issues:
    scope: auto
  jira:
    usage: auto
    project_keys: []
  linear:
    usage: auto
    team_keys: []
  pull_requests:
    scope: auto
  mcp:
    usage: auto
    disabled_servers: []
code_generation:
  docstrings:
    language: en-US
    path_instructions: []
  unit_tests:
    path_instructions: []
issue_enrichment:
  auto_enrich:
    enabled: false
  planning:
    enabled: true
    auto_planning:
      enabled: true
      labels: []
  labeling:
    labeling_instructions: []
    auto_apply_labels: false

@lemagnetic lemagnetic marked this pull request as draft January 22, 2026 22:19
@lemagnetic lemagnetic marked this pull request as ready for review January 27, 2026 15:37
@lemagnetic lemagnetic force-pushed the feat/i18n-custom-paths-middleware branch from a922cf8 to 6b1f949 Compare January 28, 2026 18:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/core/src/components/sections/Hero/Hero.tsx`:
- Line 72: The call to addCustomPathPrefix uses link?.url which can be undefined
but the function signature addCustomPathPrefix(link: string, currentPathname:
string): string requires a string; update the call in Hero (where link?.url and
router?.asPath are used) to ensure a string is passed—either by short-circuiting
(e.g., only call when link?.url exists) or by providing a safe default like an
empty string (e.g., addCustomPathPrefix(link?.url ?? '', router?.asPath ?? ''))
so the first argument is always a string and types align.

In `@packages/core/src/utils/customPaths.ts`:
- Around line 99-107: extractCustomPathPrefix uses pathname.startsWith(path)
which falsely matches partial segments (e.g., '/it' vs '/item'); update the
match logic in extractCustomPathPrefix (and the analogous check later around
lines 116-127) to require either an exact match or a segment boundary by
replacing startsWith(path) with (pathname === path || pathname.startsWith(path +
'/')) (or equivalent logic that normalizes/trims trailing slashes first) so only
whole-path prefixes are accepted.

In `@packages/core/test/utils/customPaths.test.ts`:
- Around line 4-6: The mock factory for discovery.config.js in
customPaths.test.ts should use Vitest's vi.importActual() instead of a plain
dynamic import to avoid re-entry/circular import issues; update the vi.mock
factory (the anonymous factory passed to vi.mock('../../discovery.config.js'))
to call await vi.importActual('../../discovery.config.js') to obtain the real
module, then spread or reference its exports when returning the partial mock so
tests use the actual exports safely.
🧹 Nitpick comments (5)
packages/core/src/middleware.ts (5)

51-56: ReDoS concern is mitigated by proper escaping.

Static analysis flags this as a potential ReDoS vector, but the escapedPath is properly escaped on lines 51-54, neutralizing special regex characters. The resulting pattern ^${escapedPath}(?:\\/(.*))?$ is linear and safe.

However, since the input originates from discovery.config bindings (trusted config), the risk is minimal. If you want extra safety for future-proofing:

🛡️ Optional: Add path validation
+const MAX_PATH_LENGTH = 256
+
 for (const customPath of customPaths) {
+  if (customPath.path.length > MAX_PATH_LENGTH) {
+    console.warn(`Skipping excessively long path: ${customPath.path.slice(0, 50)}...`)
+    continue
+  }
   const localeConfig = locales[customPath.locale]

69-73: Sorting by regex source length is indirect and fragile.

The intent is to match longer paths first, but sorting by regex.source.length is affected by escaping. A path with special characters will have a longer source than its actual length.

♻️ Proposed fix: Sort by original path length
+type RewriteRule = {
+  regex: RegExp
+  locale: string
+  hostname?: string
+  pathLength: number
+}

// In the rule creation:
         rules.push({
           regex,
           locale: customPath.locale,
           hostname: bindingUrl.hostname,
+          pathLength: customPath.path.length,
         })

// In the sort:
-  return rules.sort((a, b) => {
-    const aLength = a.regex.source.length
-    const bLength = b.regex.source.length
-    return bLength - aLength
-  })
+  return rules.sort((a, b) => b.pathLength - a.pathLength)

83-83: Unused hostname variable.

hostname is destructured but unused since hostname validation is disabled (lines 86-89). Remove it to avoid confusion, or keep only if you plan to re-enable soon.

-  const { pathname, search, hostname } = request.nextUrl
+  const { pathname, search } = request.nextUrl

97-100: console.warn in middleware can be noisy.

This logs on every request that hits an invalid locale rule. Consider logging once at startup during generateRewriteRules() instead, or use a debug flag.

♻️ Move validation to rule generation
// In generateRewriteRules(), after creating the rule:
+        if (!isValidLocale(customPath.locale)) {
+          console.warn(`Skipping rule for invalid locale: ${customPath.locale}`)
+          continue
+        }
+
         rules.push({
           regex,
           locale: customPath.locale,
           hostname: bindingUrl.hostname,
         })

// Then remove the runtime check in middleware:
-    if (!isValidLocale(rule.locale)) {
-      console.warn(`Locale ${rule.locale} not found in configuration`)
-      continue
-    }

105-109: Redundant url.search = search assignment.

request.nextUrl.clone() already preserves the search params. This line is harmless but unnecessary.

     const url = request.nextUrl.clone()
     url.pathname = normalizedPath
-    url.search = search

     return NextResponse.rewrite(url)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/core/test/utils/customPaths.test.ts`:
- Around line 167-176: The test contains a conditional wrapper around the
assertion which can cause the test to pass silently; remove the if
(currentPath.startsWith('/europe/it')) check and assert directly that
addCustomPathPrefix(link, currentPath) returns '/europe/it/apparel' (referencing
the test using addCustomPathPrefix and the currentPath variable) so the
expectation always runs and the intent is explicit.
- Around line 187-196: The test uses a redundant conditional guard around the
assertion; remove the if block and assert directly that addCustomPathPrefix('/',
'/europe/it/apparel') returns '/europe/it/'. Update the test in
customPaths.test.ts to call addCustomPathPrefix with link='/' and
currentPath='/europe/it/apparel' and replace the guarded expect with a direct
expect(result).toBe('/europe/it/') referencing the addCustomPathPrefix helper.
🧹 Nitpick comments (2)
packages/core/test/utils/customPaths.test.ts (2)

1-2: Consolidate vitest imports.

Minor style nit: both imports are from the same package.

-import { vi } from 'vitest'
-import { describe, expect, it } from 'vitest'
+import { vi, describe, expect, it } from 'vitest'

4-6: Add type parameter to vi.importActual for type safety.

Per TypeScript guidelines, avoid implicit any. Adding the type parameter improves IDE support and catches type mismatches in the mock spread.

Suggested fix
 vi.mock('../../discovery.config.js', async () => {
-  const original = await vi.importActual('../../discovery.config.js')
+  const original = await vi.importActual<typeof import('../../discovery.config.js')>(
+    '../../discovery.config.js'
+  )
   return {

Comment on lines 75 to 77
if (!pathname || pathname === '/') {
continue
}
Copy link
Contributor

Choose a reason for hiding this comment

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

question: could we add it to the isCustomPath check? Half of it is already there

* @param link - The link to check (e.g., '/apparel' or '/europe/it/apparel')
* @returns true if link already has a custom path prefix
*/
function hasCustomPathPrefix(link: string): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

question: do we need both extractCustomPathPrefix and hasCustomPathPrefix? We could use extractCustomPathPrefix to test if the link has a custom path prefix

Comment on lines 28 to 50
const locales = (storeConfig.localization.locales ||
{}) as LocalesSettings['locales']

for (const customPath of customPaths) {
const localeConfig = locales[customPath.locale]

if (!localeConfig?.bindings || !Array.isArray(localeConfig.bindings)) {
continue
}

for (const binding of localeConfig.bindings) {
if (!binding.url) {
continue
}

try {
const bindingUrl = new URL(binding.url)
const pathname = bindingUrl.pathname.replace(/\/$/, '')

if (pathname !== customPath.path) {
continue
}

Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: I believe we could refactor it so we don't redo things that were already calculated in getCustomPathsFromBindings

@lemagnetic lemagnetic added the enhancement New feature or request label Feb 2, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/utils/customPaths.ts`:
- Around line 103-133: The prefix detection functions extractCustomPathPrefix
and hasCustomPathPrefix currently compare raw pathname/link values and can miss
matches when inputs include query strings or hash fragments; update both
functions to normalize the input by stripping anything from the first '?' or '#'
onward (so only the pathname is compared) before calling
getCustomPathsFromBindings and performing the equality or startsWith checks
against each { path }, ensuring you still match exact path or segment-boundary
cases (e.g., '/it' vs '/it/apparel').
🧹 Nitpick comments (1)
packages/core/src/utils/customPaths.ts (1)

58-60: Avoid the type assertion and narrow safely.

The cast to LocalesSettings['locales'] skips type safety. Consider using a runtime guard or a typed helper to keep this section safe without assertions.

As per coding guidelines: Ensure type safety and avoid type assertions when possible.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/core/src/middleware.ts`:
- Around line 83-109: The hostname validation that prevents cross-host locale
rewrites is currently disabled; re-enable it in the middleware that iterates
rewriteRules by restoring the check on rule.hostname against
request.nextUrl.hostname (or adding an environment-gated check) so that in the
loop over rewriteRules you skip rules whose rule.hostname exists and does not
match hostname (e.g., if (rule.hostname && rule.hostname !== hostname)
continue), and if you want dev convenience only, wrap that skip in a
NODE_ENV/dev-only conditional so production always enforces hostname matching.

In `@packages/core/src/utils/localization/customPaths.ts`:
- Around line 112-136: The function addCustomPathPrefix currently treats
protocol-relative URLs (those starting with "//") as internal and may prefix
them; update addCustomPathPrefix to detect and skip protocol-relative URLs by
returning link unchanged when link startsWith("//") (in addition to the existing
non-slash and extractCustomPathPrefix checks). Locate addCustomPathPrefix and
add the guard before extracting/adding a prefix (use the existing
extractCustomPathPrefix and getPathOnly helpers unchanged).

@hellofanny
Copy link
Contributor

@lemagnetic I think we missed the Alert section link:
This link should also support the customPath 😉

core/src/components/sections/Alert/Alert.tsx
image

@lemagnetic
Copy link
Contributor Author

@lemagnetic I think we missed the Alert section link: This link should also support the customPath 😉

core/src/components/sections/Alert/Alert.tsx image

thanks
3cc6c64

)

if (isInternalLink) {
return (
Copy link
Contributor

Choose a reason for hiding this comment

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

it's a not about this file, but we also need to handle link in SearchHistory and SearchTop 😬

Image Image

Copy link
Contributor

Choose a reason for hiding this comment

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

ps: We can create another task just for these updates after this one 👍

import { Link as UILink } from '@faststore/ui'
import type { LinkProps as UILinkProps, LinkElementType } from '@faststore/ui'

import { useLink } from '../../../sdk/ui/useLink'
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import { useLink } from '../../../sdk/ui/useLink'
import { useLink } from 'src/sdk/ui/useLink''

export type CustomPathInfo = {
path: string
locale: string
/** Hostname from binding URL (for middleware rewrite rules; TODO: re-enable host validation) */
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/** Hostname from binding URL (for middleware rewrite rules; TODO: re-enable host validation) */
/** Hostname from binding URL (for middleware rewrite rules) */

Is this TODO still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

comment removed
65538a4

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/src/components/ui/Link/Link.tsx`:
- Around line 20-30: The code indexes and treats href as a string (href[0],
href[1], resolveLink(href)) but Next.js LinkProps allows UrlObject; add a type
guard so you only index/resolve when typeof href === 'string' (e.g., compute
isInternalLink using typeof href === 'string' && href.startsWith('/') && href[1]
!== '/'), and compute finalHref by calling resolveLink(href) only when href is a
string (otherwise leave finalHref as the original href UrlObject); also ensure
the value passed to NextLink uses this guarded finalHref.
🧹 Nitpick comments (1)
packages/core/src/utils/localization/customPaths.ts (1)

51-89: Type assertion could be tightened, but acceptable for config access.

Line 64 uses as LocalesSettings['locales'] which bypasses type checking. Consider narrowing the type via a type guard if LocalesSettings evolves, but this is acceptable given the static nature of discovery.config.

The longest-first sorting (line 87) correctly ensures /europe/it matches before /europe, preventing partial-prefix collisions.

Comment on lines +20 to +30
const { resolveLink } = useLink()

const isInternalLink = useMemo(
() => href[0] === '/' && href[1] !== '/',
[href]
)

const finalHref = useMemo(
() => (isInternalLink ? (resolveLink(href) ?? href) : href),
[href, isInternalLink, resolveLink]
)
Copy link

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:

cd packages/core/src/components/ui/Link && cat -n Link.tsx

Repository: vtex/faststore

Length of output: 1981


🏁 Script executed:

# Also check the imports to understand where LinkProps comes from
grep -r "LinkProps" packages/core/src/components/ui/Link/ -A 2 -B 2

Repository: vtex/faststore

Length of output: 1741


🌐 Web query:

Next.js LinkProps href type definition - can it be UrlObject

💡 Result:

Yes. In Next.js, next/link’s LinkProps['href'] supports a URL object (UrlObject from Node’s url types).

  • In the shipped type definitions, href is Url where type Url = string | UrlObject. [1]
  • If you enable statically typed links (typedRoutes), href becomes RouteImpl<...> | UrlObject (so UrlObject is still allowed). [2]
  • The official docs also show passing an object { pathname, query } as href. [3]

How to type it in your own component

import type { LinkProps } from 'next/link'

type Href = LinkProps['href']  // (string | UrlObject) OR (RouteImpl | UrlObject) with typedRoutes

Or explicitly:

import type { UrlObject } from 'url'
type Href = string | UrlObject

Example

import Link from 'next/link'

<Link href={{ pathname: '/about', query: { name: 'test' } }}>About</Link>

References: [1] (app.unpkg.com) [2] (app.unpkg.com) [3] (nextjs.org)

Citations:


Guard non‑string href before indexing or resolving.

href can be a UrlObject per Next.js LinkProps (e.g., { pathname: '...', query: {...} }), so indexing href[0] and calling resolveLink(href) will throw at runtime. Add a type guard.

Safe guard + fallback
  const isInternalLink = useMemo(
-   () => href[0] === '/' && href[1] !== '/',
+   () => typeof href === 'string' && href[0] === '/' && href[1] !== '/',
    [href]
  )

  const finalHref = useMemo(
-   () => (isInternalLink ? (resolveLink(href) ?? href) : href),
+   () => {
+     if (typeof href !== 'string') return href
+     return isInternalLink ? (resolveLink(href) ?? href) : href
+   },
    [href, isInternalLink, resolveLink]
  )

Also applies to: line 40 (finalHref passed to NextLink)

🤖 Prompt for AI Agents
In `@packages/core/src/components/ui/Link/Link.tsx` around lines 20 - 30, The code
indexes and treats href as a string (href[0], href[1], resolveLink(href)) but
Next.js LinkProps allows UrlObject; add a type guard so you only index/resolve
when typeof href === 'string' (e.g., compute isInternalLink using typeof href
=== 'string' && href.startsWith('/') && href[1] !== '/'), and compute finalHref
by calling resolveLink(href) only when href is a string (otherwise leave
finalHref as the original href UrlObject); also ensure the value passed to
NextLink uses this guarded finalHref.

Copy link
Contributor

@hellofanny hellofanny left a comment

Choose a reason for hiding this comment

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

Nice! 🚀
I’m approving this.
I’ve tested the common scenario, and the routing works as expected.
We can fine-tune the middleware later as we try additional scenarios.

Maybe we can address the other link resolvers in a separate PR.

Image

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.

4 participants