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¶
- Estructura del Navbar
- Sistema de Enrutamiento
- Middleware de Regiones
- Patrones de Enlaces
- Manejo de Parámetros
- Grupos de Rutas
- Problemas Identificados
- 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¶
LocalizedClientLink¶
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 │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