Sitio de demostración de Northlav Studio · catálogo ficticio. Precios y datos no son reales. Solicitar una cotización ›

Documentación técnica.

Cómo está construida esta demo y cómo comprobar por su cuenta cada capacidad del stack.

Resumen

Sitio de demostración de Northlav Studio con un catálogo ficticio de equipos de construcción. Está en producción con el mismo stack de un proyecto headless real:

  • Next.js 15 con App Router y React Server Components.
  • Payload CMS 3 autoalojado en el mismo proyecto.
  • PostgreSQL en Google Cloud SQL.
  • Imágenes y PDF en Google Cloud Storage.
  • Tailwind CSS y shadcn/ui.
  • TypeScript en modo estricto con pnpm, desplegado en Vercel.

Demuestra la conexión serverless a la base, la persistencia de archivos, la actualización del sitio sin volver a desplegar, las especificaciones técnicas estructuradas y un formulario listo para integraciones.

Cómo probarlo

Con el usuario de prueba (rol editor) en el panel:

  1. Archivos: en /admin → Media, suba una imagen o un PDF. Queda en Cloud Storage y se sirve desde el sitio.
  2. Actualización al guardar: edite el resumen de un producto, guarde y recargue su ficha. El cambio aparece sin volver a desplegar.
  3. Especificaciones: abra la Excavadora EX-45: cada especificación es un dato con tipo, valor y unidad.
  4. Cotización: envíe una solicitud desde /cotizar con datos ficticios y véala en el panel → Cotizaciones.
  5. Conexión: todo lo anterior ocurre en funciones de Vercel que leen y escriben en Cloud SQL (sección siguiente).

Infraestructura

Navegador
   │
   ▼
Vercel · funciones Node en iad1 (Washington D. C.)
   ├──► Cloud SQL · PostgreSQL 16 en us-east4
   │     conector oficial de Cloud SQL, TLS con certificado de cliente efímero
   │
   └──► Cloud Storage · bucket privado en us-east4
         los archivos se sirven a través de Payload
  • Conexión: @google-cloud/cloud-sql-connector con IP pública sin redes autorizadas. El conector obtiene un certificado efímero con la identidad de Google y abre un túnel TLS verificado; la instancia exige certificados de cliente.
  • Pool por instancia de función: máximo 3 conexiones (DB_POOL_MAX), 10 s de inactividad y 10 s de espera para conectar. El total posible es DB_POOL_MAX × instancias activas, y se contrasta contra el max_connections de la instancia.
  • Regiones: Vercel en iad1 y Cloud SQL en us-east4, geográficamente cercanas para reducir latencia.
  • Build: next build no necesita la base. Las migraciones se aplican a mano, nunca durante el despliegue.

Modelo de datos

Colecciones de Payload: Productos, Categorías, Cotizaciones, Media, Usuarios y Redirecciones.

Las especificaciones técnicas no son texto libre: son filas estructuradas que permiten tablas consistentes, comparar equipos, filtrar por valor y exportar a otros sistemas.

especificaciones: [
  { clave: "potencia",       etiqueta: "Potencia",       tipo: "numero", valorNumero: 45,   unidad: "kW", destacada: true },
  { clave: "peso-operativo", etiqueta: "Peso operativo", tipo: "numero", valorNumero: 5200, unidad: "kg", destacada: true },
  { clave: "motor",          etiqueta: "Motor",          tipo: "texto",  valorTexto: "Motor diésel de demostración" }
]
  • tipo: "numero" exige un número finito y admite unidad; tipo: "texto" exige texto. El valor que no corresponde se limpia al guardar.
  • Las claves son únicas por producto.
  • Variantes con códigos únicos y tabla de medidas ligada a variantes existentes. La cotización valida que la variante exista y esté disponible.

Actualización al guardar

  1. Un editor guarda o elimina un producto, una categoría, un archivo o una redirección.
  2. Payload ejecuta los hooks afterChange o afterDelete.
  3. El hook llama a revalidateTag con la etiqueta del contenido (productos, categorías o redirecciones).
  4. Las consultas públicas están cacheadas con esas etiquetas. La siguiente visita ya no usa la caché y lee los datos nuevos de Cloud SQL.

Sin cambios, las páginas no consultan la base. No hace falta volver a desplegar ni un endpoint manual de revalidación.

Archivos

  • Imágenes (JPG, PNG, WebP) y PDF se suben desde el panel con el adaptador oficial @payloadcms/storage-gcs.
  • Payload genera tres tamaños de cada imagen: 400, 768 y 1600 px.
  • El bucket es privado: los archivos se sirven a través de Payload (/api/media/file/…), así que sus permisos se respetan.
  • En las páginas, next/image entrega WebP o AVIF del tamaño justo para cada pantalla.
  • Los archivos persisten entre despliegues: no viven en el sistema de archivos de la función.

