cms multisitio con payload: la web de iaeste españa y una web para cada comité local


iaeste es una red de estudiantes que consigue prácticas remuneradas en el extranjero para estudiantes de ingeniería, ciencias y otras carreras técnicas. en españa funciona gracias a los comités locales: grupos de estudiantes voluntarios, uno por universidad. he hecho su web nueva: una sola instalación que sirve la web nacional y una web para cada comité que quiera una, todas editables desde un cms y sin tocar código.
todavía no ha sustituido a la web antigua de iaeste.es. mientras tanto se puede ver en iaeste-spain.joeltaylor.business, y la del comité de lleida en lleida.iaeste-spain.joeltaylor.business.
por qué una web por comité
quien busca "iaeste" y quien busca "iaeste lleida" o "prácticas en el extranjero udl" quieren cosas distintas. el primero quiere saber qué es el programa; el segundo, dónde está el despacho del comité, cuándo es la próxima charla y quién se ha ido desde su universidad. una sola web nacional responde bien a la primera búsqueda y mal a la segunda.
la idea es tener muchas webs en vez de una: la nacional para las búsquedas generales, y una por comité para las de su ciudad y su universidad. cada una publica su blog, sus eventos y sus experiencias. el riesgo es que cada comité acabe con una web distinta, hecha por quien estaba ese año y abandonada cuando se va. para evitarlo, todas salen del mismo código y del mismo cms.


todo el contenido, desde el cms
el proyecto es un monorepo con tres aplicaciones: la web, en next.js 16; el cms, en payload 3; y una api pequeña en hono para los formularios y los correos. cada una tiene su base de datos postgres, y se despliegan como tres imágenes docker en un servidor propio con coolify.
en el cms está todo lo que se ve en la web: noticias, eventos, experiencias de estudiantes, preguntas frecuentes, el equipo, los comités con sus universidades y las fotos. las páginas nacionales (portada, estudiantes, empresas, universidades…) tienen campos fijos, y no un constructor de bloques libre, para que el equipo de marketing cambie los textos sin poder romper el diseño.
lo que permite que lo usen personas que no programan:
- los borradores se guardan solos cada 2 segundos, y se puede programar cuándo se publican.
- la vista previa muestra el borrador dentro de la web real, a 390, 820 y 1.440 px de ancho.
- al publicar, el cms avisa a la web para que rehaga solo las páginas afectadas, sin volver a compilar nada.
- cada texto existe en castellano, inglés y catalán, y un panel lateral dice qué parte de la página falta traducir en cada idioma.
- los permisos van por roles. un editor nacional puede tocarlo todo; un editor de comité solo ve los documentos de su comité, y el campo que dice de qué comité es cada documento se rellena solo y no puede cambiarlo.
para el equipo hay 14 guías paso a paso, con 111 capturas que se rehacen con un script cada vez que cambia el cms.
una web para cada comité local, sin código
crear la web de un comité es rellenar un documento en el cms. la oficina nacional va a «webs de comités», crea la del comité, marca «web activada» y publica. a partir de ahí, el comité escribe sus textos con su propio usuario. no hay ningún despliegue por comité: toda la red es una sola aplicación.
la web del comité sale en un subdominio, como lleida.iaeste.es, que le damos nosotros. el comité no tiene que comprar ningún dominio, aunque si ya tiene uno, como iaestelleida.cat, recomiendo usarlo, y puede conectarlo a la misma web.


lo que decide qué web se ve es el host de la petición. proxy.ts, lo que next.js 16 llama ahora al middleware, lee el host, saca de él el comité (siteKeyFromHost) y reescribe la ruta a /[site]/[locale]/…. así, para next.js, la ruta de cada web es distinta, y cada una se guarda en caché por separado. si alguien entra en el subdominio de un comité sin web, acaba en el buscador de comités.
el diseño compartido se consigue limitando lo que se puede cambiar. la portada y las páginas de un comité se montan con 14 tipos de sección: cifras, próximos eventos, últimas noticias, experiencias, equipo, contacto, formulario, cómo funciona el intercambio, llamada a la acción, tarjetas, texto e imagen, carrusel de fotos, logos y cita. la única decisión visual es en qué lado va la imagen y si el fondo es claro o azul marino. no hay colores, ni tipografías, ni css.

