cms multisite amb payload: la web d'iaeste espanya i una web per a cada comitè local


iaeste és una xarxa d'estudiants que aconsegueix pràctiques remunerades a l'estranger per a estudiants d'enginyeria, ciències i altres carreres tècniques. a espanya funciona gràcies als comitès locals: grups d'estudiants voluntaris, un per universitat. n'he fet la web nova: una sola instal·lació que serveix la web nacional i una web per a cada comitè que en vulgui una, totes editables des d'un cms i sense tocar codi.
encara no ha substituït la web antiga d'iaeste.es. mentrestant es pot veure a iaeste-spain.joeltaylor.business, i la del comitè de lleida a lleida.iaeste-spain.joeltaylor.business.
per què una web per comitè
algú que busca "iaeste" i algú que busca "iaeste lleida" o "pràctiques a l'estranger udl" volen coses diferents. el primer vol saber què és el programa; el segon vol saber on és el despatx del comitè, quan és la propera xerrada i qui ha marxat des de la seva universitat. una sola web nacional respon bé la primera cerca i malament la segona.
la idea és tenir moltes webs en lloc d'una: la nacional per a les cerques generals, i una per a cada comitè per a les de la seva ciutat i la seva universitat. cada una publica el seu blog, els seus esdeveniments i les seves experiències. el risc d'això és que cada comitè acabi amb una web diferent, feta per qui hi havia aquell any i abandonada quan se'n va. per evitar-ho, totes surten del mateix codi i del mateix cms.


tot el contingut, des del cms
el projecte és un monorepo amb tres aplicacions: la web, en next.js 16; el cms, en payload 3; i una api petita en hono per als formularis i els correus. cada una té la seva base de dades postgres, i es despleguen com a tres imatges docker en un servidor propi amb coolify.
al cms hi ha tot el que es veu a la web: notícies, esdeveniments, experiències d'estudiants, preguntes freqüents, l'equip, els comitès amb les seves universitats i les fotos. les pàgines nacionals (portada, estudiants, empreses, universitats…) tenen camps fixos, i no un constructor de blocs lliure, perquè l'equip de màrqueting canviï els textos sense poder trencar el disseny.
el que fa que el puguin fer servir persones que no programen:
- els esborranys es desen sols cada 2 segons, i es pot programar quan es publiquen.
- la vista prèvia mostra l'esborrany dins la web real, a 390, 820 i 1.440 px d'amplada.
- en publicar, el cms avisa la web perquè refaci només les pàgines afectades, sense tornar a compilar res.
- cada text existeix en castellà, anglès i català, i un panell lateral diu quina part de la pàgina falta traduir a cada idioma.
- els permisos van per rols. un editor nacional ho pot tocar tot; un editor de comitè només veu els documents del seu comitè, i el camp que diu de quin comitè és cada document es posa sol i no el pot canviar.
per a l'equip hi ha 14 guies pas a pas en castellà, amb 111 captures que es tornen a fer amb un script cada cop que canvia el cms.
una web per a cada comitè local, sense codi
crear la web d'un comitè és omplir un document al cms. l'oficina nacional va a «webs de comités», crea la del comitè, marca «web activada» i publica. a partir d'aquí, el comitè escriu els seus textos amb el seu propi usuari. no hi ha cap desplegament per comitè: tota la xarxa és una sola aplicació.
la web del comitè surt a un subdomini, com lleida.iaeste.es, que li donem nosaltres. no cal que el comitè compri cap domini, tot i que si en té un, com iaestelleida.cat, li recomano fer-lo servir i el pot connectar a la mateixa web.


el que decideix quina web es veu és el host de la petició. proxy.ts, el que next.js 16 anomena ara el middleware, llegeix el host, en treu el comitè (siteKeyFromHost) i reescriu la ruta a /[site]/[locale]/…. així la ruta de cada web és diferent per a next.js i cada una es desa a la memòria cau per separat. si algú entra al subdomini d'un comitè que no té web, va a parar al cercador de comitès.
el disseny compartit s'aconsegueix limitant què es pot canviar. la portada i les pàgines d'un comitè es fan amb 14 tipus de secció: xifres, propers esdeveniments, últimes notícies, experiències, equip, contacte, formulari, com funciona l'intercanvi, una crida a l'acció, targetes, text i imatge, carrusel de fotos, logos i una cita. l'única decisió visual és a quin costat va la imatge i si el fons és clar o blau marí. no hi ha colors, ni tipografies, ni css.