Cotizaciones e integraciones

El formulario de /cotizar y el endpoint POST /api/cotizaciones usan el mismo servicio: validación, comprobación de producto y variante, persistencia e idempotencia.

POST /api/cotizaciones
Content-Type: application/json

{
  "nombre": "Ana", "apellido": "Solís",
  "telefono": "88887777",
  "correo": "ana@ejemplo.com",
  "producto": "generador-gd-60",
  "variante": "GD-60",
  "mensaje": "Para dos semanas",
  "idempotencyKey": "b3f1c2d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}
CódigoSignificado
201Solicitud creada. Devuelve id y eventoId.
200Reintento con la misma clave y el mismo contenido: devuelve la solicitud existente, sin duplicar.
400Campos inválidos, producto o variante no disponibles, JSON mal formado o control antispam.
403Origen de navegador no permitido.
409La clave de idempotencia ya se usó con otro contenido.
413 / 415Cuerpo mayor de 16 KB o tipo de contenido distinto de JSON.
429Límite de envíos alcanzado. Incluye Retry-After.
500No se guardó. Se puede reintentar con la misma clave.

Punto de salida para integraciones. Después de guardar, el servicio llama a notificarSolicitud() en src/lib/outbound.ts con un contrato tipado y versionado. Para conectar Brevo, Pub/Sub o una API de conversiones se implementa un adaptador:

export const adaptadorPubSub: AdaptadorSalida = {
  nombre: 'pubsub',
  async enviar(solicitud) {
    // solicitud: { version, solicitudId, eventoId, creadaEn, datos }
    await topic.publishMessage({ json: solicitud })
    return { ok: true }
  },
}

usarAdaptador(adaptadorPubSub)

Un fallo del adaptador no borra la solicitud ni la duplica. En esta demo el adaptador no envía a terceros: solo registra el ID y el resultado.

Medición

  • Aviso de consentimiento con Aceptar y Rechazar. Rechazar no impide enviar la cotización.
  • Con consentimiento y solo después de guardar, se envía al dataLayer:
{ "event": "solicitud_cotizacion", "evento_id": "…", "producto": "generador-gd-60", "variante": "GD-60" }
  • evento_id es el mismo que se guarda con la solicitud: sirve para deduplicar conversiones entre el navegador y el servidor.
  • Un reintento no repite el evento. Nunca incluye nombre, teléfono, correo ni mensaje.
  • No hay identificadores de terceros escritos en el código.

SEO

  • Todo el contenido, los filtros y la búsqueda se renderizan en el servidor y funcionan sin JavaScript.
  • Título, descripción e imagen para redes editables desde el CMS.
  • JSON-LD generado desde el CMS: Product con cada especificación como PropertyValue con su unidad, BreadcrumbList, ItemList, Organization y WebSite.
  • sitemap.xml con solo contenido publicado y robots.txt.
  • Redirecciones 301 administrables desde el panel para el mapa del sitio anterior: un solo salto al destino, sin distinguir mayúsculas ni barra final.

Seguridad y permisos

ColecciónPúblicoEditorAdministrador
ProductosSolo publicadosCrea y edita, sin eliminarTodo
CategoríasLecturaCrea y edita, sin eliminarTodo
MediaLecturaSube y edita, sin eliminarTodo
CotizacionesSin accesoSolo lecturaCambia estado y elimina; datos del cliente de solo lectura
UsuariosSin acceso ni registroSolo su propio usuario, sin cambiar rolTodo
RedireccionesLecturaSin accesoTodo
  • Los permisos se aplican en REST, GraphQL y en las consultas del servidor, no solo en el panel.
  • Cabeceras: Content-Security-Policy con frame-ancestors, X-Frame-Options, Referrer-Policy, X-Content-Type-Options y HSTS.
  • Secretos solo en variables de entorno del servidor. El repositorio no contiene credenciales.
  • Formulario con campo trampa, tiempo mínimo de llenado, límite por teléfono y por IP, y validación de origen.

En el proyecto real

Esta demo funciona en un entorno simplificado en cuentas de Northlav, con datos ficticios. En un proyecto de cliente:

  • Toda la infraestructura se crea en las cuentas del cliente.
  • Entornos separados de desarrollo, previsualización y producción, con base y bucket propios.
  • Autenticación con Google Cloud por federación de identidades, sin llaves de cuenta de servicio.
  • Usuarios de base de datos con privilegios mínimos.
  • Límite de envíos global con un almacén compartido.

Video de infraestructura

Recorrido de 3 minutos: la instancia de Cloud SQL, la subida de archivos al bucket, la actualización al guardar y una cotización que llega al panel. La carpeta incluye también los reportes Lighthouse.

Abrir la carpeta con el video ↗