@jsm406/medusa-products-related
v1.0.5
Published
Muestra productos relacionados basados en categoría, tags, vendor y type
Maintainers
Readme
@jsm406/medusa-products-related
Plugin de Medusa v2 que muestra productos relacionados basados en categoría, tags, vendor, tipo y colección.
Características
- Configurable desde Admin: Habilita/deshabilita cada tipo de relación y configura límites
- Múltiples criterios: Categoría, tags, vendor, tipo y colección
- API Store: Endpoint para obtener productos relacionados con datos completos
- Admin UI: Interfaz de administración para configurar las relaciones
- Ordenación por relevancia: Los productos se ordenan según el tipo de relación
- Compatible con Medusa 2.17.2+
Instalación
npm install @jsm406/medusa-products-relatedConfiguración
Agrega el plugin en tu medusa-config.ts:
module.exports = defineConfig({
// ...
plugins: [
{
resolve: "@jsm406/medusa-products-related",
options: {},
},
],
})Migraciones
Después de instalar el plugin, ejecuta las migraciones:
npx medusa db:migrateUso
API Store
Obtener productos relacionados para un producto específico:
GET /store/products-related/{product_id}Respuesta:
{
"products_related": [
{
"id": "prod_123",
"title": "Producto Relacionado",
"handle": "producto-relacionado",
"thumbnail": "https://...",
"status": "published",
"related_by": "category",
"relevance_score": 4
}
],
"config": {
"enabled": true,
"category_enabled": true,
"category_limit": 4,
"tag_enabled": true,
"tag_limit": 4,
"vendor_enabled": false,
"vendor_limit": 4,
"type_enabled": false,
"type_limit": 4,
"collection_enabled": false,
"collection_limit": 4
}
}API Admin
Obtener configuración:
GET /admin/products-relatedActualizar configuración:
POST /admin/products-related
Content-Type: application/json
{
"enabled": true,
"category_enabled": true,
"category_limit": 6,
"tag_enabled": true,
"tag_limit": 4,
"vendor_enabled": false,
"vendor_limit": 4,
"type_enabled": false,
"type_limit": 4,
"collection_enabled": true,
"collection_limit": 4
}Admin UI
Accede a la interfaz de administración en:
/admin/products-relatedLa interfaz permite:
- Habilitar/deshabilitar el plugin completo
- Habilitar/deshabilitar cada tipo de relación
- Configurar el límite de productos por cada tipo
Uso en el Storefront
Ejemplo básico con fetch
// Función para obtener productos relacionados
async function getProductsRelated(productId: string) {
const response = await fetch(
`${MEDUSA_BACKEND_URL}/store/products-related/${productId}`,
{
headers: {
"x-publishable-api-key": MEDUSA_PUBLISHABLE_KEY,
},
}
)
if (!response.ok) {
throw new Error("Error al obtener productos relacionados")
}
const data = await response.json()
return data.products_related
}Componente React/Next.js (App Router)
// app/components/products-related.tsx
import { getProductsRelated } from "@/lib/medusa"
import Image from "next/image"
import Link from "next/link"
type RelatedProduct = {
id: string
title: string
handle: string
thumbnail: string | null
related_by: "category" | "tag" | "vendor" | "type" | "collection"
relevance_score: number
}
export default async function ProductsRelated({
productId
}: {
productId: string
}) {
const products = await getProductsRelated(productId)
if (!products || products.length === 0) {
return null
}
return (
<section className="py-8">
<h2 className="text-2xl font-bold mb-6">Productos Relacionados</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{products.map((product: RelatedProduct) => (
<Link
key={product.id}
href={`/products/${product.handle}`}
className="group"
>
<div className="border rounded-lg p-4 hover:shadow-lg transition-shadow">
{product.thumbnail && (
<div className="relative aspect-square mb-3">
<Image
src={product.thumbnail}
alt={product.title}
fill
className="object-cover rounded"
/>
</div>
)}
<h3 className="font-medium text-sm line-clamp-2 group-hover:text-primary">
{product.title}
</h3>
<span className="inline-block mt-2 px-2 py-1 text-xs bg-gray-100 rounded">
{product.related_by === "category" && "Misma categoría"}
{product.related_by === "tag" && "Mismo tag"}
{product.related_by === "vendor" && "Mismo vendor"}
{product.related_by === "type" && "Mismo tipo"}
{product.related_by === "collection" && "Misma colección"}
</span>
</div>
</Link>
))}
</div>
</section>
)
}Integración en la página de producto
// app/products/[handle]/page.tsx
import { getProductByHandle } from "@/lib/medusa"
import ProductsRelated from "@/components/products-related"
export default async function ProductPage({
params
}: {
params: { handle: string }
}) {
const product = await getProductByHandle(params.handle)
return (
<div className="container mx-auto px-4 py-8">
{/* Contenido del producto */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Galería de imágenes, detalles, etc. */}
</div>
{/* Productos relacionados */}
<ProductsRelated productId={product.id} />
{/* Productos comprados juntos (si tienes ese plugin) */}
{/* <ProductsBoughtTogether productId={product.id} /> */}
</div>
)
}Ejemplo con Medusa SDK
// lib/medusa.ts
import Medusa from "@medusajs/medusa-js"
const medusa = new Medusa({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
maxRetries: 3,
publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
export async function getProductsRelated(productId: string) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL}/store/products-related/${productId}`,
{
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,
},
next: { revalidate: 3600 }, // Cache por 1 hora
}
)
if (!response.ok) {
console.error("Error fetching related products")
return []
}
const data = await response.json()
return data.products_related
}Componente con estado (Client Component)
// components/products-related-client.tsx
"use client"
import { useEffect, useState } from "react"
import Image from "next/image"
import Link from "next/link"
type RelatedProduct = {
id: string
title: string
handle: string
thumbnail: string | null
related_by: string
relevance_score: number
}
export default function ProductsRelatedClient({
productId
}: {
productId: string
}) {
const [products, setProducts] = useState<RelatedProduct[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
async function fetchProducts() {
try {
const response = await fetch(
`/api/products-related?productId=${productId}`
)
const data = await response.json()
setProducts(data.products_related)
} catch (error) {
console.error("Error:", error)
} finally {
setLoading(false)
}
}
fetchProducts()
}, [productId])
if (loading) {
return (
<section className="py-8">
<h2 className="text-2xl font-bold mb-6">Productos Relacionados</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="border rounded-lg p-4 animate-pulse">
<div className="aspect-square bg-gray-200 rounded mb-3" />
<div className="h-4 bg-gray-200 rounded mb-2" />
<div className="h-3 bg-gray-200 rounded w-1/2" />
</div>
))}
</div>
</section>
)
}
if (products.length === 0) return null
return (
<section className="py-8">
<h2 className="text-2xl font-bold mb-6">Productos Relacionados</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{products.map((product) => (
<Link
key={product.id}
href={`/products/${product.handle}`}
className="group"
>
<div className="border rounded-lg p-4 hover:shadow-lg transition-shadow">
{product.thumbnail && (
<div className="relative aspect-square mb-3">
<Image
src={product.thumbnail}
alt={product.title}
fill
className="object-cover rounded"
/>
</div>
)}
<h3 className="font-medium text-sm line-clamp-2 group-hover:text-primary">
{product.title}
</h3>
<span className="inline-block mt-2 px-2 py-1 text-xs bg-gray-100 rounded">
{product.related_by === "category" && "Misma categoría"}
{product.related_by === "tag" && "Mismo tag"}
{product.related_by === "vendor" && "Mismo vendor"}
{product.related_by === "type" && "Mismo tipo"}
{product.related_by === "collection" && "Misma colección"}
</span>
</div>
</Link>
))}
</div>
</section>
)
}Variables de entorno necesarias
# .env.local
NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=your_publishable_api_keyCómo funciona
- Configuración: El admin configura qué tipos de relaciones habilitar y sus límites
- Consulta: Cuando se visita un producto, la API obtiene el producto con sus relaciones
- Búsqueda: Busca productos que compartan las características configuradas
- Ordenación: Los productos se ordenan por relevancia según el tipo de relación
- Respuesta: Devuelve los productos relacionados con datos completos
Estructura del plugin
src/
├── modules/
│ └── productsRelated/
│ ├── index.ts # Definición del módulo
│ ├── service.ts # Lógica del servicio
│ ├── models/ # Modelo de configuración
│ └── migrations/ # Migraciones de BD
├── api/
│ ├── store/ # API pública para storefront
│ └── admin/ # API para administración
├── admin/
│ └── routes/ # UI de administración
└── workflows/
└── index.ts # Exports de workflowsCompatibilidad
- Medusa v2.17.2+
- Node.js >= 20
- PostgreSQL
Licencia
MIT
Autor
jsm406