algunas reglas más lo mantienen ordenado: como mucho una sección de cifras y un formulario por página, como mucho dos páginas propias en el menú, y una página nueva no puede usar la dirección de una fija. el compilador de typescript avisa si alguien añade un tipo de sección y se olvida de dibujarlo.
seo con muchas webs a la vez
con una web por comité publicando contenido, el problema es que google vea la misma noticia en dos sitios y no sepa cuál mostrar. la regla es que cada documento tiene una sola casa:
- una noticia, un evento o una experiencia de un comité con web vive en la web del comité.
- si un editor nacional marca «mostrar también en iaeste.es», sale también en la nacional, pero el enlace lleva a la web del comité y la etiqueta canónica apunta allí.
- si alguien abre ese documento desde la otra web, se le redirige a su casa.
- el sitemap, el robots.txt y el llms.txt son por host, y cada web solo incluye los documentos que son suyos.
el resto es lo que haría en cualquier web, pero para cada web y cada idioma:
- las rutas y los slugs están traducidos (
/es/estudiantes,/en/students,/ca/estudiants), conhreflangpara cada idioma yx-defaulten castellano. - datos estructurados en json-ld:
NGOen cada página, con el comité como parte de iaeste españa e iaeste españa como parte de iaeste, y tambiénArticle,EventyFAQPage. - cada página tiene su imagen para compartir, generada con el título y la foto de la página y recomprimida para que quede por debajo de los 300 kb que aceptan whatsapp y linkedin.
- cada página existe también en markdown, añadiendo
.mda la dirección, para los asistentes de ia. - las direcciones de la web antigua en wordpress redirigen con un 301 a las nuevas.
- un script,
check:seo, recorre el sitemap de una web en marcha y comprueba títulos, descripciones, canónicas, que loshreflangse correspondan, las imágenes para compartir y el json-ld.
si en tu universidad no hay iaeste
de los 12 comités que hay en la lista, hoy solo 7 están activos. mucha gente que busca iaeste estudia en una universidad donde no hay ninguno. la web los trata como posibles fundadores de un comité nuevo.

el recorrido es este:
- en el buscador de comités, una búsqueda sin resultados muestra «¿no hay comité en tu ciudad?» con lo que has buscado. el buscador funciona también sin javascript, porque filtra en el servidor.
- el botón baja al formulario con «otra universidad» ya elegida, y entre los intereses está «unirme a un comité o fundar uno».
- la api mira si la universidad que has escrito es de algún comité activo. si lo es, avisa a ese comité; si no, el contacto va a la oficina nacional.
- en el cms, la oficina nacional tiene un informe, «universidades sin comité», con los contactos agrupados por universidad. sirve para decidir dónde abrir el siguiente comité.

además, las analíticas registran cada búsqueda del buscador con el número de resultados, y las de cero resultados dicen qué ciudades buscan iaeste sin encontrarlo.
el siguiente paso es una página para cada universidad sin comité, para que quien busque "iaeste" y el nombre de su universidad encuentre una página que le explique que todavía no hay y cómo fundarlo.
lo que no esperaba
- los filtros de correo de universidades y empresas abren todos los enlaces de un mensaje para comprobarlos. si confirmar la suscripción fuera solo abrir el enlace, el filtro confirmaría a todo el mundo. la confirmación es un post: la página lo envía sola con javascript, algo que un filtro no hace, y sin javascript hay un botón.
- el límite de formularios es de 60 por dirección ip cada 15 minutos, porque en una charla en la universidad un aula entera envía el formulario desde la misma red.
- satori, lo que dibuja las imágenes para compartir, no sabe leer los webp que sirve el cms, y todas las tarjetas con foto daban un error 500. ahora la foto se recorta y se pasa a jpeg con sharp antes.
- la imagen docker del cms pesa unos 1,7 gb y la de la web unos 400 mb: el modo standalone de next.js deja fuera el comando
payload migrate, que es el que aplica las migraciones antes de arrancar. - la validación del formulario, con zod, solo se carga cuando alguien empieza a rellenarlo, y eso quitó unos 90 kb comprimidos de las páginas que lo tienen.
en cifras
- 7 comités activos de 12 en la lista, en 3 idiomas, con 14 tipos de sección.
- unas 41.000 líneas de typescript sin contar el código generado ni las migraciones: 17.800 en la web, 12.100 en el cms, 2.900 en la api y 7.800 en los paquetes compartidos.
- 452 tests en 67 ficheros, que se ejecutan con cada cambio contra una base de datos postgres real.
contado sobre el repositorio el 12 de octubre de 2026.