Invitaciones Publicas (nvito-invitations)
Tabla de Contenidos
- Estructura del Proyecto
- Flujo SSG + ISR
- InvitationRenderer
- Modo Preview para Admin
- Tracking de Analitica
- SEO y Metadata Dinamica
- Webhook de Revalidacion
- Estrategia de Cache
- Headers de Seguridad
- Arquitectura SOLID
- Testing
- Calidad del Codigo
Estructura del Proyecto
nvito-invitations es una aplicacion Next.js 16 minimalista cuyo unico proposito es servir las invitaciones publicas generadas por el sistema. Su diseno prioriza rendimiento y tiempo de carga, manteniendo las dependencias al minimo absoluto.
Agnostico al origen: La aplicacion renderiza invitaciones de forma identica sin importar si fueron creadas con el motor de templates de Nvito (INTERNAL) o subidas como HTML externo (EXTERNAL). La distincion de source solo existe en el backend y en nvito-admin; para nvito-invitations, toda invitacion publicada es simplemente un htmlUrl que se sirve como HTML puro.
Dependencias de Produccion (solo 7)
| Dependencia | Version | Proposito |
|---|---|---|
next | ^16.1.6 | Framework SSG/ISR |
react | ^19.2.3 | Libreria de UI |
react-dom | ^19.2.3 | Renderizado DOM |
dompurify | ^3.x | Sanitizacion HTML segura |
zod | ^3.x | Validacion de respuestas API en runtime |
ua-parser-js | ^2.0.9 | Deteccion de dispositivo/navegador para analitica |
web-vitals | ^4.x | Metricas de rendimiento web (LCP, INP, CLS) |
Esta arquitectura ultraligera garantiza que la aplicacion sea extremadamente rapida y tenga una superficie de ataque minima.
Estructura de Archivos
nvito-invitations/
├── app/
│ ├── layout.tsx # Layout raiz (html lang="es")
│ ├── page.tsx # Pagina principal (landing)
│ ├── globals.css # Estilos base minimos
│ ├── i/
│ │ └── [slug]/
│ │ ├── route.ts # Route handler SSG+ISR (orquestador ~35 LOC)
│ │ ├── InvitationRenderer.tsx # Client component de renderizado
│ │ ├── not-found.tsx # Pagina 404 para invitacion
│ │ └── __tests__/
│ │ └── route.test.ts # Tests de seguridad y flujo
│ ├── preview/
│ │ └── [token]/
│ │ ├── route.ts # Vista previa con token JWT
│ │ ├── not-found.tsx # 404 para preview invalido
│ │ └── __tests__/
│ │ └── route.test.ts # Tests de preview
│ ├── api/
│ │ └── revalidate/
│ │ ├── route.ts # Webhook de revalidacion ISR
│ │ └── __tests__/
│ │ └── route.test.ts # Tests de revalidacion
│ └── lib/
│ ├── analytics.ts # Sistema de tracking de eventos
│ ├── global-tracking.ts # Funciones globales para JS de invitacion
│ ├── schemas.ts # Schemas Zod (invitationData, previewData, revalidateRequest)
│ ├── html-responses.ts # Respuestas HTML error reutilizables
│ ├── error-reporting.ts # Reporte de errores estructurado
│ ├── web-vitals.ts # Metricas de rendimiento web
│ ├── services/ # Servicios SOLID (SRP)
│ │ ├── invitation-fetcher.ts # Fetch API + validacion Zod
│ │ ├── html-processor.ts # Fetch CDN + inyeccion meta/analytics
│ │ ├── cdn-validator.ts # Validacion SSRF de URLs CDN
│ │ ├── analytics-script.ts # Generacion de script con escape XSS
│ │ └── __tests__/ # Tests unitarios por servicio
│ ├── dom/ # Servicios de manipulacion DOM
│ │ ├── font-injector.ts # Extraccion e inyeccion de Google Fonts
│ │ ├── html-sanitizer.ts # Sanitizacion DOMPurify centralizada
│ │ └── __tests__/ # Tests unitarios DOM
│ └── __tests__/ # Tests de lib
│ ├── html-responses.test.ts
│ ├── error-reporting.test.ts
│ └── web-vitals.test.ts
├── config/
│ └── environments/ # Archivos .env por ambiente
└── package.json
Rutas de la Aplicacion
| Ruta | Tipo | Descripcion |
|---|---|---|
/ | Estatica | Pagina principal / landing |
/i/[slug] | SSG + ISR | Invitacion publica por slug |
/preview/[token] | Dinamica (SSR) | Vista previa con token JWT |
/api/revalidate | API Route | Webhook para revalidacion on-demand |
Flujo SSG + ISR
Las invitaciones publicas utilizan Static Site Generation (SSG) combinado con Incremental Static Regeneration (ISR) para lograr tiempos de carga instantaneos con contenido actualizable.
Configuracion ISR
La pagina /i/[slug]/page.tsx configura ISR con los siguientes parametros:
export const revalidate = 3600; // Revalidar cada hora (3600 segundos)
export const dynamic = 'force-static'; // Forzar generacion estatica
export const dynamicParams = true; // Permitir generacion on-demand de nuevas rutas
Generacion Estatica
La funcion generateStaticParams() retorna un array vacio en build time, lo que significa que todas las invitaciones se generan on-demand la primera vez que se visitan. Despues de la primera visita, la pagina queda en cache.
InvitationRenderer
El componente InvitationRenderer es un client component responsable de inyectar de forma segura el HTML, CSS y JS compilado de la invitacion en el DOM.
Funcionamiento
- Inyeccion de CSS: Crea un elemento
<style>en el<head>del documento con atributodata-invitation-slugpara identificacion - Inyeccion de HTML: Establece el
innerHTMLdel contenedor principal - Inyeccion de JS (opcional): Crea un elemento
<script>dentro del contenedor - Tracking: Registra page view y activa tracking global al montar
- Cleanup: Al desmontar, remueve los elementos
<style>y<script>inyectados y limpia el tracking global
Props
interface InvitationRendererProps {
html: string; // HTML compilado de la invitacion
css: string; // CSS compilado
js?: string; // JS compilado (opcional)
slug: string; // Slug de la invitacion
invitationId: string; // ID para tracking de analitica
}
Aislamiento
- El componente usa
suppressHydrationWarningpara evitar errores de hidratacion con contenido dinamico - Los refs (
styleRef,scriptRef) previenen duplicacion de inyeccion en re-renders - El
viewTrackedRefasegura que el page view se registre solo una vez
Modo Preview para Admin
La ruta /preview/[token] permite a los administradores ver una vista previa de la invitacion antes de publicarla. Se accede desde el panel admin mediante un iframe.
Caracteristicas
- Autenticacion por token JWT: El token se genera en el backend y tiene expiracion
- Sin cache (
cache: 'no-store',dynamic: 'force-dynamic',revalidate: 0) - No indexable:
robots: { index: false, follow: false } - HTML completo: El backend compila el template y devuelve un
fullDocumentHTML completo - Renderizado directo: Usa
dangerouslySetInnerHTMLpara inyectar el documento completo
Flujo de Preview
- Admin abre el editor visual en nvito-admin
- El admin solicita un token de preview al API
- El API genera un JWT con expiracion corta
- nvito-admin carga un iframe con
/preview/{token} - nvito-invitations obtiene el HTML compilado del API usando el token
- Se renderiza el documento completo sin cache
Tracking de Analitica
Tipos de Eventos
El sistema de analitica registra 6 tipos de eventos:
| Evento | Descripcion | Activacion |
|---|---|---|
VIEW | Vista de la pagina | Automatica al cargar |
RSVP_OPEN | Apertura del formulario RSVP | Automatica via MutationObserver |
RSVP_SUBMIT | Envio del formulario RSVP | Automatica al submit |
LINK_CLICK | Clic en enlace externo | Automatica via event listener |
SHARE | Accion de compartir | Via window.NvitoAnalytics |
DOWNLOAD | Descarga de archivo | Via window.NvitoAnalytics |
Datos Recopilados
Cada evento de analitica incluye:
- visitorId: ID unico persistido en
localStorage - userAgent: Cadena completa del user agent
- device: Tipo de dispositivo (desktop, mobile, tablet)
- browser: Nombre del navegador
- os: Sistema operativo
- referrer: URL de procedencia
Global Tracking
El modulo global-tracking.ts expone un objeto window.NvitoAnalytics que el JavaScript generado de la invitacion puede utilizar para registrar eventos personalizados:
// Disponible dentro del JS de la invitacion
window.NvitoAnalytics.trackRsvpOpen(invitationId);
window.NvitoAnalytics.trackShare(invitationId, 'whatsapp');
window.NvitoAnalytics.trackDownload(invitationId, 'pdf');
Auto-tracking
El sistema configura automaticamente:
- Links externos: Detecta clics en
<a>conhrefexterno - Formularios RSVP: Usa
MutationObserverpara detectar cuando un formulario RSVP aparece en el DOM, ysubmitlistener para registrar envios
SEO y Metadata Dinamica
Cada invitacion genera metadata unica para SEO y redes sociales usando generateMetadata():
Open Graph
openGraph: {
title: `Invitacion - ${slug}`,
description: `Te invitamos a nuestro evento especial - ${eventName}`,
type: 'website',
url: `/i/${slug}`,
}
Twitter Card
twitter: {
card: 'summary_large_image',
title: `Invitacion - ${slug}`,
description: 'Te invitamos a nuestro evento especial',
}
Robots
Las invitaciones publicadas son indexables: robots: { index: true, follow: true }.
Las vistas de preview no son indexables: robots: { index: false, follow: false }.
Webhook de Revalidacion
Cuando un administrador publica o actualiza una invitacion, el API envia un webhook a nvito-invitations para invalidar el cache ISR y regenerar la pagina.
Seguridad del Webhook
- Timing-safe comparison: Se usa
crypto.timingSafeEqual()para comparar el secreto, previniendo timing attacks que podrian inferir el secreto caracter por caracter midiendo tiempos de respuesta - Validacion Zod: El body se valida con
revalidateRequestSchema(paths como array de strings con regex, slug con regex y max 200 chars) viasafeParse()en lugar de validacion manual - Rate limiting: Maximo 10 requests por ventana de tiempo; el request 11 retorna 429
- Metodo HTTP: Solo acepta POST; GET retorna 405
- Logging detallado: Cada revalidacion exitosa o fallida se registra en logs
- Respuesta estructurada: Retorna timestamp, slug y resultado por cada path
Estrategia de Cache
Niveles de Cache
| Nivel | TTL | Proposito |
|---|---|---|
| ISR (Next.js) | 3600s (1 hora) | Cache de pagina generada estaticamente |
| Fetch API metadata | revalidate: 3600 | Cache de metadata de la invitacion |
| Fetch HTML/CSS/JS | revalidate: 3600 | Cache de archivos compilados del CDN |
| Revalidacion on-demand | Instantanea | Via webhook POST /api/revalidate |
Headers HTTP Esperados
Para los archivos servidos desde el CDN (R2/MinIO):
Cache-Control: public, max-age=3600 # HTML (1 hora)
Cache-Control: public, max-age=31536000 # CSS y JS (1 ano, inmutable)
Regeneracion
La pagina se regenera en dos escenarios:
- Automatica: Cuando el TTL de 1 hora expira y llega una nueva solicitud
- On-demand: Cuando el API envia el webhook de revalidacion al publicar una invitacion
Headers de Seguridad
Headers HTTP Configurados
| Header | Valor | Proteccion |
|---|---|---|
Content-Security-Policy | frame-ancestors 'self' {admin-domains} | Previene clickjacking: solo el panel admin puede cargar invitaciones en iframes |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Fuerza HTTPS durante 1 ano, incluyendo subdominios |
Permissions-Policy | camera=(), microphone=(), geolocation=(), interest-cohort=() | Deshabilita APIs innecesarias del navegador y FLoC tracking |
X-Content-Type-Options | nosniff | Previene MIME type sniffing |
X-Frame-Options | SAMEORIGIN | Proteccion adicional contra clickjacking |
Prevencion XSS en Analytics Script
Las invitaciones publicas inyectan un script de analitica con variables dinamicas (invitationId, API_URL). Para prevenir inyeccion XSS:
escapeJsString(): Funcion dedicada que escapa',",\,</script>, newlines y caracteres Unicode peligrosos antes de inyectarlos en el<script>tag- Vectores neutralizados: Payloads como
'; alert(1); //o</script><script>malicious()son escapados antes de renderizar
Validacion SSRF de URLs CDN
La funcion validateCdnUrl() valida que las URLs de archivos HTML/CSS/JS apunten exclusivamente a hosts CDN permitidos (R2/MinIO). Esto previene ataques SSRF donde un atacante podria manipular URLs para redirigir requests a servicios internos.
Sanitizacion HTML con DOMPurify
El InvitationRenderer sanitiza el HTML inyectado con DOMPurify:
- Whitelist explicita de atributos
data-*permitidos viaADD_ATTR(sinALLOW_DATA_ATTR: trueque permitiriadata-*arbitrarios) - Extraccion de fuentes: Las URLs de Google Fonts se extraen e inyectan via
<link>tags seguros en lugar de ejecutar el CSS directamente
Consideraciones Adicionales
- El modo preview usa
force-dynamicpara evitar servir contenido obsoleto - Las invitaciones no publicadas (status !==
PUBLISHED) retornan 404 - Slugs con caracteres especiales o longitud > 200 caracteres retornan 404
Scripts de Desarrollo
# Desarrollo local (puerto 3001)
npm run dev
# Cambiar ambiente
npm run env:dev # Desarrollo local
npm run env:dev-remote # API remota desarrollo
npm run env:test-remote # API remota testing
# Build de produccion
npm run build
# Desarrollo con Bitwarden Secrets Manager
npm run dev:bws
Variables de Entorno
| Variable | Descripcion | Default |
|---|---|---|
NEXT_PUBLIC_API_URL | URL base del API | http://localhost:3000 |
NEXT_PUBLIC_CDN_URL | URL del CDN para archivos estaticos | http://localhost:9000 |
REVALIDATE_SECRET | Secreto compartido para webhook de revalidacion | (requerido) |
Arquitectura SOLID
El proyecto sigue principios SOLID, especialmente Single Responsibility Principle (SRP). Cada archivo tiene una unica razon de cambio.
Descomposicion del Route Handler
El route handler /i/[slug]/route.ts originalmente mezclaba 10+ responsabilidades en 158 lineas. Fue descompuesto en servicios con responsabilidad unica:
| Servicio | Archivo | Responsabilidad |
|---|---|---|
invitation-fetcher | lib/services/invitation-fetcher.ts | Fetch de metadata desde API + validacion Zod |
html-processor | lib/services/html-processor.ts | Fetch de HTML/CSS/JS desde CDN + inyeccion de meta tags |
cdn-validator | lib/services/cdn-validator.ts | Validacion SSRF de URLs CDN contra whitelist de hosts |
analytics-script | lib/services/analytics-script.ts | Generacion del <script> de analitica con escape XSS |
html-responses | lib/html-responses.ts | Respuestas HTML de error reutilizables (404, error generico) |
El route handler quedo como orquestador limpio de ~35 lineas que delega a cada servicio.
Descomposicion del InvitationRenderer
| Servicio | Archivo | Responsabilidad |
|---|---|---|
font-injector | lib/dom/font-injector.ts | Extraccion de URLs de Google Fonts e inyeccion via <link> tags |
html-sanitizer | lib/dom/html-sanitizer.ts | Sanitizacion DOMPurify con configuracion centralizada |
Testing
El proyecto cuenta con 17 suites de prueba y 180 tests individuales, todos con 100% de tasa de exito.
Cobertura por Area
| Area | Suites | Que prueban |
|---|---|---|
| Route handlers | 3 | /i/[slug], /preview/[token], /api/revalidate |
| Servicios SOLID | 4 | invitation-fetcher, html-processor, cdn-validator, analytics-script |
| DOM utilities | 2 | font-injector, html-sanitizer |
| Schemas Zod | 1 | Validacion de datos de invitacion y revalidacion |
| Lib utilities | 3 | html-responses, error-reporting, web-vitals |
| Existentes | 3 | Tests previos del proyecto |
Tests de Seguridad
Los tests incluyen vectores de ataque reales:
- Timing attacks: Secrets de longitudes diferentes y contenido diferente
- XSS payloads:
</script><script>alert(1),'; DROP TABLE, comillas en invitationId - SSRF: URLs CDN con hosts no permitidos
- Rate limiting: 11+ requests consecutivos verificando respuesta 429
- Input validation: Slugs con caracteres especiales, longitud > 200 chars
Calidad del Codigo
| Indicador | Valor |
|---|---|
| TypeScript strict | Habilitado |
any explicitos | 0 |
| JSDoc | 100% en metodos publicos |
| Schemas Zod | Validacion en runtime de API responses y webhook body |
| Test suites | 17 |
| Tests individuales | 180 |
| Tasa de exito | 100% |
| Vulnerabilidades criticas | 0 (timing-safe, XSS escaped, SSRF validated, HSTS enabled) |
| Violaciones SRP | 0 (todos los archivos con responsabilidad unica) |
Referencias
- Route handler:
app/i/[slug]/route.ts - Renderer:
app/i/[slug]/InvitationRenderer.tsx - Preview:
app/preview/[token]/route.ts - Revalidacion:
app/api/revalidate/route.ts - Servicios:
app/lib/services/ - DOM utilities:
app/lib/dom/ - Schemas:
app/lib/schemas.ts - Analytics:
app/lib/analytics.ts - Global Tracking:
app/lib/global-tracking.ts