Saltar a contenido

Vigencia por confirmar

Documento preexistente migrado al estándar docs-standard el 2026-08-19 sin reescribirlo. Su contenido no fue verificado contra el código actual: puede describir un estado anterior del sistema. Requiere revisión humana.

Navegación del Navbar y Sistema de Enrutamiento

Este documento describe la arquitectura de navegación del navbar, el sistema de enrutamiento basado en regiones y los patrones de navegación implementados en el storefront.

Tabla de Contenidos

  1. Estructura del Navbar
  2. Sistema de Enrutamiento
  3. Middleware de Regiones
  4. Patrones de Enlaces
  5. Manejo de Parámetros
  6. Grupos de Rutas
  7. Problemas Identificados
  8. Estándares de Implementación

Estructura del Navbar

Jerarquía de Componentes

Nav (Server Component)
├── Row 1: Header Principal
│   ├── SideMenu (Client) - Menú hamburguesa móvil
│   ├── Logo (LocalizedClientLink)
│   ├── SearchBar (Client) - Búsqueda Algolia
│   ├── Account Link (LocalizedClientLink)
│   └── CartButton (Server)
│       └── CartDropdown (Client)
│
├── Row 2: SearchBar Móvil
│   └── SearchBar (visible solo en móvil)
│
└── Row 3: Navegación Horizontal
    └── HorizontalNav (Client)
        └── Category Links (dinámicos desde Payload CMS)

Archivos Principales

Archivo Tipo Propósito
src/modules/layout/templates/nav/index.tsx Server Componente raíz del navbar
src/modules/layout/templates/horizontal-nav/index.tsx Client Navegación de categorías
src/modules/layout/components/cart-dropdown/index.tsx Client Dropdown del carrito
src/modules/layout/components/side-menu/index.tsx Client Menú lateral móvil
src/modules/layout/components/country-select/index.tsx Client Selector de región
src/modules/common/components/localized-client-link/index.tsx Client Link con country code

Flujo de Datos

┌─────────────────────────────────────────────────────────────┐
│                        Nav (Server)                          │
│  ┌─────────────────┐    ┌─────────────────┐                 │
│  │  listRegions()  │    │ getMainNavigation() │             │
│  │  (Medusa API)   │    │   (Payload CMS)     │             │
│  └────────┬────────┘    └─────────┬───────────┘             │
│           │                       │                          │
│           ▼                       ▼                          │
│  ┌────────────────────────────────────────────┐             │
│  │           processNavItems(navItems)         │             │
│  └────────────────────────────────────────────┘             │
│           │                       │                          │
│           ▼                       ▼                          │
│  ┌─────────────┐          ┌─────────────────┐               │
│  │  SideMenu   │          │  HorizontalNav  │               │
│  │  (Client)   │          │    (Client)     │               │
│  └─────────────┘          └─────────────────┘               │
└─────────────────────────────────────────────────────────────┘

Sistema de Enrutamiento

Estructura de URLs

El storefront utiliza un patrón de URL basado en códigos de país:

/{countryCode}/{route}

Ejemplos:
/py/                     → Homepage Paraguay
/py/products/celular-x   → Producto específico
/py/store?q=samsung      → Tienda con búsqueda
/py/cart                 → Carrito
/py/account              → Cuenta de usuario
/py/checkout             → Proceso de compra

Segmentos Dinámicos

Segmento Patrón Propósito
[countryCode] /py, /ar, /br Código ISO de país
[handle] /products/[handle] Slug del producto
[[...variantId]] /products/[handle]/[[...variantId]] Variante opcional
[...category] /categories/[...category] Categorías anidadas
[id] /order/[id] ID de orden

Árbol de Rutas

src/app/
└── [countryCode]/
    ├── (main)/                    # Route Group Principal
    │   ├── layout.tsx            # Layout con Nav + Footer
    │   ├── page.tsx              # Homepage
    │   ├── products/
    │   │   └── [handle]/
    │   │       └── [[...variantId]]/
    │   │           └── page.tsx
    │   ├── store/
    │   │   └── page.tsx
    │   ├── cart/
    │   │   └── page.tsx
    │   ├── collections/
    │   │   └── [handle]/
    │   │       └── page.tsx
    │   ├── categories/
    │   │   └── [...category]/
    │   │       └── page.tsx
    │   └── account/
    │       ├── layout.tsx        # Maneja parallel routes
    │       ├── @dashboard/       # Usuarios autenticados
    │       │   ├── page.tsx
    │       │   ├── profile/
    │       │   ├── addresses/
    │       │   └── orders/
    │       └── @login/           # Usuarios anónimos
    │           └── page.tsx
    │
    └── (checkout)/               # Route Group Checkout
        ├── layout.tsx           # Layout simplificado
        └── checkout/
            └── page.tsx

