Web pública de CREUP construida con Nuxt 4, Nitro, PostgreSQL, Redis, BullMQ y Drizzle ORM. Incluye un panel de administración para gestionar el contenido del sitio e integraciones con Google Calendar, una API externa y correo.
- Web pública SSR con SEO, accesibilidad e i18n.
- Panel de administración para accesos, carrusel, "Qué es CREUP", igualdad, newsletter, prensa, dossier de prensa, enlaces, etiquetas, medios e informes económicos.
- Integración con Google Calendar para la agenda pública y agendas individuales.
- Integración con una API externa para miembros, organigrama, comités, eventos y documentos.
- Caché SSR/API, caché SWR de integraciones externas y limitación de peticiones compartidas en Redis.
- Cola BullMQ para envío de newsletters y tareas periódicas de mantenimiento.
- Envío de correos mediante SMTP y Mailpit para revisarlos en local.
- Nuxt 4 + Nitro
- Nuxt UI v4 + Tailwind CSS
@nuxtjs/i18nnuxt-security(CSP, HSTS y cabeceras HTTP de seguridad)- Redis + BullMQ
- PostgreSQL + Drizzle ORM
better-authcon Google OAuth para el panel de administración
- Node.js compatible con Nuxt 4
pnpm- Docker y Docker Compose para el entorno local (PostgreSQL, Redis, Adminer y Mailpit)
- En producción, un proxy inverso delante de Nitro. Configura
NUXT_TRUSTED_PROXY_CIDRScon los CIDRs del proxy; solo las conexiones desde esos rangos tendránX-Forwarded-Foren cuenta. Por defecto, solo loopback (127.0.0.1/32,::1/128).
- Instala dependencias:
pnpm install-
Crea tu
.envbasándote en.env.exampley configura las variables necesarias. -
Levanta los servicios auxiliares:
docker compose up -d postgres redis adminer mailpitEl init de PostgreSQL ejecuta docker/postgres/init/001-extensions.sql, que instala pg_trgm automáticamente en el primer arranque.
- Arranca la aplicación:
pnpm dev- Si cambias el esquema de base de datos:
pnpm db:generate
pnpm db:migrate
pnpm db:seedpnpm db:migrate carga .env, muestra progreso, adquiere el advisory lock de PostgreSQL y garantiza las extensiones necesarias antes de aplicar migraciones.
pnpm db:seed es destructivo: vacía y recarga las tablas de contenido dentro de una única transacción (si algo falla, revierte sin dejar la base a medias). El script ya incluye --confirm y solo se ejecuta si el host de DATABASE_URL es local (localhost, 127.0.0.1, ::1 o el servicio Docker postgres); para un wipe intencionado contra otro host define ALLOW_SEED_WIPE=true. En producción exige además ALLOW_PRODUCTION_SEED=true. No borra las tablas de suscriptores de la newsletter (newsletter_subscribers / newsletter_subscription_events): conservan la evidencia de consentimiento RGPD.
En desarrollo local, NUXT_SITE_URL puede quedarse en http://localhost:3000. Si la imagen de producción debe compilarse incrustando otro origen público (por ejemplo al ejecutar deploy.sh desde una máquina cuyo .env sigue apuntando a localhost), define NUXT_DEPLOY_SITE_URL: deploy.sh y deploy-local.sh usarán esa variable solo como --build-arg del build; el runtime del contenedor sigue configurándose con NUXT_SITE_URL en el entorno donde arranca Compose (p. ej. el .env del VPS).
Casi toda la configuración es runtime; la imagen no lleva secretos ni URLs baked por defecto y las variables se leen al arrancar el contenedor.
Excepción importante: NUXT_SITE_URL, NUXT_UMAMI_HOST y NUXT_UMAMI_ID también deben estar disponibles durante pnpm build o docker build, porque Nuxt las usa al compilar la configuración del sitio y del módulo de analítica.
El bloque completo de variables con descripciones y valores de ejemplo está en DEPLOYMENT.md.
En resumen:
- Obligatorias al arrancar:
NUXT_SITE_URL,DATABASE_URL,APP_SECRET,NUXT_ADMIN_EMAILS,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,NUXT_REDIS_URL,NUXT_EXTERNAL_API_BASE_URL,NUXT_GOOGLE_CALENDAR_API_KEY,NUXT_GOOGLE_CALENDAR_ID,NUXT_SMTP_HOST/PORT/SECURE/USER/PASS - Necesarias también en build:
NUXT_SITE_URLy, si activas Umami,NUXT_UMAMI_HOST+NUXT_UMAMI_ID - Opcionales según funcionalidad:
NUXT_EXTERNAL_ASSET_BASE_URL,NUXT_SMTP_FROM_EMAIL,NUXT_SMTP_TO_EMAIL,NUXT_SMTP_PRESS_EMAIL,NUXT_EXTERNAL_ASSET_PROXY_*,NUXT_EXTERNAL_API_CACHE_*,NUXT_TURNSTILE_*,NUXT_UMAMI_*,NUXT_TRUSTED_PROXY_CIDRS - Solo en local (compose):
POSTGRES_USER/PASSWORD/DB/PORT,REDIS_PORT,ADMINER_PORT,MAILPIT_SMTP_PORT,MAILPIT_WEB_PORT
DATABASE_URL va sin prefijo NUXT_ porque se lee con process.env directamente. Cambiar de dominio no requiere reconstruir la imagen; basta con actualizar NUXT_SITE_URL y recrear el contenedor.
- Redis es obligatorio para caché de handlers, SWR de APIs externas, rate limiting, almacenamiento de Better Auth y colas BullMQ.
- Al menos una instancia de Nitro debe permanecer activa para procesar la cola de newsletters.
- Los archivos subidos desde administración viven en
.data/admin-assets/y subdirectorios depublic/. No están versionados y deben incluirse en el plan de copias de seguridad. - Configura un límite de cuerpo en el proxy frontal alineado con el mayor upload permitido. La app usa un techo duro de 22 MB por petición, exige
Content-Lengthy también cuenta los bytes realmente recibidos antes de procesar el multipart. - La ruta
/healthrechaza peticiones conX-Forwarded-For(devuelve 404), así que los health checks solo funcionan directamente, sin pasar por proxy. - La CSP envía informes a
/api/csp-report. El endpoint valida el cuerpo, aplica rate limiting y escribe un resumen en logs para poder endurecer directivas comostyle-srccon datos reales.
| Script | Descripción |
|---|---|
pnpm dev |
Servidor de desarrollo |
pnpm build |
Construir para producción |
pnpm preview |
Previsualizar build |
pnpm lint / pnpm lint:fix |
Lint |
pnpm i18n:check |
Verifica paridad de claves de cada idioma vs es.json |
pnpm i18n:audit-identical |
Detecta valores idénticos a es.json (por defecto en) |
pnpm db:generate |
Genera migración tras cambios en el esquema |
pnpm db:migrate |
Aplica migraciones pendientes |
pnpm db:studio |
Abre Drizzle Studio |
pnpm db:seed |
Carga datos de prueba (destructivo) |
pnpm db:seed:content |
Backfill idempotente de traducciones de contenido |
| Servicio | URL |
|---|---|
| Aplicación | http://localhost:3000 |
| Health check | http://localhost:3000/health |
| Adminer | http://localhost:8088 (por defecto) |
| Mailpit web | http://localhost:8025 (por defecto) |
| Mailpit SMTP | localhost:1025 (por defecto) |
app/componentes, layouts, páginas y composablesserver/api/endpoints públicos y de administraciónserver/middleware/middleware del servidorserver/utils/utilidades compartidasserver/db/cliente y esquema de Drizzlei18n/locales/mensajes de traduccióndrizzle/migraciones y seeds
Idiomas: es (por defecto), en, ca, eu, gl, val (valenciano), con estrategia
prefix_except_default (el español no lleva prefijo; el resto van bajo /en/, /ca/, /eu/,
/gl/, /val/). El valenciano usa code: 'val' con hreflang ca-ES-valencia.
Política de URLs: los slugs públicos se mantienen en español en todos los idiomas.
Solo cambia el prefijo de idioma; la ruta (/conocenos/eventos/..., /prensa/...) es la
misma en todas las versiones. Es una decisión intencionada: simplifica enlaces, canónicas
y el alta de un idioma nuevo.
Si en el futuro se quisieran slugs traducidos, se haría con las rutas personalizadas de
@nuxtjs/i18n (custom route paths) — pero eso multiplica el radio de impacto de añadir un
idioma (hay que traducir y mantener cada ruta). No conviene caer en ello sin decidirlo
explícitamente.
El build siempre ocurre en local o CI; el VPS solo hace pull de la imagen y la ejecuta.
bash ./deploy.shEl script construye la imagen, la publica en GHCR, conecta al VPS por SSH, aplica migraciones y recrea los contenedores.
Para el flujo completo (VPS, NGINX, TLS, migraciones, backups), consulta DEPLOYMENT.md.
Para validar cómo queda la web desplegada sin necesitar un VPS, usa el stack local con NGINX:
bash ./deploy-local.shComandos útiles:
bash ./deploy-local.sh status
bash ./deploy-local.sh logs [app|nginx|postgres|redis|mailpit|all]
bash ./deploy-local.sh doctor [/ruta/publica.webp]
bash ./deploy-local.sh down
pnpm cache:purgeEl stack exporta NODE_OPTIONS=--enable-source-maps --trace-uncaught por defecto para que los errores del contenedor app sean más útiles en modo producción local. Puedes sobreescribirlo con LOCAL_DEPLOY_NODE_OPTIONS=... en .env.local-deploy.