Reorganiza el routing multi-tenant: funnel por slug, B2B en raíz

- / y /b2b sirven la landing B2B estática (rewrites beforeFiles)
- /{slug} resuelve el funnel del reformista (app/[slug]/page.tsx) con
  branding propio (TenantBrand) y atribución de leads por tenant
- crearLead(slug) y páginas /solicitud usan el tenant del lead
- Panel: edición del slug del funnel + URL pública en /panel/empresa
- Helper de slugs reservados para evitar colisiones con rutas reales

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-01 11:09:44 +02:00
parent e26e6be38b
commit 1a1caaf0df
15 changed files with 268 additions and 86 deletions

View File

@@ -19,3 +19,45 @@ export function slugify(value: string): string {
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
// Rutas de la app que NO pueden ser slug de un reformista: colisionarían con
// páginas reales o con ficheros públicos servidos en la raíz del dominio.
export const RESERVED_SLUGS = new Set([
'panel',
'login',
'logout',
'signup',
'admin',
'solicitud',
'api',
'b2b',
'b2b-assets',
'assets',
'_next',
'favicon.ico',
'icon.svg',
'robots.txt',
'sitemap.xml',
]);
export function isReservedSlug(slug: string): boolean {
return RESERVED_SLUGS.has(slug);
}
export type SlugValidation = { ok: true; slug: string } | { ok: false; error: string };
// Normaliza y valida un slug propuesto para el funnel público de un reformista.
// No comprueba unicidad (eso requiere BD); solo formato y palabras reservadas.
export function validarSlug(raw: string): SlugValidation {
const slug = slugify(raw);
if (slug.length < 2) {
return { ok: false, error: 'El enlace debe tener al menos 2 caracteres.' };
}
if (slug.length > 40) {
return { ok: false, error: 'El enlace no puede superar los 40 caracteres.' };
}
if (isReservedSlug(slug)) {
return { ok: false, error: 'Ese enlace está reservado. Elige otro.' };
}
return { ok: true, slug };
}