Docs

Invitaciones Publicas

Aplicación Next.js 16 mínimalista para servir invitaciones públicas con SSG, ISR, tracking de analitica y revalidación on-demand.

PublicadoMarzo 2026Equipo de desarrollo, arquitectos, stakeholders

Estructura del Proyecto

nvito-invitations es una aplicación Next.js 16 mínimalista cuyo único proposito es servir las invitaciones públicas generadas por el sistema. Su diseño prioriza rendimiento y tiempo de carga, manteniendo las dependencias al mínimo absoluto.

Agnostico al origen: La aplicación 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 invitación publicada es simplemente un htmlUrl que se sirve como HTML puro.

Dependencias de Producción (solo 7)

DependenciaVersiónProposito
next^16.1.6Framework SSG/ISR
react^19.2.3Libreria de UI
react-dom^19.2.3Renderizado DOM
dompurify^3.xSanitizacion HTML segura
zod^3.xValidación de respuestas API en runtime
ua-parser-js^2.0.9Detección de dispositivo/navegador para analitica
web-vitals^4.xMetricas de rendimiento web (LCP, INP, CLS)

Esta arquitectura ultraligera garantiza que la aplicación sea extremadamente rápida y tenga una superficie de ataque mínima.

Estructura de Archivos

Estructura de Directorios

nvito-invitations/
├── app/
├── layout.tsxLayout raiz (html lang="es")
├── page.tsxPagina principal (landing)
├── globals.cssEstilos base minimos
├── i/
└── [slug]/
├── route.tsRoute handler SSG+ISR (orquestador ~35 LOC)
├── InvitationRenderer.tsxClient component de renderizado
├── not-found.tsxPagina 404 para invitacion
└── __tests__/
└── route.test.tsTests de seguridad y flujo
├── preview/
└── [token]/
├── route.tsVista previa con token JWT
├── not-found.tsx404 para preview invalido
└── __tests__/
└── route.test.tsTests de preview
├── api/
└── revalidate/
├── route.tsWebhook de revalidacion ISR
└── __tests__/
└── route.test.tsTests de revalidacion
└── lib/
├── analytics.tsSistema de tracking de eventos
├── global-tracking.tsFunciones globales para JS de invitacion
├── schemas.tsSchemas Zod (invitationData, previewData, revalidateRequest)
├── html-responses.tsRespuestas HTML error reutilizables
├── error-reporting.tsReporte de errores estructurado
├── web-vitals.tsMetricas de rendimiento web
├── services/Servicios SOLID (SRP)
├── invitation-fetcher.tsFetch API + validacion Zod
├── html-processor.tsFetch CDN + inyeccion meta/analytics
├── cdn-validator.tsValidacion SSRF de URLs CDN
├── analytics-script.tsGeneracion de script con escape XSS
└── __tests__/Tests unitarios por servicio
├── dom/Servicios de manipulacion DOM
├── font-injector.tsExtraccion e inyeccion de Google Fonts
├── html-sanitizer.tsSanitizacion 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 Aplicación

RutaTipoDescripción
/EstaticaPágina principal / landing
/i/[slug]SSG + ISRInvitación pública por slug
/preview/[token]Dinamica (SSR)Vista previa con token JWT
/api/revalidateAPI RouteWebhook para revalidación on-demand

Flujo SSG + ISR

Las invitaciones públicas utilizan Static Site Generation (SSG) combinado con Incremental Static Regeneration (ISR) para lograr tiempos de carga instantaneos con contenido actualizable.

Configuración ISR

La página /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 generación estática
export const dynamicParams = true;     // Permitir generación on-demand de nuevas rutas

Generación Estatica

La función 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. Después de la primera visita, la página 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 invitación en el DOM.

Funcionamiento

  1. Inyección de CSS: Crea un elemento <style> en el <head> del documento con atributo data-invitation-slug para identificacion
  2. Inyección de HTML: Establece el innerHTML del contenedor principal
  3. Inyección de JS (opcional): Crea un elemento <script> dentro del contenedor
  4. Tracking: Registra page view y activa tracking global al montar
  5. Cleanup: Al desmontar, remueve los elementos <style> y <script> inyectados y limpia el tracking global

