Sitio Web Institucional y Headless CMS
Universidad Nacional de Colombia · 2026 - Actualmente
El Centro de Prototipado necesitaba un sitio público que su propio equipo pudiera mantener al día — portafolio, catálogo de equipos, personas, FAQ, contacto — sin abrir un editor de código ni esperar a un desarrollador. La respuesta fue convertir Notion en el CMS: el equipo edita las bases que ya usa, y el sitio se sincroniza y se republica solo.
Arquitectura y Stack Tecnológico
Arquitectura Central
- Monorepo: Turborepo + pnpm workspaces, con las versiones compartidas fijadas en un único bloque
catalog: - App:
@cen/web— Next.js 16 (App Router), React 19, TypeScript, Server Components por defecto - CMS: Notion vía
@notionhq/client(versión de API2025-09-03, modelo de data sources) — sin base de datos propia - Caché: la directiva
"use cache"concacheTag+cacheLife("max"), refrescada solo on-demand - Imágenes: Cloudinary, alimentado por un sync idempotente desde las propiedades file de Notion
- Correo: Resend para las solicitudes de contacto, con plantillas de
@react-email/componentsy un adjunto de@react-pdf/renderer - 3D: Three.js para el robot del hero,
cobepara el globo — imports solo en cliente - UI: shadcn/ui + Tailwind CSS 4, tema oscuro por defecto
Arquitectura en Capas
Flujo de una Petición
Características Principales
Características de un Vistazo
Notion como CMS headless
Seis bases cuelgan de una sola página de CMS: Portafolio, Tecnologías, Equipo, FAQ, Configuración y Solicitudes. Cada lectura filtra por un checkbox Publicado, así publicar es marcar una casilla y no hacer un deploy. Las entradas de portafolio son híbridas: las propiedades estructuradas alimentan el layout (hero, reto, solución, tecnologías) mientras el cuerpo de la página de Notion se trae como markdown y se renderiza con react-markdown + remark-gfm.
Revalidación on-demand desde un botón de Notion
POST /api/revalidate?secret=…&tag=… valida un secreto compartido y llama a revalidateTag. Cada pestaña de base en Notion lleva un botón «Actualizar» conectado a ese endpoint, así el ciclo de edición es: editar → clic → en vivo. Sin tag refresca los cinco tags de contenido.
Un sync de imágenes que sobrevive a las URLs que expiran
Las URLs de archivos subidos a Notion expiran en cerca de una hora, así que no se pueden servir directo. El editor sube el archivo a una propiedad file, y el sync lo sube a Cloudinary y escribe la secure_url permanente de vuelta en la propiedad destino. El public_id de Cloudinary incluye un hash del archivo de origen, así que volver a correr el sync solo re-sube lo que de verdad cambió.
Un formulario con cuatro formas
Un solo formulario, cuatro tipos (general, servicios, visita docente, visita externa), cada uno con su esquema Zod y sus campos — incluidos un roster dinámico de estudiantes (useFieldArray) y un pad de firma en canvas. El resolver cambia de esquema según el tipo activo.
Una solicitud, dos destinos
La server action valida, arma un PDF con los datos, el roster y la firma, y luego dispara ambos destinos en paralelo: un correo al Centro por Resend (con el PDF adjunto y replyTo apuntando al solicitante, para que responder sea un correo normal desde Gmail) y una fila en la base Solicitudes de Notion, que funciona como el registro consultable.
Destacados Técnicos
Una sola versión de React para todas las apps
Las versiones de las dependencias transversales (react, next, typescript, tailwindcss, eslint, prettier) se fijan una vez en el catalog: del workspace y se referencian como "catalog:" en cada package.json. Subir una versión es una línea, y desaparece la clase de bug de "funciona en una app pero no en la otra".
Hosts de imagen explícitos, nunca un comodín
images.remotePatterns lista cada host permitido en vez de **. Un comodín ahí convierte el optimizador de imágenes de Next en un vector de SSRF — de esos defaults que en un archivo de configuración parecen inofensivos y no lo son.
El artefacto formal y el registro están separados a propósito
El PDF firmado vive en el hilo de Gmail, que es donde además se responde; Notion guarda la fila estructurada para el seguimiento. Riesgo conocido de la v1, dicho en vez de escondido: si un destino funciona y el otro falla no hay compensación, y al usuario le llega un error genérico.
El contenido hardcodeado es una excepción deliberada y acotada
Los copys decorativos que casi nunca cambian (hero, CTA, franja de capacidades, puntos STEM, pasos del proceso de portafolio) se quedan en código, mientras que tecnologías, métricas de impacto, datos de contacto y misión/visión tienen fuente única en Notion. La regla está escrita en el documento de arquitectura para que las dos nunca deriven en duplicados.
Estructura del Proyecto
cenprototipado/ ├── apps/ │ └── web/ @cen/web — sitio público │ ├── app/(marketing)/ /, /centro, /portafolio[/slug], /tecnologias, /contacto │ ├── app/api/revalidate/ invalidación de caché on-demand + sync de imágenes │ ├── components/sections/ hero, robot 3D, casos, galería, asistente de contacto │ └── lib/ │ ├── notion/ cliente, mapeadores de propiedades, un fetcher por base │ ├── cloudinary/sync propiedad file de Notion → Cloudinary → propiedad url │ ├── forms/ esquemas Zod + configuración de formulario por tipo │ ├── pdf/ documento de solicitud con @react-pdf/renderer │ └── email/ cliente de Resend + plantilla de React Email ├── packages/ ui / auth / db — compartidos, hoy como andamiaje ├── turbo.json └── pnpm-workspace.yaml catalog: versiones compartidas
Impacto y Escalabilidad
- El equipo publica y actualiza el contenido por su cuenta; un cambio de texto ya no necesita un desarrollador ni un deploy.
- El contenido se cachea indefinidamente y solo se refresca on-demand, así el sitio sirve páginas estáticas sin dejar de ser editable.
- El monorepo está dispuesto para que las demás apps del Centro (dashboard interno, LMS) entren como
apps/*sobre los mismos paquetes compartidos; el documento de arquitectura ya especifica la identidad prevista (Logto self-hosted), el almacenamiento (Cloudflare R2) y el modelo de un schema por app en Supabase para ese paso.
Notas
Construido con Next.js 16, Turborepo, la API de Notion como CMS, Three.js, Cloudinary, Resend y shadcn/ui. El código es público en GitHub y el sitio está en vivo en cenprototipado.vercel.app.