Docs

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

  1. Vision General
  2. ISR: Generacion Estatica Incremental
  3. Revalidacion On-Demand
  4. Pipeline de Renderizado del Servidor
  5. Prevencion SSRF
  6. Seguridad: Sanitizacion y Escape
  7. Renderizado en el Cliente
  8. Modo Preview
  9. Headers de Seguridad y CSP
  10. Rate Limiting
  11. Diagrama del Pipeline Completo
  12. 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:

  1. Recibe el slug de una invitacion (ej. /i/boda-ana-carlos)
  2. Consulta metadata a nvito-api
  3. Descarga el HTML compilado desde CDN
  4. Inyecta meta tags, analytics, y scripts de seguridad
  5. Sanitiza el HTML con DOMPurify (prevencion XSS)
  6. 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

MetricaSin ISRCon ISR
Latencia primera visita200-500ms200-500ms
Latencia visitas posteriores200-500ms< 10ms
Carga en nvito-apiCada visita1 vez/hora
Carga en CDNCada visita1 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

  1. El anfitrion modifica la invitacion y hace clic en "Publicar" en nvito-admin
  2. nvito-admin ejecuta server action que llama a PUT /v1/invitations/{id}/publish en nvito-api
  3. nvito-api actualiza el estado de la invitacion a PUBLISHED
  4. nvito-api llama a POST {INVITATIONS_URL}/api/revalidate con:
    • Header Authorization: Bearer {REVALIDATION_SECRET}
    • Body { paths: ["/i/boda-ana-carlos"] }
  5. nvito-invitations valida el secret con crypto.timingSafeEqual()
  6. Ejecuta revalidatePath() de Next.js para cada path
  7. 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:

  1. Convierte ambos strings (recibido y esperado) a Buffer
  2. Si las longitudes difieren, ejecuta timingSafeEqual(expected, expected) para mantener tiempo constante
  3. Si las longitudes coinciden, ejecuta timingSafeEqual(received, expected)
  4. 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 CdnValidator antes 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:

HeaderValor
Content-Typetext/html; charset=utf-8
Cache-Controls-maxage=3600, stale-while-revalidate
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY

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

HostAmbiente
cdn.nvito.mxProduccion
localhostDesarrollo
127.0.0.1Desarrollo
minio (Docker)Desarrollo

Validacion

  1. Parsear la URL con new URL()
  2. Extraer el hostname
  3. Verificar que este en la whitelist
  4. Rechazar IPs privadas (10.x, 172.16-31.x, 192.168.x) excepto en desarrollo
  5. Rechazar esquemas que no sean https (o http solo 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:

ContextoFuncionEjemplo
Dentro de string JSescapeJsString()Variable JS con valor escapado
Atributo HTMLescapeHtmlAttr()Atributo content con valor escapado

escapeJsString() reemplaza: \ a \\, " a \", ' a \', < a \x3C, > a \x3E, / a \/, y caracteres de control.

escapeHtmlAttr() reemplaza: & a &amp;, " a &quot;, ' a &#39;, < a &lt;, > a &gt;.

DOMPurify (client-side)

El componente InvitationRenderer sanitiza el HTML antes de insertarlo en el DOM usando DOMPurify:

  • Configuracion explicita de ADD_ATTR con lista blanca de atributos data-* permitidos (ej. data-section-id, data-transition)
  • Nunca usa ALLOW_DATA_ATTR: true (permitiria data-* 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

  1. HTML sanitization: DOMPurify limpia el HTML recibido
  2. Font injection: FontInjector extrae las fuentes declaradas en CSS y las inyecta como <link> tags de Google Fonts
  3. Loader: Muestra la animacion de carga seleccionada (CSS clasico o Canvas 2D cinematografico)
  4. Render: Inserta el HTML sanitizado en el DOM
  5. Transitions: El motor de transiciones inicializa los listeners para scroll entre secciones fullpage
  6. 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:

  1. CLOSE: Un overlay cubre la seccion actual mientras una animacion Canvas se ejecuta (ej. petalos cayendo, confetti estallando)
  2. STAMP: Se muestra brevemente el monograma de la pareja con el nombre de la siguiente seccion
  3. 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-motion para 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

AspectoRuta publica /i/[slug]Preview /preview/[token]
CacheISR (1 hora)cache: 'no-store'
Cache-Controls-maxage=3600no-store
IndexacionPermitidaX-Robots-Tag: noindex, nofollow
FrameX-Frame-Options: DENYX-Frame-Options: SAMEORIGIN
CSP frame-ancestors'none''self' admin.nvito.mx
AutenticacionNingunaToken temporal (1 hora)
Estado requeridoPUBLISHEDCualquiera (incluso IN_CONSTRUCTION)

Flujo de preview

  1. El admin solicita un token de preview: POST /v1/invitations/{id}/preview-token
  2. nvito-api genera un JWT con exp: 1 hora y sub: invitationId
  3. El admin abre un iframe con la URL de preview incluyendo el token
  4. 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

HeaderValor
Strict-Transport-Securitymax-age=31536000; includeSubDomains
X-Content-Type-Optionsnosniff
Permissions-Policycamera=(), 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.

ParametroValor
Ventana60 segundos
Limite por IP10 requests
Respuesta al exceder429 Too Many Requests
HeaderRetry-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

ArchivoResponsabilidad
app/i/[slug]/route.tsRoute handler principal: orquesta el pipeline
app/preview/[token]/route.tsRoute handler de preview sin cache
app/api/revalidate/route.tsEndpoint de revalidacion on-demand
app/lib/services/invitation-fetcher.tsFetch metadata + validacion Zod
app/lib/services/html-processor.tsFetch HTML CDN + inyeccion meta/analytics
app/lib/services/cdn-validator.tsValidacion SSRF de URLs CDN
app/lib/services/analytics-script.tsGeneracion del script de analytics
app/lib/services/html-escape.tsEscape context-aware (JS string + HTML attr)
app/lib/services/html-responses.tsRespuestas HTML de error (404, error generico)
app/lib/dom/font-injector.tsExtraccion e inyeccion de Google Fonts
app/lib/dom/html-sanitizer.tsDOMPurify con configuracion centralizada
app/lib/schemas.tsSchemas Zod (invitationData, previewData)
app/lib/analytics.tsSistema de tracking de eventos
app/lib/global-tracking.tsAPI window.NvitoAnalytics
next.config.mjsHeaders CSP, output standalone, ISR config

nvito-api (Backend que alimenta el pipeline)

ArchivoResponsabilidad
invitations.controller.tsEndpoint publico de metadata
invitations.service.tsLogica de publicacion y revalidacion webhook
document-generator.service.tsCompilacion HTML de invitaciones
Esta pagina fue util?