Props

interface InvitationRendererProps {
  html: string;         // HTML compilado de la invitación
  css: string;          // CSS compilado
  js?: string;          // JS compilado (opcional)
  slug: string;         // Slug de la invitación
  invitationId: string; // ID para tracking de analitica
}

Aislamiento

  • El componente usa suppressHydrationWarning para evitar errores de hidratacion con contenido dinámico
  • Los refs (styleRef, scriptRef) previenen duplicacion de inyección en re-renders
  • El viewTrackedRef asegura 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 invitación antes de publicarla. Se accede desde el panel admin mediante un iframe.

Caracteristicas

  • Autenticación por token JWT: El token se genera en el backend y tiene expiración
  • 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 fullDocument HTML completo
  • Renderizado directo: Usa dangerouslySetInnerHTML para inyectar el documento completo

Flujo de Preview

  1. Admin abre el editor visual en nvito-admin
  2. El admin solicita un token de preview al API
  3. El API genera un JWT con expiración corta
  4. nvito-admin carga un iframe con /preview/{token}
  5. nvito-invitations obtiene el HTML compilado del API usando el token
  6. Se renderiza el documento completo sin cache

Tracking de Analitica

Tipos de Eventos

El sistema de analitica registra 6 tipos de eventos:

EventoDescripciónActivación
VIEWVista de la páginaAutomática al cargar
RSVP_OPENApertura del formulario RSVPAutomática via MutationObserver
RSVP_SUBMITEnvio del formulario RSVPAutomática al submit
LINK_CLICKClic en enlace externoAutomática via event listener
SHAREAcción de compartirVia window.NvitoAnalytics
DOWNLOADDescarga de archivoVia window.NvitoAnalytics

Datos Recopilados

Cada evento de analitica incluye:

  • visitorId: ID único 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 módulo global-tracking.ts expone un objeto window.NvitoAnalytics que el JavaScript generado de la invitación puede utilizar para registrar eventos personalizados:

// Disponible dentro del JS de la invitación
window.NvitoAnalytics.trackRsvpOpen(invitationId);
window.NvitoAnalytics.trackShare(invitationId, 'whatsapp');
window.NvitoAnalytics.trackDownload(invitationId, 'pdf');

Auto-tracking

El sistema configura automáticamente:

  • Links externos: Detecta clics en <a> con href externo
  • Formularios RSVP: Usa MutationObserver para detectar cuando un formulario RSVP aparece en el DOM, y submit listener para registrar envios

SEO y Metadata Dinamica

Cada invitación genera metadata única para SEO y redes sociales usando generateMetadata():

Open Graph

openGraph: {
  title: `Invitación - ${slug}`,
  description: `Te invitamos a nuestro evento especial - ${eventName}`,
  type: 'website',
  url: `/i/${slug}`,
}

Twitter Card