Middleware de Regiones

Archivo: src/middleware.ts

El middleware intercepta todas las requests para: 1. Detectar/validar el código de país 2. Obtener mapa de regiones de Medusa 3. Establecer cookies de caché 4. Redirigir si es necesario

Flujo del Middleware

Request Entrante
       │
       ▼
┌──────────────────────────┐
│ ¿Es asset estático?      │──────Yes──────► NextResponse.next()
│ (tiene extensión de      │
│ archivo)                 │
└───────────┬──────────────┘
            │ No
            ▼
┌──────────────────────────┐
│ Obtener regionMap        │
│ (cached 1 hora)          │
│ de Medusa Backend        │
└───────────┬──────────────┘
            │
            ▼
┌──────────────────────────┐
│ Extraer countryCode      │
│ de URL path              │
└───────────┬──────────────┘
            │
            ▼
┌──────────────────────────┐
│ ¿countryCode válido?     │──────No───────► Detectar país
│                          │                  (header Vercel o
└───────────┬──────────────┘                  DEFAULT_REGION)
            │ Yes                                    │
            ▼                                        ▼
┌──────────────────────────┐        ┌──────────────────────────┐
│ ¿Tiene cookie            │        │ Redirect 307 a           │
│ _medusa_cache_id?        │        │ /{countryCode}{path}     │
└───────────┬──────────────┘        └──────────────────────────┘
            │
    Yes     │     No
     │      │      │
     ▼      │      ▼
   next()   │  Set cookie +
            │  redirect
            ▼
      NextResponse.next()

Funciones Clave

// Obtener mapa de regiones (cacheado)
async function getRegionMap(cacheId: string): Promise<Map<string, Region>> {
  // Cache en memoria por 1 hora
  // Fetches de MEDUSA_BACKEND_URL/store/regions
}

// Extraer country code de la request
function getCountryCode(
  request: NextRequest,
  regionMap: Map<string, Region>
): string | undefined {
  // 1. Intenta extraer de URL
  // 2. Fallback a header Vercel (x-vercel-ip-country)
  // 3. Fallback a DEFAULT_REGION
}

Cookies Utilizadas

Cookie Propósito Duración
_medusa_cache_id Identificador para cache por usuario Session
_medusa_jwt Token de autenticación HTTP-only
_medusa_cart_id ID del carrito activo HTTP-only

Patrones de Enlaces

Archivo: src/modules/common/components/localized-client-link/index.tsx

Componente wrapper que automáticamente agrega el country code a los enlaces:

"use client"

import Link from "next/link"
import { useParams } from "next/navigation"

interface LocalizedClientLinkProps {
  href: string
  children: React.ReactNode
  // ... otras props de Next Link
}

export default function LocalizedClientLink({
  href,
  children,
  ...props
}: LocalizedClientLinkProps) {
  const { countryCode } = useParams()

  return (
    <Link href={`/${countryCode}${href}`} {...props}>
      {children}
    </Link>
  )
}

Uso Correcto

// ✅ CORRECTO - Usa LocalizedClientLink
<LocalizedClientLink href="/products/my-product">
  Ver Producto
</LocalizedClientLink>
// Output: /py/products/my-product (si countryCode = "py")

// ✅ CORRECTO - Link manual con countryCode
const { countryCode } = useParams()
<Link href={`/${countryCode}/store?category=electronics`}>
  Electrónica
</Link>

// ❌ INCORRECTO - Sin country code
<Link href="/products/my-product">
  Ver Producto
</Link>
// Esto romperá el routing regional

Patrones de Navegación Programática

"use client"

import { useParams, useRouter } from "next/navigation"

function SearchComponent() {
  const { countryCode } = useParams<{ countryCode: string }>()
  const router = useRouter()

  // ✅ CORRECTO
  const handleSearch = (query: string) => {
    router.push(`/${countryCode}/store?q=${encodeURIComponent(query)}`)
  }

  // ❌ INCORRECTO - Falta countryCode
  const handleSearchBad = (query: string) => {
    router.push(`/store?q=${query}`)
  }
}

Manejo de Parámetros

Parámetros de Ruta (Path Parameters)

// En Server Component
interface PageProps {
  params: Promise<{
    countryCode: string
    handle?: string
    category?: string[]
  }>
}

export default async function ProductPage({ params }: PageProps) {
  const { countryCode, handle } = await params
  // ...
}

// En Client Component
"use client"
import { useParams } from "next/navigation"

function ProductClient() {
  const { countryCode, handle } = useParams<{
    countryCode: string
    handle: string
  }>()
  // ...
}

Parámetros de Búsqueda (Query Parameters)

// En Server Component
interface PageProps {
  searchParams: Promise<{
    q?: string
    category?: string
    brand?: string
    page?: string
  }>
}

