Docs

Invitaciones Publicas (nvito-invitations)

Tabla de Contenidos

  1. Estructura del Proyecto
  2. Flujo SSG + ISR
  3. InvitationRenderer
  4. Modo Preview para Admin
  5. Tracking de Analitica
  6. SEO y Metadata Dinamica
  7. Webhook de Revalidacion
  8. Estrategia de Cache
  9. Headers de Seguridad
  10. Arquitectura SOLID
  11. Testing
  12. 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)

DependenciaVersionProposito
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.xValidacion de respuestas API en runtime
ua-parser-js^2.0.9Deteccion de dispositivo/navegador para analitica
web-vitals^4.xMetricas 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

RutaTipoDescripcion
/EstaticaPagina principal / landing
/i/[slug]SSG + ISRInvitacion publica por slug
/preview/[token]Dinamica (SSR)Vista previa con token JWT
/api/revalidateAPI RouteWebhook 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

  1. Inyeccion de CSS: Crea un elemento <style> en el <head> del documento con atributo data-invitation-slug para identificacion
  2. Inyeccion de HTML: Establece el innerHTML del contenedor principal
  3. Inyeccion 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 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 suppressHydrationWarning para evitar errores de hidratacion con contenido dinamico
  • Los refs (styleRef, scriptRef) previenen duplicacion de inyeccion 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 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 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 expiracion 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:

EventoDescripcionActivacion
VIEWVista de la paginaAutomatica al cargar
RSVP_OPENApertura del formulario RSVPAutomatica via MutationObserver
RSVP_SUBMITEnvio del formulario RSVPAutomatica al submit
LINK_CLICKClic en enlace externoAutomatica via event listener
SHAREAccion de compartirVia window.NvitoAnalytics
DOWNLOADDescarga de archivoVia 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> 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 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) via safeParse() 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

NivelTTLProposito
ISR (Next.js)3600s (1 hora)Cache de pagina generada estaticamente
Fetch API metadatarevalidate: 3600Cache de metadata de la invitacion
Fetch HTML/CSS/JSrevalidate: 3600Cache de archivos compilados del CDN
Revalidacion 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)

Regeneracion

La pagina se regenera en dos escenarios:

  1. Automatica: Cuando el TTL de 1 hora expira y llega una nueva solicitud
  2. On-demand: Cuando el API envia el webhook de revalidacion al publicar una invitacion

Headers de Seguridad

Headers HTTP Configurados

HeaderValorProteccion
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-OptionsSAMEORIGINProteccion 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 via ADD_ATTR (sin ALLOW_DATA_ATTR: true que permitiria data-* 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-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 produccion
npm run build

# Desarrollo con Bitwarden Secrets Manager
npm run dev:bws

Variables de Entorno

VariableDescripcionDefault
NEXT_PUBLIC_API_URLURL base del APIhttp://localhost:3000
NEXT_PUBLIC_CDN_URLURL del CDN para archivos estaticoshttp://localhost:9000
REVALIDATE_SECRETSecreto 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:

ServicioArchivoResponsabilidad
invitation-fetcherlib/services/invitation-fetcher.tsFetch de metadata desde API + validacion Zod
html-processorlib/services/html-processor.tsFetch de HTML/CSS/JS desde CDN + inyeccion de meta tags
cdn-validatorlib/services/cdn-validator.tsValidacion SSRF de URLs CDN contra whitelist de hosts
analytics-scriptlib/services/analytics-script.tsGeneracion 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.

Descomposicion del InvitationRenderer

ServicioArchivoResponsabilidad
font-injectorlib/dom/font-injector.tsExtraccion de URLs de Google Fonts e inyeccion via <link> tags
html-sanitizerlib/dom/html-sanitizer.tsSanitizacion 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

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 Zod1Validacion de datos de invitacion y revalidacion
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 Codigo

IndicadorValor
TypeScript strictHabilitado
any explicitos0
JSDoc100% en metodos publicos
Schemas ZodValidacion en runtime de API responses y webhook body
Test suites17
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
  • 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
Esta pagina fue util?