Flujo de Renderizado de Invitacion Publica
Pipeline completo desde que un invitado abre el enlace de su invitacion hasta que ve la pagina renderizada con transiciones Canvas, fuentes personalizadas y analiticas. Incluye ISR, revalidacion on-demand, sanitizacion de seguridad y preview embebido.
Tabla de Contenidos
- Vision General
- ISR: Generacion Estatica Incremental
- Revalidacion On-Demand
- Pipeline de Renderizado del Servidor
- Prevencion SSRF
- Seguridad: Sanitizacion y Escape
- Renderizado en el Cliente
- Modo Preview
- Headers de Seguridad y CSP
- Rate Limiting
- Diagrama del Pipeline Completo
- Archivos Clave
1. Vision General
nvito-invitations es una aplicacion Next.js 16 minimalista cuyo unico proposito es servir invitaciones publicas. No tiene panel de administracion, no tiene login, no tiene base de datos propia. Es un servidor de contenido estatico inteligente que:
- Recibe el slug de una invitacion (ej.
/i/boda-ana-carlos) - Consulta metadata a nvito-api
- Descarga el HTML compilado desde CDN
- Inyecta meta tags, analytics, y scripts de seguridad
- Sanitiza el HTML con DOMPurify (prevencion XSS)
- Devuelve una pagina HTML completa lista para renderizar
La aplicacion usa ISR (Incremental Static Regeneration) para cachear las paginas y servirlas como HTML estatico, revalidandolas automaticamente cada hora o bajo demanda cuando el anfitrion publica cambios.
2. ISR: Generacion Estatica Incremental
Primera visita (cache MISS)
Cuando un invitado accede a una invitacion por primera vez, Next.js ejecuta el pipeline completo de renderizado y cachea el resultado como HTML estatico. Las visitas subsiguientes reciben este HTML cacheado directamente, sin ejecutar el pipeline.
Revalidacion por tiempo
El route handler configura revalidate: 3600 (1 hora). Despues de 1 hora, la siguiente visita servira el HTML cacheado (stale) mientras en background se regenera una nueva version. Este patron se conoce como stale-while-revalidate.
Beneficios
| Metrica | Sin ISR | Con ISR |
|---|---|---|
| Latencia primera visita | 200-500ms | 200-500ms |
| Latencia visitas posteriores | 200-500ms | < 10ms |
| Carga en nvito-api | Cada visita | 1 vez/hora |
| Carga en CDN | Cada visita | 1 vez/hora |
3. Revalidacion On-Demand
Cuando el anfitrion publica o modifica una invitacion desde el admin, nvito-api dispara un webhook que invalida el cache de ISR inmediatamente.
Flujo
- El anfitrion modifica la invitacion y hace clic en "Publicar" en nvito-admin
- nvito-admin ejecuta server action que llama a
PUT /v1/invitations/{id}/publishen nvito-api - nvito-api actualiza el estado de la invitacion a
PUBLISHED - nvito-api llama a
POST {INVITATIONS_URL}/api/revalidatecon:- Header
Authorization: Bearer {REVALIDATION_SECRET} - Body
{ paths: ["/i/boda-ana-carlos"] }
- Header
- nvito-invitations valida el secret con
crypto.timingSafeEqual() - Ejecuta
revalidatePath()de Next.js para cada path - La siguiente visita a la invitacion obtiene la version actualizada
Validacion del secret
La comparacion del secret de revalidacion usa timing-safe comparison para prevenir timing attacks:
- Convierte ambos strings (recibido y esperado) a
Buffer - Si las longitudes difieren, ejecuta
timingSafeEqual(expected, expected)para mantener tiempo constante - Si las longitudes coinciden, ejecuta
timingSafeEqual(received, expected) - Retorna el resultado booleano
Este patron asegura que un atacante no pueda inferir caracteres correctos del secret midiendo el tiempo de respuesta.
4. Pipeline de Renderizado del Servidor
El route handler /i/[slug]/route.ts orquesta el pipeline completo. Cada paso es un servicio independiente con responsabilidad unica.
Paso 1: Fetch de metadata (InvitationFetcher)
- Llama a
GET {API_URL}/v1/invitations/public/{slug} - Valida la respuesta con
invitationDataSchema.safeParse() - Verifica que
status === 'PUBLISHED' - Si el slug no existe o no esta publicado: retorna 404
Paso 2: Fetch de HTML (HtmlProcessor)
- Construye la URL del HTML compilado:
{CDN_URL}/templates/{organizationId}/{invitationId}/index.html - Valida la URL con
CdnValidatorantes de hacer fetch (prevencion SSRF) - Descarga el HTML desde CDN
- Inyecta meta tags OpenGraph y Twitter Card en el
<head> - Inyecta el script de analytics con escape context-aware
Paso 3: Inyeccion de meta tags
<meta property="og:title" content="{event.name}" />
<meta property="og:description" content="{event.description}" />
<meta property="og:image" content="{event.heroImageUrl}" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
Los valores se escapan con escapeHtmlAttr() para prevenir XSS via atributos.
Paso 4: Inyeccion de analytics
El script de analytics se genera con AnalyticsScript.generate():
- Inserta un
<script>con un objeto de configuracion escapado - Usa
escapeJsString()para todos los valores dinamicos (invitationId, slug, eventId) - Registra: page views, scroll depth, tiempo en pagina, clics en secciones, RSVP submissions
Paso 5: Retorno
Retorna un Response con el HTML completo y headers:
| Header | Valor |
|---|---|
Content-Type | text/html; charset=utf-8 |
Cache-Control | s-maxage=3600, stale-while-revalidate |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
5. Prevencion SSRF
CdnValidator protege contra ataques SSRF (Server-Side Request Forgery) validando que las URLs de CDN apunten exclusivamente a hosts permitidos.
Whitelist de hosts
| Host | Ambiente |
|---|---|
cdn.nvito.mx | Produccion |
localhost | Desarrollo |
127.0.0.1 | Desarrollo |
minio (Docker) | Desarrollo |
Validacion
- Parsear la URL con
new URL() - Extraer el hostname
- Verificar que este en la whitelist
- Rechazar IPs privadas (10.x, 172.16-31.x, 192.168.x) excepto en desarrollo
- Rechazar esquemas que no sean
https(ohttpsolo en desarrollo)
Si la URL no pasa la validacion, el pipeline retorna un error 500 generico sin revelar detalles de la URL rechazada.
6. Seguridad: Sanitizacion y Escape
Escape context-aware
nvito-invitations implementa escape especifico segun el contexto donde se inyecta un valor dinamico:
| Contexto | Funcion | Ejemplo |
|---|---|---|
| Dentro de string JS | escapeJsString() | Variable JS con valor escapado |
| Atributo HTML | escapeHtmlAttr() | Atributo content con valor escapado |
escapeJsString() reemplaza: \ a \\, " a \", ' a \', < a \x3C, > a \x3E, / a \/, y caracteres de control.
escapeHtmlAttr() reemplaza: & a &, " a ", ' a ', < a <, > a >.
DOMPurify (client-side)
El componente InvitationRenderer sanitiza el HTML antes de insertarlo en el DOM usando DOMPurify:
- Configuracion explicita de
ADD_ATTRcon lista blanca de atributosdata-*permitidos (ej.data-section-id,data-transition) - Nunca usa
ALLOW_DATA_ATTR: true(permitiriadata-*arbitrarios) - Elimina:
<script>,<iframe>,<object>,<embed>,onclick,onerror, y demas vectores XSS - El HTML sanitizado se inserta en el DOM de forma segura tras pasar por DOMPurify
7. Renderizado en el Cliente
Una vez que el HTML llega al browser, el componente React InvitationRenderer toma el control.
Pipeline del cliente
- HTML sanitization: DOMPurify limpia el HTML recibido
- Font injection:
FontInjectorextrae las fuentes declaradas en CSS y las inyecta como<link>tags de Google Fonts - Loader: Muestra la animacion de carga seleccionada (CSS clasico o Canvas 2D cinematografico)
- Render: Inserta el HTML sanitizado en el DOM
- Transitions: El motor de transiciones inicializa los listeners para scroll entre secciones fullpage
- Analytics: Registra listeners para tracking de interacciones
Motor de transiciones Canvas 2D
Las invitaciones con transiciones cinematograficas usan el siguiente ciclo para cada cambio de seccion:
- CLOSE: Un overlay cubre la seccion actual mientras una animacion Canvas se ejecuta (ej. petalos cayendo, confetti estallando)
- STAMP: Se muestra brevemente el monograma de la pareja con el nombre de la siguiente seccion
- OPEN: El overlay se retira revelando la nueva seccion, y el Canvas se limpia
Motor de loaders Canvas 2D
Los loaders siguen el mismo patron pero se ejecutan una sola vez al cargar la invitacion:
- CSS clasicos (8): animaciones CSS puras sin Canvas
- Canvas cinematograficos (17): animaciones Canvas 2D con particulas, efectos de luz, etc.
- Todos usan
prefers-reduced-motionpara desactivar animaciones cuando el usuario lo prefiere - En mobile (< 640px), el Canvas se oculta por performance
8. Modo Preview
La ruta /preview/[token] permite al anfitrion previsualizar la invitacion desde el admin antes de publicarla.
Diferencias con la ruta publica
| Aspecto | Ruta publica /i/[slug] | Preview /preview/[token] |
|---|---|---|
| Cache | ISR (1 hora) | cache: 'no-store' |
| Cache-Control | s-maxage=3600 | no-store |
| Indexacion | Permitida | X-Robots-Tag: noindex, nofollow |
| Frame | X-Frame-Options: DENY | X-Frame-Options: SAMEORIGIN |
| CSP frame-ancestors | 'none' | 'self' admin.nvito.mx |
| Autenticacion | Ninguna | Token temporal (1 hora) |
| Estado requerido | PUBLISHED | Cualquiera (incluso IN_CONSTRUCTION) |
Flujo de preview
- El admin solicita un token de preview:
POST /v1/invitations/{id}/preview-token - nvito-api genera un JWT con
exp: 1 horaysub: invitationId - El admin abre un iframe con la URL de preview incluyendo el token
- nvito-invitations valida el token JWT y renderiza la invitacion sin cache
Embebido en el admin
El preview se muestra dentro de un iframe en el panel de administracion. Los CSP headers de la ruta preview permiten frame-ancestors 'self' admin.nvito.mx localhost:3001 para que el iframe funcione tanto en produccion como en desarrollo.
9. Headers de Seguridad y CSP
nvito-invitations configura headers de seguridad separados para rutas publicas y preview.
Headers comunes
| Header | Valor |
|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains |
X-Content-Type-Options | nosniff |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
CSP para rutas publicas
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline' fonts.googleapis.com;
font-src fonts.gstatic.com;
img-src 'self' cdn.nvito.mx data:;
connect-src 'self' api.nvito.mx;
frame-ancestors 'none';
CSP para rutas preview
Identico al publico excepto:
frame-ancestors 'self' admin.nvito.mx localhost:3001;
Esto permite que el admin embeba la preview en un iframe.
10. Rate Limiting
El endpoint /api/revalidate tiene rate limiting in-memory para prevenir abuso.
| Parametro | Valor |
|---|---|
| Ventana | 60 segundos |
| Limite por IP | 10 requests |
| Respuesta al exceder | 429 Too Many Requests |
| Header | Retry-After: {segundos restantes} |
Implementacion
Un Map<string, { count: number, resetAt: number }> almacena contadores por IP. Cada request incrementa el contador y verifica contra el limite. Los contadores se limpian automaticamente cuando expira la ventana.
11. Diagrama del Pipeline Completo
12. Archivos Clave
nvito-invitations
| Archivo | Responsabilidad |
|---|---|
app/i/[slug]/route.ts | Route handler principal: orquesta el pipeline |
app/preview/[token]/route.ts | Route handler de preview sin cache |
app/api/revalidate/route.ts | Endpoint de revalidacion on-demand |
app/lib/services/invitation-fetcher.ts | Fetch metadata + validacion Zod |
app/lib/services/html-processor.ts | Fetch HTML CDN + inyeccion meta/analytics |
app/lib/services/cdn-validator.ts | Validacion SSRF de URLs CDN |
app/lib/services/analytics-script.ts | Generacion del script de analytics |
app/lib/services/html-escape.ts | Escape context-aware (JS string + HTML attr) |
app/lib/services/html-responses.ts | Respuestas HTML de error (404, error generico) |
app/lib/dom/font-injector.ts | Extraccion e inyeccion de Google Fonts |
app/lib/dom/html-sanitizer.ts | DOMPurify con configuracion centralizada |
app/lib/schemas.ts | Schemas Zod (invitationData, previewData) |
app/lib/analytics.ts | Sistema de tracking de eventos |
app/lib/global-tracking.ts | API window.NvitoAnalytics |
next.config.mjs | Headers CSP, output standalone, ISR config |
nvito-api (Backend que alimenta el pipeline)
| Archivo | Responsabilidad |
|---|---|
invitations.controller.ts | Endpoint publico de metadata |
invitations.service.ts | Logica de publicacion y revalidacion webhook |
document-generator.service.ts | Compilacion HTML de invitaciones |