fix: usar normalizeTitleForMatch en import de productos + notFound() en generateMetadata
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 33s
Build and Push Medusa / Escaneo de Secretos (Gitleaks) (push) Successful in 23s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 20m5s
Build and Push Medusa / Construir y publicar Medusa (push) Successful in 27m10s

El importador de commerce-backend definia normalizeTitleForMatch (colapsa
guion largo/comillas tipograficas) pero nunca la usaba en el matching de
producto/categoria/coleccion -- seguia comparando con toLowerCase().trim()
plano, por eso un re-import de "Toy Story 5 - Woody" con guion largo pegado
desde Excel volvio a crear un duplicado en vez de actualizar el existente.

En el storefront, cualquier slug de producto inexistente devolvia HTTP 200
en vez de 404 (confirmado con un slug inventado): el Suspense del navbar
hace que el layout empiece a stream-ear la respuesta con status 200 antes
de que notFound() se ejecute dentro de la pagina. Se mueve la verificacion
a generateMetadata, que se resuelve completo antes de iniciar el stream.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-09-19 19:38:07 +00:00
1 parent 7dd2c139cb
commit cc7ddddf58
2 files changed
+51 -14

No files matched your search

@@ -38,11 +38,22 @@ import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
// generaba un producto duplicado con handle distinto (confirmado en
// vivo con "Hello Kitty & Friends": misma fila re-subida para sumar
// fotos, dos productos con el mismo titulo en la tienda). El match es
// por titulo exacto (case-insensitive/trim), igual que categoria y
// coleccion; en un match, las fotos nuevas se agregan a las que ya
// tenia (no se reemplazan) y la descripcion se actualiza si la fila
// trae una. No toca precio/cantidad/categoria/coleccion del producto
// existente -- eso se edita a mano en el admin si hace falta.
// por titulo normalizado (case/espacios/guiones-comillas tipograficas,
// ver normalizeTitleForMatch), igual que categoria y coleccion; en un
// match, las fotos nuevas se agregan a las que ya tenia (no se
// reemplazan) y la descripcion se actualiza si la fila trae una. No
// toca precio/cantidad/categoria/coleccion del producto existente --
// eso se edita a mano en el admin si hace falta.
//
// El match era por titulo exacto (case-insensitive/trim) hasta que
// volvio a duplicar "Toy Story 5 - Woody": la fila reimportada traia
// un guion largo "–" (pegado desde Excel/Sheets, que autocorrige "-" a
// en-dash) en vez del guion normal "-" del producto original, asi que
// el string no calzaba y se creo un producto nuevo en vez de
// actualizar el existente (confirmado en vivo 2026-09-19). Ademas de
// guiones, se normalizan comillas tipograficas por la misma razon --
// cualquier CSV pegado desde un editor de texto enriquecido puede
// traerlas.
//
// Las fotos se descargan del origen y se re-alojan en el mismo
// MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow ->
@@ -88,6 +99,22 @@ type RowResult = {
// 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.
// Colapsa variantes de guion (-, en-dash, em-dash, minus) y comillas
// tipograficas a su forma ASCII antes de comparar titulos, para que un
// re-import pegado desde Excel/Sheets/Word (que autocorrigen esos
// caracteres) siga matcheando el producto ya existente. Ver nota mas
// arriba sobre "Toy Story 5 - Woody".
function normalizeTitleForMatch(title: string): string {
return title
.normalize("NFKC")
.replace(/[‐-―−]/g, "-")
.replace(/[‘’‛]/g, "'")
.replace(/[“”‟]/g, '"')
.replace(/\s+/g, " ")
.trim()
.toLowerCase();
}
function slugify(title: string, uniquePart: string): string {
const base = title
.normalize("NFD")
@@ -190,13 +217,13 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
}
const categoryByName = new Map(
categories.filter((c) => c.name).map((c) => [c.name.toLowerCase().trim(), c.id]),
categories.filter((c) => c.name).map((c) => [normalizeTitleForMatch(c.name), c.id]),
);
const collectionByName = new Map(
collections.filter((c) => c.title).map((c) => [c.title.toLowerCase().trim(), c.id]),
collections.filter((c) => c.title).map((c) => [normalizeTitleForMatch(c.title), c.id]),
);
const productByTitle = new Map(
existingProducts.filter((p) => p.title).map((p) => [p.title.toLowerCase().trim(), p]),
existingProducts.filter((p) => p.title).map((p) => [normalizeTitleForMatch(p.title), p]),
);
const results: RowResult[] = [];
@@ -228,7 +255,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
},
});
const existingProduct = productByTitle.get(title.toLowerCase());
const existingProduct = productByTitle.get(normalizeTitleForMatch(title));
if (existingProduct) {
await updateProductsWorkflow(req.scope).run({
input: {
@@ -262,7 +289,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
if (!inventoryItemId) throw new Error("No se pudo crear el registro de inventario");
const categoryName = row.categoria?.trim();
let categoryId = categoryName ? categoryByName.get(categoryName.toLowerCase()) : undefined;
let categoryId = categoryName ? categoryByName.get(normalizeTitleForMatch(categoryName)) : undefined;
if (categoryName && !categoryId) {
const { result: createdCategories } = await createProductCategoriesWorkflow(req.scope).run({
input: {
@@ -272,11 +299,11 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
},
});
categoryId = createdCategories[0].id;
categoryByName.set(categoryName.toLowerCase(), categoryId);
categoryByName.set(normalizeTitleForMatch(categoryName), categoryId);
}
const collectionName = row.coleccion?.trim();
let collectionId = collectionName ? collectionByName.get(collectionName.toLowerCase()) : undefined;
let collectionId = collectionName ? collectionByName.get(normalizeTitleForMatch(collectionName)) : undefined;
if (collectionName && !collectionId) {
const { result: createdCollections } = await createCollectionsWorkflow(req.scope).run({
input: {
@@ -286,7 +313,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
},
});
collectionId = createdCollections[0].id;
collectionByName.set(collectionName.toLowerCase(), collectionId);
collectionByName.set(normalizeTitleForMatch(collectionName), collectionId);
}
await createProductsWorkflow(req.scope).run({