export default async function StorePage({ searchParams }: PageProps) {
  const params = await searchParams
  const query = params.q || ""
  const page = Number(params.page) || 1
  // ...
}

// En Client Component
"use client"
import { useSearchParams } from "next/navigation"

function StoreClient() {
  const searchParams = useSearchParams()
  const query = searchParams.get("q") || ""
  const category = searchParams.get("category")
  // ...
}

Construcción de URLs con Parámetros

// Utilidad para construir URLs
function buildStoreUrl(
  countryCode: string,
  params: {
    q?: string
    category?: string
    brand?: string
    price_min?: number
    price_max?: number
  }
): string {
  const url = new URL(`/${countryCode}/store`, window.location.origin)

  if (params.q) url.searchParams.set("q", params.q)
  if (params.category) url.searchParams.set("category", params.category)
  if (params.brand) url.searchParams.set("brand", params.brand)
  if (params.price_min) url.searchParams.set("price_min", String(params.price_min))
  if (params.price_max) url.searchParams.set("price_max", String(params.price_max))

  return url.pathname + url.search
}

Grupos de Rutas

Route Group (main)

Layout: src/app/[countryCode]/(main)/layout.tsx

export default function MainLayout({ children }) {
  return (
    <>
      <Nav />
      <CartMismatchBanner />
      <FreeShippingNudge />
      <main>{children}</main>
      <Footer />
    </>
  )
}

Características: - Navbar completo con búsqueda - Footer con enlaces y newsletter - Banners promocionales - Contenedor principal para contenido

Route Group (checkout)

Layout: src/app/[countryCode]/(checkout)/layout.tsx

export default function CheckoutLayout({ children }) {
  return (
    <div className="checkout-wrapper">
      <CheckoutNav />
      <main>{children}</main>
    </div>
  )
}

Características: - Navbar simplificado (solo logo y volver) - Sin footer - Enfoque en el proceso de compra - Menos distracciones

Parallel Routes (Account)

Layout: src/app/[countryCode]/(main)/account/layout.tsx

interface AccountLayoutProps {
  dashboard: React.ReactNode  // @dashboard slot
  login: React.ReactNode      // @login slot
}

export default async function AccountLayout({
  dashboard,
  login
}: AccountLayoutProps) {
  const customer = await retrieveCustomer()

  return (
    <div className="account-container">
      {customer ? dashboard : login}
    </div>
  )
}

Comportamiento: - @dashboard/ → Se muestra si hay cliente autenticado - @login/ → Se muestra si no hay sesión - Misma URL (/account) para ambos estados


Problemas Identificados

Problema #1: SearchBar No Incluye Country Code (CRÍTICO)

Archivo: src/modules/search/component/SearchBar.tsx

// ❌ ACTUAL - Sin country code
onSubmit: (event, { state }) => {
  router.push(`/store?q=${state.query}`)
}

// También afecta:
// - Click en sugerencias
// - Click en categorías
// - Click en productos
// - Botón de limpiar

Impacto: El usuario pierde contexto regional al buscar.

Solución:

"use client"
import { useParams, useRouter } from "next/navigation"

function SearchBar() {
  const { countryCode } = useParams<{ countryCode: string }>()
  const router = useRouter()

  // ✅ CORRECTO
  onSubmit: (event, { state }) => {
    router.push(`/${countryCode}/store?q=${state.query}`)
  }
}

Problema #2: Homepage Restringida a Países Específicos

Archivo: src/app/[countryCode]/(main)/page.tsx

const ALLOWED = ["py", "ar", "br"] as const

export default async function Home({ params }) {
  const { countryCode } = await params

  if (!ALLOWED.includes(countryCode)) {
    notFound()  // ❌ Retorna 404 para otros países
  }
  // ...
}

Impacto: Usuarios de otras regiones (ej: US, UK) reciben 404 en la homepage.

Solución: Definir estrategia clara: - Opción A: Permitir todos los países configurados en Medusa - Opción B: Redirigir países no soportados a una landing específica - Opción C: Mostrar mensaje de "región no disponible"


Problema #3: Fallback del CartButton Estático

Archivo: src/modules/layout/templates/nav/index.tsx

function CartButtonFallback() {
  return (
    <LocalizedClientLink href="/cart">
      <span>0</span>  // ❌ Siempre muestra 0
    </LocalizedClientLink>
  )
}

Impacto: Durante la carga, siempre muestra "0" items aunque el usuario tenga productos.

Solución: Usar skeleton o spinner en lugar de valor hardcodeado.


Problema #4: Servicio de Navegación Sin Fallback

Archivo: src/lib/services/navigation-service.ts

export async function getMainNavigation(): Promise<NavItem[]> {
  try {
    const response = await fetch(`${PAYLOAD_URL}/api/navigation`)
    // ...
  } catch (error) {
    console.error("Error fetching navigation")
    return []  // ❌ Retorna array vacío silenciosamente
  }
}

