Files
mercadodevida/work/artifacts/F-136/implementer.md
2026-08-21 21:10:29 +02:00

5.3 KiB

F-136 — Implementer notes: Brand names Title Case + SEO title auto-fill

Cambios

project/src/shared/text.ts (nuevo)

Helper puro toTitleCase(input: string): string:

  • Divide el string en palabras usando como separadores espacio, guión, punto, ampersand y slash.
  • Para cada palabra: primera letra mayúscula, resto minúscula (con toLocaleUpperCase('es-ES') / toLocaleLowerCase('es-ES') para manejar acentos correctamente).
  • Mantiene y, e, o, u en minúscula cuando NO son la primera palabra (tipografía española estándar).
  • Idempotente sobre mixed-case: si la entrada no es ALL-CAPS, la devuelve sin tocar (así BioSana se queda como BioSana, y aplicar dos veces da el mismo resultado).
  • Sin dependencias externas. Sin estado. Pura.

project/src/shared/tests/text.test.ts (nuevo)

15 tests vitest que cubren:

  • String vacío → vacío.
  • Single word ALL CAPS (BIOCOPBiocop).
  • Multi-word con espacios (EL GRANERO INTEGRALEl Granero Integral).
  • Punto preservado (A.VOGELA.Vogel).
  • Guion preservado (DAS-BROTDas-Brot).
  • Ampersand preservado (TEA & INFUSIONSTea & Infusions).
  • Acentos españoles (COMPLEMENTOS Y NUTRICIÓNComplementos y Nutrición).
  • Idempotencia sobre Title Case ya aplicado.
  • Mixed-case sin tocar (BioSanaBioSana).
  • Dígitos intactos (500 KILOS500 Kilos).
  • Separadores múltiples consecutivos.
  • String de solo separadores.
  • Single-letter conjunctions lowercase cuando no son primera palabra.
  • Single-letter conjunctions mayúscula cuando SÍ son primera palabra.
  • Single-letter words no-conjunción (A SIDE BA Side B).

Resultado: 15/15 passed.

project/migrations/042_brand_title_case.js (nuevo)

Migración de datos node-pg-migrate:

  • Selecciona todas las filas de brands_brands.
  • Para cada fila calcula titleCased = toTitleCase(name) y newSeoTitle = (seo_title || titleCased).
  • Solo ejecuta UPDATE si el nombre cambió o seo_title estaba vacío.
  • No toca slugs (ya están en lowercase vía slugify()).
  • Loggea cuántos rows actualizó de los totales.

Inline la lógica de toTitleCase (en vez de importar de src/shared/text.ts) para que la migración sea autocontenida y no requiera npm run build antes de correr. Los 15 unit tests cubren la lógica y detectarían cualquier drift entre el helper compartido y la versión inline.

Resultado de la migración

F-136: updated 28 brand row(s) (of 33 total)
Métrica Antes Después
Total brands 33 33
ALL-CAPS 28 0
sin seo_title 28 0
Slugs modificados 0 (lowercase se preserva)

Re-ejecución es no-op: node-pg-migrate up reporta No migrations to run! (porque la migración ya está en pgmigrations).

Ejemplos antes/después

Antes Después
A.VOGEL A.Vogel
BIOCOP Biocop
COMPLEMENTOS Y NUTRICIÓN Complementos y Nutrición
EL GRANERO INTEGRAL El Granero Integral
LA FINESTRA SUL CIELO La Finestra Sul Cielo
DULCES LISSEN Dulces Lissen
BioSana (mixed) BioSana (sin tocar)

Todos los seo_title se backfillearon con el nuevo nombre en Title Case.

Archivos

  • project/src/shared/text.ts (nuevo)
  • project/src/shared/tests/text.test.ts (nuevo)
  • project/migrations/042_brand_title_case.js (nuevo)
  • pgmigrations table: nueva fila 042_brand_title_case con run_on timestamp

Evidencia

$ npx tsc --noEmit
(exit 0)

$ npx eslint src/shared/text.ts src/shared/tests/text.test.ts migrations/042_brand_title_case.js
(exit 0)

$ npx vitest run src/shared/tests/text.test.ts
✓ src/shared/tests/text.test.ts (15 tests) 2ms
Test Files  1 passed (1)
     Tests  15 passed (15)

$ node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js up --migrations-dir migrations
F-136: updated 28 brand row(s) (of 33 total)
Migrations complete!

$ psql -c "SELECT name, seo_title FROM brands_brands ORDER BY name LIMIT 35;"
(muestra todas las marcas en Title Case con seo_title poblado)

$ node-pg-migrate.js up (re-run)
No migrations to run!   ← idempotencia confirmada

Sin cambios en frontend

El form de admin brands/page.tsx ya tenía autoSeoTitle(name) → name y handleNameChange que lo invoca cuando !seoTitleManual. Eso significa que las marcas NUEVAS ya reciben seo_title = name automáticamente. Para las marcas existentes (que era el problema real), la migración lo arregla en DB.

No fue necesario tocar código del admin.

Notas

  • El operador puede verificar el resultado con psql -c "SELECT name, slug, seo_title FROM brands_brands ORDER BY name" o navegando a /brands en el admin.
  • La columna slug no se tocó porque slugify() lowercasea; los slugs a-vogel, biocop, etc. ya estaban correctos desde el import inicial.
  • Si en el futuro el operador pide lo mismo para categorías, el mismo helper toTitleCase se puede reutilizar en una nueva migración (las traducciones al español ya están en legacy-catalog.ts desde F-116).
  • La migración es destructiva en sentido figurado: pasa A.VOGELA.Vogel. No hay forma de rollback automático porque el original está perdido. Si el operador pide revertir, sería con un script ad-hoc que lea el log de git para reconstruir.