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.

Integración con Algolia e InstantSearch

Este documento describe el estado actual de la integración con Algolia, el uso de InstantSearch y los patrones de navegación implementados en el storefront.

Tabla de Contenidos

  1. Configuración de Algolia
  2. Arquitectura de Búsqueda
  3. Implementación de InstantSearch
  4. Componentes de Búsqueda
  5. Parámetros de URL y Navegación
  6. Funciones de API Server-Side
  7. Problemas Identificados
  8. Estándares de Implementación

Configuración de Algolia

Variables de Entorno

Las siguientes variables deben estar configuradas en .env.local:

NEXT_PUBLIC_ALGOLIA_APP_ID=<app_id>
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=<search_key>
NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX=product_items_view_index_dev
NEXT_PUBLIC_ALGOLIA_SUGGESTIONS_INDEX=product_items_view_index_dev_query_suggestions
ALGOLIA_ADMIN_API_KEY=<admin_key>  # Solo para operaciones del servidor

Dependencias

{
  "algoliasearch": "^5.45.0",
  "react-instantsearch": "^7.20.0",
  "react-instantsearch-nextjs": "^1.0.6",
  "react-instantsearch-router-nextjs": "^7.20.0",
  "@algolia/autocomplete-js": "^1.19.4",
  "@algolia/autocomplete-theme-classic": "^1.19.4"
}

Archivos de Inicialización del Cliente

Archivo Propósito Paquete Usado
src/lib/algolia.ts Cliente para operaciones server-side algoliasearch (completo)
src/lib/search-client.ts Cliente para componentes client-side algoliasearch/lite

Arquitectura de Búsqueda

El storefront implementa dos enfoques paralelos para la búsqueda:

1. InstantSearch (Moderno - Client-Side)

Usuario → SearchBar (Autocomplete) → InstantSearch Provider → Algolia API
                                            ↓
                                    InstantSearchContent
                                            ↓
                                    Facets, Hits, Filters

Archivos principales: - src/modules/store/templates/instantsearch-store-template.tsx - src/modules/store/components/instantsearch-content/index.tsx - src/modules/search/component/SearchBar.tsx

2. API Directa (Legacy - Server-Side)

Page Request → Server Action → algolia-products.ts → Algolia API
                                      ↓
                               AlgoliaProductGrid

Archivos principales: - src/lib/algolia-products.ts - src/modules/store/templates/algolia-store-template.tsx - src/modules/categories/templates/algolia-category-template.tsx


Implementación de InstantSearch

Configuración del Provider

Archivo: src/modules/store/templates/instantsearch-store-template.tsx

import { InstantSearchNext } from "react-instantsearch-nextjs"
import { liteClient as algoliasearch } from "algoliasearch/lite"

const searchClient = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
)

export default function InstantSearchStoreTemplate({ initialFilters }) {
  return (
    <InstantSearchNext
      searchClient={searchClient}
      indexName="product_items_view_index_dev"
      routing={false}  // Routing manual habilitado
    >
      <Configure filters="total_stock > 0" />
      <InstantSearchContent initialFilters={initialFilters} />
    </InstantSearchNext>
  )
}

Hooks de InstantSearch Utilizados

Hook Propósito Componente
useInstantSearch() Acceso al estado global InstantSearchContent
useInfiniteHits() Scroll infinito de productos ProductHits
useStats() Conteo de resultados Header de resultados
useRefinementList() Filtros por facetas NativeRefinementList
useRange() Rango de precios RangeSlider
useClearRefinements() Limpiar filtros Botón "Limpiar"
useCurrentRefinements() Filtros activos ActiveFilters
useDynamicWidgets() Facetas dinámicas DynamicFacets

Facetas Configuradas

const FACET_LABELS: Record<string, string> = {
  category: "Categoría",
  brand: "Marca",
  product_tags: "Tags",
  _collections: "Colecciones",
  price: "Precio"
}

Componentes de Búsqueda

SearchBar (Autocomplete)

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

El SearchBar implementa autocompletado con múltiples fuentes:

// Fuentes de datos
const sources = [
  {
    sourceId: "querySuggestionsSource",
    // Sugerencias de búsqueda del índice de suggestions
  },
  {
    sourceId: "categorySuggestionsSource",
    // Búsqueda en facetas de categorías
  },
  {
    sourceId: "products",
    // Productos directos del índice principal
  }
]

Flujos de navegación:

Acción Destino
Enter en búsqueda /store?q={query}
Click en sugerencia /store?q={suggestion}
Click en categoría /store?category={category}
Click en producto /products/{sku}
Botón limpiar /store

ProductHits (Resultados)

Archivo: src/modules/store/components/instantsearch-content/index.tsx

Implementa scroll infinito con IntersectionObserver:

function ProductHits() {
  const { hits, showMore, isLastPage } = useInfiniteHits()
  const sentinelRef = useRef(null)

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && !isLastPage) {
        showMore()
      }
    })

    if (sentinelRef.current) {
      observer.observe(sentinelRef.current)
    }

    return () => observer.disconnect()
  }, [showMore, isLastPage])

  return (
    <>
      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
        {hits.map(hit => <ProductCard key={hit.objectID} hit={hit} />)}
      </div>
      <div ref={sentinelRef} />
    </>
  )
}

Parámetros de URL y Navegación

Estructura de URLs

/{countryCode}/store[?params]

Parámetros soportados:
- q            → Texto de búsqueda
- category    → Filtro de categoría
- brand       → Filtro de marca
- product_tag → Filtro de etiqueta
- collection  → Filtro de colección
- price_min   → Precio mínimo
- price_max   → Precio máximo

Ejemplos de URLs

/py/store                                    # Tienda sin filtros
/py/store?q=celular                          # Búsqueda por texto
/py/store?category=Celulares                 # Filtro por categoría
/py/store?brand=Samsung&price_max=5000000    # Combinación de filtros
/py/store?category=Celulares&brand=Apple&q=iphone  # Múltiples filtros

Sincronización URL ↔ InstantSearch

Componente: ApplyFiltersFromURL

function ApplyFiltersFromURL({ initialFilters }) {
  const { setIndexUiState } = useInstantSearch()

  useEffect(() => {
    const refinementList: Record<string, string[]> = {}

    if (initialFilters.category) {
      refinementList.category = [initialFilters.category]
    }
    if (initialFilters.brand) {
      refinementList.brand = [initialFilters.brand]
    }
    // ... otros filtros

    setIndexUiState({ refinementList })
  }, [initialFilters])
}

Funciones de API Server-Side

Archivo: src/lib/algolia-products.ts

Funciones Principales

Función Propósito Parámetros
listProducts(limit) Lista básica de productos limit: número
searchProducts(filterType, filterValue, limit) Búsqueda con filtro único tipo, valor, límite
searchProductsByCategory(name, page, hitsPerPage, sortBy) Búsqueda paginada por categoría múltiples
searchProductsByTag(tag, page, hitsPerPage, sortBy) Búsqueda paginada por tag múltiples
searchProductsWithQuery(query, page, hitsPerPage, sortBy, filters) Búsqueda con query de texto múltiples
searchProductsWithFilters(options) Búsqueda completa con facetas objeto de opciones

Ejemplo de Uso

// En un Server Component
import { searchProductsWithFilters } from "@lib/algolia-products"

export default async function StorePage({ searchParams }) {
  const { products, facets, totalHits } = await searchProductsWithFilters({
    query: searchParams.q,
    category: searchParams.category,
    brand: searchParams.brand,
    priceMin: searchParams.price_min,
    priceMax: searchParams.price_max,
    page: 1,
    hitsPerPage: 24
  })

  return <ProductGrid products={products} />
}

Tipos de Datos

interface GridCard {
  id: string
  title: string
  handle: string
  thumbnail: string | null
  sku?: string
  category?: string
  brand?: string | null
  price?: number
  regular_price?: string
  special_price?: string
  price_type?: "REGULAR" | "OFERTA"
  in_stock?: boolean
  total_stock?: number
  on_sale?: boolean
  product_tags?: string[]
}

interface PaginatedResult {
  products: GridCard[]
  totalHits: number
  totalPages: number
  currentPage: number
  hitsPerPage: number
  facets?: {
    categories: FacetValue[]
    brands: FacetValue[]
    tags: FacetValue[]
    priceRange: { min: number; max: number }
  }
}

Problemas Identificados

Problema #1: Variable de Entorno Incorrecta (CRÍTICO)

Archivo: src/lib/search-client.ts

// ❌ INCORRECTO - La variable no existe
const searchClient = liteClient(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.NEXT_PUBLIC_ALGOLIA_API_KEY!  // Esta variable NO existe
)

// ✅ CORRECTO - Usar la variable definida
const searchClient = liteClient(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!  // Esta es la correcta
)

Impacto: El componente SearchModal que importa este archivo fallará en runtime.


Problema #2: Falta Country Code en Navegación de Búsqueda (CRÍTICO)

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

// ❌ INCORRECTO - No incluye countryCode
router.push(`/store?q=${query}`)
router.push(`/products/${sku}`)

// ✅ CORRECTO - Debe incluir countryCode
const { countryCode } = useParams()
router.push(`/${countryCode}/store?q=${query}`)
router.push(`/${countryCode}/products/${sku}`)

Impacto: Los usuarios pierden el contexto de región al realizar búsquedas.


Problema #3: Routing de InstantSearch Deshabilitado

Archivo: src/modules/store/templates/instantsearch-store-template.tsx

<InstantSearchNext
  routing={false}  // Routing nativo deshabilitado
>

Impacto: La sincronización URL ↔ Estado se maneja manualmente con ApplyFiltersFromURL, lo cual es menos eficiente y propenso a errores.

Recomendación: Habilitar el routing nativo de InstantSearch:

import { history } from "instantsearch.js/es/lib/routers"
import { simple } from "instantsearch.js/es/lib/stateMappings"

<InstantSearchNext
  routing={{
    router: history({
      getLocation: () => window.location
    }),
    stateMapping: simple()
  }}
>

Problema #4: Duplicación de Clientes de Búsqueda

Existen tres instancias del cliente Algolia: 1. src/lib/algolia.ts → Server-side (algoliasearch completo) 2. src/lib/search-client.ts → Client-side (algoliasearch/lite) 3. instantsearch-store-template.tsx → Inline (algoliasearch/lite)

Recomendación: Consolidar en dos archivos únicos: - src/lib/algolia/server.ts → Cliente server-side - src/lib/algolia/client.ts → Cliente client-side (exportar singleton)


Problema #5: Índice Hardcodeado en Múltiples Lugares

// En algolia.ts
export const ALGOLIA_INDEX = process.env.NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX!

// En instantsearch-store-template.tsx
indexName="product_items_view_index_dev"  // ❌ Hardcodeado

// En parse-search-params.ts
const indexName = "product_items_view_index_dev"  // ❌ Hardcodeado

Recomendación: Usar siempre la constante ALGOLIA_INDEX o la variable de entorno.


Problema #6: Transformación de URLs de Imágenes Específica

function getCloudflareThumbUrl(url: string): string {
  if (url.includes("imagedelivery.net")) {
    return url.replace(/\/[^/]+$/, "/thumbnail")
  }
  return url
}

Impacto: Asume estructura específica de Cloudflare que puede no existir en todos los entornos.


Estándares de Implementación

1. Inicialización del Cliente

// ✅ CORRECTO - src/lib/algolia/client.ts
import { liteClient as algoliasearch } from "algoliasearch/lite"

const ALGOLIA_APP_ID = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!
const ALGOLIA_SEARCH_KEY = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!

export const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_KEY)
export const ALGOLIA_INDEX = process.env.NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX!

2. Uso de InstantSearch en Componentes

// ✅ CORRECTO - Componente con InstantSearch
"use client"

import { InstantSearchNext } from "react-instantsearch-nextjs"
import { searchClient, ALGOLIA_INDEX } from "@lib/algolia/client"

export function SearchProvider({ children }) {
  return (
    <InstantSearchNext
      searchClient={searchClient}
      indexName={ALGOLIA_INDEX}
      future={{ preserveSharedStateOnUnmount: true }}
    >
      <Configure filters="total_stock > 0" />
      {children}
    </InstantSearchNext>
  )
}

3. Navegación con Country Code

// ✅ CORRECTO - Siempre incluir countryCode
"use client"

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

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

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

  const handleProductClick = (handle: string) => {
    router.push(`/${countryCode}/products/${handle}`)
  }
}

4. Manejo de Filtros en URL

// ✅ CORRECTO - Parsear y aplicar filtros desde URL
interface StorePageProps {
  searchParams: Promise<{
    q?: string
    category?: string
    brand?: string
    product_tag?: string
    price_min?: string
    price_max?: string
  }>
}

export default async function StorePage({ searchParams }: StorePageProps) {
  const params = await searchParams

  return (
    <InstantSearchStoreTemplate
      initialFilters={{
        query: params.q,
        category: params.category,
        brand: params.brand,
        tag: params.product_tag,
        priceMin: params.price_min ? Number(params.price_min) : undefined,
        priceMax: params.price_max ? Number(params.price_max) : undefined,
      }}
    />
  )
}

5. Server Actions para Búsqueda

// ✅ CORRECTO - Server Action con tipado
"use server"

import { searchClient, ALGOLIA_INDEX } from "@lib/algolia/server"

export async function searchProducts(
  query: string,
  options: {
    page?: number
    hitsPerPage?: number
    filters?: string
  } = {}
) {
  const { results } = await searchClient.search([
    {
      indexName: ALGOLIA_INDEX,
      query,
      params: {
        page: options.page ?? 0,
        hitsPerPage: options.hitsPerPage ?? 24,
        filters: `total_stock > 0${options.filters ? ` AND ${options.filters}` : ""}`,
        facets: ["category", "brand", "product_tags", "price"],
      },
    },
  ])

  return results[0]
}

Resumen de Acciones Requeridas

Prioridad Acción Archivo(s)
Alta Corregir variable NEXT_PUBLIC_ALGOLIA_API_KEY → NEXT_PUBLIC_ALGOLIA_SEARCH_KEY search-client.ts
Alta Agregar countryCode a todas las rutas de navegación en SearchBar SearchBar.tsx
Media Consolidar clientes de Algolia en archivos únicos algolia.ts, search-client.ts
Media Usar constante ALGOLIA_INDEX en lugar de valores hardcodeados Múltiples archivos
Baja Evaluar habilitar routing nativo de InstantSearch instantsearch-store-template.tsx
Baja Documentar estructura esperada de URLs de imágenes instantsearch-content/index.tsx

Referencias