Impacto: Si Payload CMS falla, el navbar queda sin categorías.

Solución: Implementar navegación fallback hardcodeada.


Problema #5: Inconsistencia en updateRegion

Archivo: src/lib/data/cart.ts

export async function updateRegion(countryCode: string, currentPath: string) {
  // Actualiza carrito con nueva región
  // Revalida caches
  // Redirige a nueva URL

  redirect(`/${countryCode}${currentPath}`)
}

Potencial Problema: Si currentPath no empieza con /, genera URL inválida.

Validación necesaria:

export async function updateRegion(countryCode: string, currentPath: string) {
  const path = currentPath.startsWith("/") ? currentPath : `/${currentPath}`
  // ...
  redirect(`/${countryCode}${path}`)
}

Estándares de Implementación

1. Enlaces Internos

// ✅ SIEMPRE usar LocalizedClientLink para enlaces internos
import LocalizedClientLink from "@modules/common/components/localized-client-link"

<LocalizedClientLink href="/products/my-product">
  Ver Producto
</LocalizedClientLink>

2. Navegación Programática

// ✅ SIEMPRE incluir countryCode en router.push
"use client"

import { useParams, useRouter } from "next/navigation"

function MyComponent() {
  const { countryCode } = useParams<{ countryCode: string }>()
  const router = useRouter()

  const navigate = (path: string) => {
    router.push(`/${countryCode}${path}`)
  }
}

3. Server Components con Params

// ✅ Await params en Server Components (Next.js 15)
interface PageProps {
  params: Promise<{ countryCode: string }>
}

export default async function MyPage({ params }: PageProps) {
  const { countryCode } = await params
  // ...
}

4. Generación de URLs

// ✅ Utilidad centralizada para construcción de URLs
// src/lib/utils/url.ts

export function buildUrl(
  countryCode: string,
  path: string,
  params?: Record<string, string | number | undefined>
): string {
  const base = `/${countryCode}${path.startsWith("/") ? path : `/${path}`}`

  if (!params) return base

  const searchParams = new URLSearchParams()
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined) {
      searchParams.set(key, String(value))
    }
  })

  const query = searchParams.toString()
  return query ? `${base}?${query}` : base
}

// Uso:
const url = buildUrl("py", "/store", { q: "samsung", category: "phones" })
// → "/py/store?q=samsung&category=phones"

5. Validación de Country Code

// ✅ Validar countryCode antes de usar
import { listRegions } from "@lib/data/regions"

export async function validateCountryCode(
  countryCode: string
): Promise<boolean> {
  const regions = await listRegions()
  const validCodes = regions.flatMap(r =>
    r.countries?.map(c => c.iso_2) || []
  )
  return validCodes.includes(countryCode.toLowerCase())
}

6. Manejo de Parallel Routes

// ✅ Estructura de parallel routes
// src/app/[countryCode]/(main)/account/
// ├── layout.tsx          → Decide qué slot mostrar
// ├── @dashboard/
// │   └── page.tsx        → Usuario autenticado
// └── @login/
//     └── page.tsx        → Usuario anónimo

// layout.tsx
interface LayoutProps {
  dashboard: React.ReactNode
  login: React.ReactNode
}

export default async function Layout({ dashboard, login }: LayoutProps) {
  const customer = await retrieveCustomer()
  return customer ? dashboard : login
}

Diagrama de Flujo de Navegación

┌─────────────────────────────────────────────────────────────────────┐
│                         REQUEST FLOW                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Usuario → /store?q=samsung                                          │
│              │                                                       │
│              ▼                                                       │
│  ┌─────────────────────────────┐                                    │
│  │       Middleware            │                                    │
│  │  - Detecta sin countryCode  │                                    │
│  │  - Obtiene región default   │                                    │
│  │  - Redirect 307             │                                    │
│  └─────────────┬───────────────┘                                    │
│                │                                                     │
│                ▼                                                     │
│  Usuario → /py/store?q=samsung                                       │
│              │                                                       │
│              ▼                                                       │
│  ┌─────────────────────────────┐                                    │
│  │    Route Resolution         │                                    │
│  │  [countryCode] = "py"       │                                    │
│  │  (main) route group         │                                    │
│  │  /store/page.tsx            │                                    │
│  └─────────────┬───────────────┘                                    │
│                │                                                     │
│                ▼                                                     │
│  ┌─────────────────────────────┐                                    │
│  │      Layout Chain           │                                    │
│  │  1. Root Layout             │                                    │
│  │  2. [countryCode] Layout    │                                    │
│  │  3. (main) Layout → Nav     │                                    │
│  │  4. Store Page              │                                    │
│  └─────────────────────────────┘                                    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Referencias