fix(product-import): generar handle ASCII-safe en vez del default de Medusa

3 productos importados con título acentuado (ñ/tildes) quedaron con handle
no-ASCII y daban 404 real al entrar por shop.cruzcloud.net -- el tunel/
ingress público rompe rutas con UTF-8 no-ASCII en el path, confirmado en
vivo comparando el mismo request directo al servicio (200 OK) contra el
dominio público (404). Se genera el handle explícito (slugify propio,
sin dependencia nueva) con sufijo único por fila para evitar colisiones
entre títulos parecidos. Los 3 productos ya publicados con handle roto
se corrigieron en caliente vía Admin API, sin esperar este deploy.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SEsGCsCmxkyXi9XctNdrT1
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-09-06 03:43:47 +00:00
1 parent 63746eb090
commit a5ede05d96
1 file changed
+23
@@ -53,6 +53,28 @@ type RowResult = {
error?: string; error?: string;
}; };
// Medusa genera el handle por defecto solo en minusculas + espacios a
// guiones, sin quitar tildes/enies -- confirmado en vivo: 3 productos
// importados con titulo acentuado (ej. "niñas", "Lógicos") quedaron con
// handle como "...niñas..." y daban 404 real al entrar por
// shop.cruzcloud.net (el tunel/ingress publico rompe rutas con UTF-8
// no-ASCII en el path, aunque el mismo request funciona perfecto pegandole
// directo al servicio dentro del cluster). Se genera el handle nosotros
// mismos, ASCII-only, en vez de dejarselo al default de Medusa. El sufijo
// corto evita colisiones entre titulos parecidos/repetidos sin necesidad
// de revisar unicidad antes de crear.
function slugify(title: string, uniquePart: string): string {
const base = title
.normalize("NFD")
.replace(/[̀-ͯ]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80)
.replace(/-+$/g, "");
return `${base || "producto"}-${uniquePart}`;
}
function extensionFromContentType(contentType: string | null, url: string): string { function extensionFromContentType(contentType: string | null, url: string): string {
if (contentType?.includes("png")) return "png"; if (contentType?.includes("png")) return "png";
if (contentType?.includes("webp")) return "webp"; if (contentType?.includes("webp")) return "webp";
@@ -201,6 +223,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
products: [ products: [
{ {
title, title,
handle: slugify(title, `${Date.now().toString(36)}${index}`),
description: row.descripcion || undefined, description: row.descripcion || undefined,
status: "published", status: "published",
thumbnail: uploadedFiles[0]?.url, thumbnail: uploadedFiles[0]?.url,