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¶
- Configuración de Algolia
- Arquitectura de Búsqueda
- Implementación de InstantSearch
- Componentes de Búsqueda
- Parámetros de URL y Navegación
- Funciones de API Server-Side
- Problemas Identificados
- 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 |