twitter: {
  card: 'summary_large_image',
  title: `Invitación - ${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 Revalidación

Cuando un administrador pública o actualiza una invitación, el API envia un webhook a nvito-invitations para invalidar el cache ISR y regenerar la página.

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
  • Validación Zod: El body se valida con revalidateRequestSchema (paths como array de strings con regex, slug con regex y max 200 chars) via safeParse() en lugar de validación manual
  • Rate limiting: Máximo 10 requests por ventana de tiempo; el request 11 retorna 429
  • Metodo HTTP: Solo acepta POST; GET retorna 405
  • Logging detallado: Cada revalidación exitosa o fallida se registra en logs
  • Respuestá estructurada: Retorna timestamp, slug y resultado por cada path

Estrategia de Cache

Niveles de Cache

NivelTTLProposito
ISR (Next.js)3600s (1 hora)Cache de página generada estáticamente
Fetch API metadatarevalidate: 3600Cache de metadata de la invitación
Fetch HTML/CSS/JSrevalidate: 3600Cache de archivos compilados del CDN
Revalidación on-demandInstantaneaVia 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)

Regeneración

La página se regenera en dos escenarios:

  1. Automática: Cuando el TTL de 1 hora expira y llega una nueva solicitud
  2. On-demand: Cuando el API envia el webhook de revalidación al publicar una invitación

Headers de Seguridad

Headers HTTP Configurados

HeaderValorProtección
Content-Security-Policyframe-ancestors 'self' {admin-domains}Previene clickjacking: solo el panel admin puede cargar invitaciones en iframes
Strict-Transport-Securitymax-age=31536000; includeSubDomainsFuerza HTTPS durante 1 ano, incluyendo subdominios
Permissions-Policycamera=(), microphone=(), geolocation=(), interest-cohort=()Deshabilita APIs innecesarias del navegador y FLoC tracking
X-Content-Type-OptionsnosniffPreviene MIME type sniffing
X-Frame-OptionsSAMEORIGINProtección adicional contra clickjacking

Prevencion XSS en Analytics Script

Las invitaciones públicas inyectan un script de analitica con variables dinámicas (invitationId, API_URL). Para prevenir inyección XSS:

  • escapeJsString(): Función 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

Validación SSRF de URLs CDN

La función 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 via ADD_ATTR (sin ALLOW_DATA_ATTR: true que permitiria data-* arbitrarios)
  • Extracción 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-dynamic para 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 producción
npm run build

# Desarrollo con Bitwarden Secrets Manager
npm run dev:bws

Variables de Entorno

VariableDescripciónDefault
NEXT_PUBLIC_API_URLURL base del APIhttp://localhost:3000
NEXT_PUBLIC_CDN_URLURL del CDN para archivos estáticoshttp://localhost:9000
REVALIDATE_SECRETSecreto compartido para webhook de revalidación(requerido)

Arquitectura SOLID

El proyecto sigue principios SOLID, especialmente Single Responsibility Principle (SRP). Cada archivo tiene una única razon de cambio.

Descomposición del Route Handler

El route handler /i/[slug]/route.ts originalmente mezclaba 10+ responsabilidades en 158 lineas. Fue descompuesto en servicios con responsabilidad unica:

ServicioArchivoResponsabilidad
invitation-fetcherlib/services/invitation-fetcher.tsFetch de metadata desde API + validación Zod
html-processorlib/services/html-processor.tsFetch de HTML/CSS/JS desde CDN + inyección de meta tags
cdn-validatorlib/services/cdn-validator.tsValidación SSRF de URLs CDN contra whitelist de hosts
analytics-scriptlib/services/analytics-script.tsGeneración del <script> de analitica con escape XSS
html-responseslib/html-responses.tsRespuestas HTML de error reutilizables (404, error generico)

El route handler quedo como orquestador limpio de ~35 lineas que delega a cada servicio.

Descomposición del InvitationRenderer

ServicioArchivoResponsabilidad
font-injectorlib/dom/font-injector.tsExtracción de URLs de Google Fonts e inyección via <link> tags
html-sanitizerlib/dom/html-sanitizer.tsSanitizacion DOMPurify con configuración centralizada

Testing

El proyecto cuenta con 16 suites de prueba y 180 tests individuales, todos con 100% de tasa de exito.

Cobertura por Area

AreaSuitesQue prueban
Route handlers3/i/[slug], /preview/[token], /api/revalidate
Servicios SOLID4invitation-fetcher, html-processor, cdn-validator, analytics-script
DOM utilities2font-injector, html-sanitizer
Schemas Zod1Validación de datos de invitación y revalidación
Lib utilities3html-responses, error-reporting, web-vitals
Existentes3Tests 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 Código

IndicadorValor
TypeScript strictHabilitado
any explicitos0
JSDoc100% en métodos públicos
Schemas ZodValidación en runtime de API responses y webhook body
Test suites16
Tests individuales180
Tasa de exito100%
Vulnerabilidades criticas0 (timing-safe, XSS escaped, SSRF validated, HSTS enabled)
Violaciones SRP0 (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
  • Revalidación: 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
Esta pagina fue util?