Docs

Flujo de Renderizado de Invitación Publica

Pipeline ISR desde la solicitud hasta el renderizado final en el navegador

PublicadoMarzo 2026Equipo de desarrollo, arquitectos, stakeholders

1. Vision General

nvito-invitations es una aplicación Next.js 16 mínimalista cuyo único proposito es servir invitaciones públicas. No tiene panel de administración, no tiene login, no tiene base de datos propia. Es un servidor de contenido estático inteligente que:

  1. Recibe el slug de una invitación (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 página HTML completa lista para renderizar

La aplicación usa ISR (Incremental Static Regeneration) para cachear las páginas y servirlas como HTML estático, revalidandolas automáticamente cada hora o bajo demanda cuando el anfitrion pública cambios.

2. ISR: Generación Estatica Incremental

Primera visita (cache MISS)

Cuando un invitado accede a una invitación por primera vez, Next.js ejecuta el pipeline completo de renderizado y cachea el resultado como HTML estático. Las visitas subsiguientes reciben este HTML cacheado directamente, sin ejecutar el pipeline.

Revalidación por tiempo

El route handler configura revalidate: 3600 (1 hora). Después de 1 hora, la siguiente visita servira el HTML cacheado (stale) mientras en background se regenera una nueva versión. Este patrón 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. Revalidación On-Demand

Cuando el anfitrion pública o modifica una invitación desde el admin, nvito-api dispara un webhook que invalida el cache de ISR inmediatamente.

Flujo

  1. El anfitrion modifica la invitación 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 invitación 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 invitación obtiene la versión actualizada

Validación del secret

La comparacion del secret de revalidación 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 patrón 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 orquestá 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 está 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: Inyección 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: Inyección de analytics

El script de analytics se genera con AnalyticsScript.generate():

  • Inserta un <script> con un objeto de configuración escapado
  • Usa escapeJsString() para todos los valores dinámicos (invitationId, slug, eventId)
  • Registra: page views, scroll depth, tiempo en página, 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.mxProducción
localhostDesarrollo
127.0.0.1Desarrollo
minio (Docker)Desarrollo

Validación

  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 validación, 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 específico según el contexto donde se inyecta un valor dinámico:

ContextoFunciónEjemplo
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:

  • Configuración 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 demás 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. Guest personalization: Si la URL incluye ?t={shortCode}, se resuelven los datos del invitado via GET /v1/invitations/public/guest/{shortCode} y se inyectan en el loader y la sección RSVP usando textContent (prevencion XSS)
  4. Loader: Muestra la animación de carga seleccionada (CSS clasico o Canvas 2D cinematografico) con el nombre personalizado del invitado o texto generico de fallback
  5. Render: Inserta el HTML sanitizado en el DOM
  6. Transitions: El motor de transiciones inicializa los listeners para scroll entre secciones fullpage
  7. RSVP personalization: La sección RSVP muestra un saludo personalizado con el nombre del invitado si se resolvio el shortCode
  8. Calendar button: Botón flotante "Agendar" en esquina inferior derecha (position: fixed) con contraste dinámico
  9. 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 sección:

  1. CLOSE: Un overlay cubre la sección actual mientras una animación Canvas se ejecuta (ej. petalos cayendo, confetti estallando)
  2. STAMP: Se muestra brevemente el monograma de la pareja con el nombre de la siguiente sección
  3. OPEN: El overlay se retira revelando la nueva sección, y el Canvas se limpia

Motor de loaders Canvas 2D

Los loaders siguen el mismo patrón pero se ejecutan una sola vez al cargar la invitación:

  • 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 invitación desde el admin antes de publicarla.

Diferencias con la ruta publica

AspectoRuta pública /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
AutenticaciónNingunaToken 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 invitación sin cache

Embebido en el admin

El preview se muestra dentro de un iframe en el panel de administración. Los CSP headers de la ruta preview permiten frame-ancestors 'self' admin.nvito.mx localhost:3001 para que el iframe funcione tanto en producción como en desarrollo.

9. Headers de Seguridad y CSP

nvito-invitations configura headers de seguridad separados para rutas públicas y preview.

Headers comunes

HeaderValor
Strict-Transport-Securitymax-age=31536000; includeSubDomains
X-Content-Type-Optionsnosniff
Permissions-Policycamera=(), microphone=(), geolocation=()

CSP para rutas públicas

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 público 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
Respuestá al exceder429 Too Many Requests
HeaderRetry-After: {segundos restantes}

Implementación

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 automáticamente cuando expira la ventana.

11. Diagrama del Pipeline Completo

12. Archivos Clave

nvito-invitations

ArchivoResponsabilidad
app/i/[slug]/route.tsRoute handler principal: orquestá el pipeline
app/preview/[token]/route.tsRoute handler de preview sin cache
app/api/revalidate/route.tsEndpoint de revalidación on-demand
app/lib/services/invitation-fetcher.tsFetch metadata + validación Zod
app/lib/services/html-processor.tsFetch HTML CDN + inyección meta/analytics
app/lib/services/cdn-validator.tsValidación SSRF de URLs CDN
app/lib/services/analytics-script.tsGeneración 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.tsExtracción e inyección de Google Fonts
app/lib/dom/html-sanitizer.tsDOMPurify con configuración 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 público de metadata
invitations.service.tsLógica de publicación y revalidación webhook
document-generator.service.tsCompilacion HTML de invitaciones
Esta pagina fue util?