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,uen 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í
BioSanase queda comoBioSana, 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 (
BIOCOP→Biocop). - Multi-word con espacios (
EL GRANERO INTEGRAL→El Granero Integral). - Punto preservado (
A.VOGEL→A.Vogel). - Guion preservado (
DAS-BROT→Das-Brot). - Ampersand preservado (
TEA & INFUSIONS→Tea & Infusions). - Acentos españoles (
COMPLEMENTOS Y NUTRICIÓN→Complementos y Nutrición). - Idempotencia sobre Title Case ya aplicado.
- Mixed-case sin tocar (
BioSana→BioSana). - Dígitos intactos (
500 KILOS→500 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 B→A 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)ynewSeoTitle = (seo_title || titleCased). - Solo ejecuta UPDATE si el nombre cambió o
seo_titleestaba 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)pgmigrationstable: nueva fila042_brand_title_caseconrun_ontimestamp
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/brandsen el admin. - La columna
slugno se tocó porqueslugify()lowercasea; los slugsa-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
toTitleCasese puede reutilizar en una nueva migración (las traducciones al español ya están enlegacy-catalog.tsdesde F-116). - La migración es destructiva en sentido figurado: pasa
A.VOGEL→A.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.