algunes regles més ho mantenen endreçat: com a molt una secció de xifres i un formulari per pàgina, com a molt dues pàgines pròpies al menú, i una pàgina nova no pot fer servir l'adreça d'una de fixa. el compilador de typescript avisa si algú afegeix un tipus de secció i s'oblida de dibuixar-lo.
seo amb moltes webs alhora
amb una web per comitè publicant contingut, el problema és que google vegi la mateixa notícia a dos llocs i no sàpiga quin ha de mostrar. la regla és que cada document té una sola casa:
- una notícia, un esdeveniment o una experiència d'un comitè amb web viu a la web del comitè.
- si un editor nacional marca «mostrar también en iaeste.es», també surt a la nacional, però l'enllaç porta a la web del comitè i l'etiqueta canònica apunta allà.
- si algú obre aquell document des de l'altra web, el redirigeix a casa seva.
- el sitemap, el robots.txt i el llms.txt són per host, i cada web només hi posa els documents que són seus.
la resta és el que faria a qualsevol web, però per a cada web i cada idioma:
- les rutes i els slugs estan traduïts (
/es/estudiantes,/en/students,/ca/estudiants), ambhreflangper a cada idioma ix-defaulten castellà. - dades estructurades en json-ld:
NGOa cada pàgina, amb el comitè com a part d'iaeste espanya i iaeste espanya com a part d'iaeste, i tambéArticle,EventiFAQPage. - cada pàgina té la seva imatge per compartir, generada amb el títol i la foto de la pàgina i recomprimida perquè quedi per sota dels 300 kb que accepten whatsapp i linkedin.
- cada pàgina existeix també en markdown, afegint
.mda l'adreça, per als assistents d'ia. - les adreces de la web antiga en wordpress redirigeixen amb un 301 a les noves.
- un script,
check:seo, recorre el sitemap d'una web en marxa i comprova títols, descripcions, canòniques, que elshreflanges corresponguin, les imatges per compartir i el json-ld.
si a la teva universitat no hi ha iaeste
dels 12 comitès que hi ha a la llista, avui només 7 són actius. vol dir que molta gent que busca iaeste estudia en una universitat on no n'hi ha cap. la web els tracta com a possibles fundadors d'un comitè nou.

el recorregut és aquest:
- al cercador de comitès, una cerca sense resultats mostra «no hi ha cap comitè a la teva ciutat?» amb el nom que has buscat. el cercador funciona també sense javascript, perquè filtra al servidor.
- el botó baixa al formulari amb «una altra universitat» ja triada, i entre els interessos hi ha «unir-me a un comitè o fundar-ne un».
- l'api mira si la universitat que has escrit és d'algun comitè actiu. si ho és, avisa aquell comitè; si no, el contacte va a l'oficina nacional.
- al cms, l'oficina nacional té un informe, «universidades sin comité», amb els contactes agrupats per universitat. serveix per decidir on obrir el comitè següent.

a més, les analítiques registren cada cerca del cercador amb el nombre de resultats, i les de zero resultats diuen quines ciutats busquen iaeste sense trobar-lo.
el pas següent és una pàgina per a cada universitat que no té comitè, perquè qui busqui "iaeste" i el nom de la seva universitat trobi una pàgina que li expliqui que encara no n'hi ha i com fundar-lo.
el que no esperava
- els filtres de correu de les universitats i les empreses obren tots els enllaços d'un missatge per comprovar-los. si confirmar la subscripció fos només obrir l'enllaç, el filtre confirmaria tothom. la confirmació és un post: la pàgina l'envia sola amb javascript, cosa que un filtre no fa, i si no hi ha javascript hi ha un botó.
- el límit de formularis és de 60 per adreça ip cada 15 minuts, perquè en una xerrada a la universitat tota una aula envia el formulari des de la mateixa xarxa.
- satori, el que dibuixa les imatges per compartir, no sap llegir les webp que serveix el cms, i totes les targetes amb foto donaven un error 500. ara la foto es retalla i es passa a jpeg amb sharp abans.
- la imatge docker del cms pesa uns 1,7 gb i la de la web uns 400 mb: el mode standalone de next.js deixa fora l'ordre
payload migrate, que és la que aplica les migracions abans d'arrencar. - la validació del formulari, amb zod, només es carrega quan algú el comença a omplir, i això va treure uns 90 kb comprimits de les pàgines que el tenen.
en xifres
- 7 comitès actius de 12 a la llista, en 3 idiomes, amb 14 tipus de secció.
- unes 41.000 línies de typescript sense comptar el codi generat ni les migracions: 17.800 a la web, 12.100 al cms, 2.900 a l'api i 7.800 als paquets compartits.
- 452 tests en 67 fitxers, que corren a cada canvi contra una base de dades postgres real.
comptat sobre el repositori el 12 d'octubre de 2026.