5.9 KiB
5.9 KiB
F-136 — Design: Brand names Title Case + SEO title auto-fill
Author: architect Date: 2026-08-21 Stage: design
Context (problem)
- DB inspection (
SELECT name FROM brands_brands) shows 28 of 33 brands have ALL-CAPS names imported from OpenCart (e.g.A.VOGEL,BIOCOP,COMPLEMENTOS Y NUTRICIÓN,EL GRANERO INTEGRAL). - The same 28 brands have
seo_title IS NULL OR ''because the legacy seed script never set it. - The admin
brands/page.tsxform already auto-fillsseo_titlefromnameon create viahandleNameChange, so new brands get it right. The problem is purely with the existing imported data. - Operator's two-part request:
- "poner las marcas en formato 'capital case'" → Title Case the existing names.
- "SEO title igual al nombre de la marca" → for new brands the auto-fill already handles this; for existing brands we need to backfill.
Constraints
- C-1. Pure data fix: no API changes, no UI behavior changes, no new endpoints.
- C-2. The migration must be idempotent — safe to re-run after partial application (the orchestre may apply it more than once if a feature is reopened).
- C-3. Slugs (
a-vogel,biocop) are derived fromslugify()which lowercases everything; they don't need updating. - C-4. The Title Case function must:
- Preserve dots, hyphens, ampersands as word boundaries (
A.VOGEL→A.Vogel, notA.vogel). - Handle Spanish accented characters (
NUTRICIÓN→Nutrición). - Handle multi-word names (
EL GRANERO INTEGRAL→El Granero Integral). - Leave already-correct names untouched (idempotency for re-runs).
- Preserve dots, hyphens, ampersands as word boundaries (
- C-5. We do NOT want to lowercase Spanish articles/prepositions like
y,e,o,de,la,el— for brand names, the standard is to capitalize every word (El Granero Integral, notEl Granero integral). Keeping it simple.
Design
Decision: shared helper + data migration
Two deliverables:
-
project/src/shared/text.ts— new pure helpertoTitleCase(input: string): stringthat handles dot/hyphen/space/ampersand word boundaries. Unit-tested inproject/src/shared/tests/text.test.ts.- Lives in
shared/because it's pure (no DB / no HTTP) and could be reused by categories/products later if the operator requests it.
- Lives in
-
project/migrations/042_brand_title_case.js— node-pg-migrate data migration:- Selects all rows from
brands_brands. - For each row, computes
titleCased = toTitleCase(name). - Updates the row IFF
titleCased !== nameORseo_title IS NULL OR seo_title = ''. - When updating:
name = titleCased,seo_title = (existing || titleCased),updated_at = NOW().
- Selects all rows from
Helper signature
/**
* Convert an ALL-CAPS brand/category name to Title Case.
* Splits on word boundaries (space, hyphen, dot, ampersand, slash)
* and uppercases the first letter of each word, lowercasing the rest.
*
* Examples:
* toTitleCase('A.VOGEL') // 'A.Vogel'
* toTitleCase('BIOCOP') // 'Biocop'
* toTitleCase('EL GRANERO INTEGRAL') // 'El Granero Integral'
* toTitleCase('COMPLEMENTOS Y NUTRICIÓN') // 'Complementos y Nutrición'
* toTitleCase('DULCES LISSEN') // 'Dulces Lissen'
* toTitleCase('La Finestra Sul Cielo') // 'La Finestra Sul Cielo' (unchanged)
* toTitleCase('') // ''
*/
export function toTitleCase(input: string): string;
Idempotency
Re-running the migration is a no-op because:
- For ALL-CAPS rows that were already converted,
toTitleCase(name) === name(case-insensitive split produces the same word capitalizations), so theUPDATEnever fires. - For rows where
seo_titlewas already set, the CASE expression preserves it.
We add a whereNeedsUpdate check so we don't bump updated_at unnecessarily.
Test cases for the helper (vitest)
- Empty string → empty string.
- Single word all caps → title case.
- Multi-word with spaces → each word capitalized.
- Names with dots (
A.VOGEL) → dots preserved as boundaries. - Names with hyphens (
DAS-BROT) → hyphen preserved, both sides capitalized. - Names with
&(TEA & INFUSIONS) →&preserved. - Spanish accents (
NUTRICIÓN) →Nutrición(correct NFD handling). - Already Title Case (
La Finestra Sul Cielo) → unchanged (idempotency). - Mixed case (
BioSana) → unchanged. - Numbers (
500 Kilos) →500 Kilos(digits unaffected).
Files affected
| File | Change |
|---|---|
project/src/shared/text.ts |
NEW — toTitleCase() helper |
project/src/shared/tests/text.test.ts |
NEW — vitest unit tests |
project/migrations/042_brand_title_case.js |
NEW — data migration |
Out of scope
- Frontend changes. The auto-fill in
brands/page.tsxalready works for new brands. - Categories: the operator asked about brands specifically. F-116 already fixed categories (see
LEGACY_TRANSLATIONSinlegacy-catalog.ts). If the operator later asks, the sametoTitleCasehelper can be reused in a future migration. - Slug changes: slugs are already lowercase; they don't depend on name case.
Acceptance criteria
- AC-1.
npm test -- text.test.tspasses with all 10 test cases. - AC-2.
node-pg-migrate upapplies migration 042 without errors. - AC-3. After migration:
SELECT name FROM brands_brandsshows all 33 names in Title Case (or unchanged if already correct). No ALL-CAPS remain. - AC-4. After migration:
SELECT COUNT(*) FROM brands_brands WHERE seo_title IS NULL OR seo_title = ''returns 0. - AC-5. Re-running the migration (
node-pg-migrate upagain) is a no-op: 0 rows updated, no errors. - AC-6. Slugs unchanged (still lowercase). Verify with
SELECT slug FROM brands_brands ORDER BY name LIMIT 5. - AC-7. The frontend admin form still works: creating a new brand with name "TestBrand" sets
seo_titleto "TestBrand" automatically. - AC-8. Backend typecheck + admin typecheck + lint green.
- AC-9.
verify.shexit 0.