Los repositorios institucionales no solo deben almacenar y presentar documentos. También necesitan ser correctamente interpretados por buscadores, recolectores académicos y servicios externos.
En una implementación reciente de DSpace detectamos un desafío interesante: la interfaz operaba mediante Client-Side Rendering (CSR), lo que entregaba una navegación rápida y reducía la carga del servidor. Sin embargo, las páginas individuales de las publicaciones necesitaban Server-Side Rendering (SSR) para exponer desde la respuesta HTML inicial elementos esenciales como el título, los autores, la descripción y las etiquetas bibliográficas.
La solución no fue abandonar CSR ni transformar todo el repositorio en SSR. Diseñamos una arquitectura híbrida: CSR para la navegación general y SSR exclusivamente para las páginas documentales.
En una aplicación Angular con CSR, el servidor entrega inicialmente una estructura HTML básica. Luego, el navegador ejecuta JavaScript, consulta la API REST de DSpace y construye la página.
Para una persona, el resultado puede verse perfectamente. Sin embargo, un robot que analiza la respuesta inicial podría no encontrar inmediatamente información como:
<title>Título de la publicación</title>
<meta name="citation_title" content="Título de la publicación">
<meta name="citation_author" content="Nombre del autor">
<meta name="description" content="Resumen del documento">
Google tiene capacidad para ejecutar JavaScript, pero el proceso es más costoso, puede ejecutarse en una etapa posterior y no siempre ofrece la misma consistencia que recibir directamente el contenido renderizado.
SSR permite que DSpace Angular prepare la página en el servidor antes de entregarla. De este modo, los metadatos importantes están presentes desde la primera respuesta.
Procesar mediante SSR cada búsqueda, listado, comunidad y navegación interna puede consumir recursos innecesariamente.
Por eso se definieron dos comportamientos:
| Tipo de página | Renderizado |
|---|---|
| Inicio, búsqueda y navegación | CSR |
| Comunidades y colecciones | CSR |
Página individual /items/<UUID> |
SSR |
Entidad CRIS /entities/<tipo>/<UUID> |
SSR |
API REST /server/api |
Backend DSpace |
Esta separación mantiene la agilidad del frontend y reserva el procesamiento SSR para las páginas cuyo contenido necesita ser indexado con precisión.
La pieza central fue una regla de Nginx que reconoce únicamente URLs documentales con un UUID válido:
location ~* "^/(items/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|entities/[^/]+/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/?$" {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 15s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
}
El resto de las rutas continúa utilizando el frontend estático:
location / {
root /ruta/dspace-angular/dist/browser;
try_files $uri $uri/ /index.html;
}
La regla también contempla las entidades de DSpace-CRIS:
/entities/publication/<UUID>
/entities/person/<UUID>
/entities/project/<UUID>
/entities/orgunit/<UUID>
Así, publicaciones, personas, proyectos y unidades organizacionales pueden entregar HTML renderizado sin obligar a que todo el repositorio opere mediante SSR.
El servidor Angular SSR fue administrado mediante PM2 en modo clúster:
{
"apps": [
{
"name": "dspace-ui",
"cwd": "/ruta/dspace-angular",
"script": "/ruta/dspace-angular/dist/server/main.js",
"instances": 2,
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production"
},
"max_memory_restart": "2048M",
"node_args": "--max-old-space-size=2048 --use-openssl-ca"
}
]
}
Esto permite ejecutar varias instancias, reiniciar el proceso ante fallos y restaurarlo automáticamente después de un reinicio del servidor.
Un aspecto fundamental fue comprobar que:
dist/browser/index.html
dist/server/main.js
pertenecieran a la misma compilación. Ejecutar un servidor SSR antiguo junto a archivos de navegador más recientes puede provocar errores difíciles de interpretar, aunque el sitio parezca funcionar correctamente mediante CSR.
Durante la implementación surgió un escenario especialmente interesante.
El dominio público estaba protegido por Cloudflare, mientras el servidor resolvía localmente el mismo dominio para evitar que las llamadas internas atravesaran la red de Cloudflare. Como resultado, Node recibía directamente un certificado Cloudflare Origin CA.
Ese certificado es válido para la comunicación entre Cloudflare y el servidor de origen, pero no forma parte de las autoridades públicas confiables utilizadas normalmente por Node y curl.
El síntoma era claro:
unable to verify the first certificate
UNABLE_TO_VERIFY_LEAF_SIGNATURE
Al intentar consultar la API REST, SSR no recibía la estructura HAL esperada y terminaba mostrando errores derivados:
No _links section found
Http failure response: 0 Unknown Error
Enviar las consultas internas nuevamente por Cloudflare tampoco era apropiado: las protecciones del WAF podían responder con códigos 403 o aplicar limitaciones a la IP del propio servidor.
La solución consistió en mantener el tráfico interno y configurar correctamente la confianza en la autoridad certificadora del origen. Node fue iniciado utilizando las autoridades OpenSSL del sistema:
--use-openssl-ca
Con esto, SSR pudo comunicarse de forma segura con la API REST local sin desactivar la validación TLS.
Es importante destacar que no se utilizó:
NODE_TLS_REJECT_UNAUTHORIZED=0
Aunque esa variable puede ocultar temporalmente un error de certificados, desactiva globalmente la validación TLS de Node y no constituye una solución segura para producción.
Una respuesta HTTP 200 no basta para confirmar que SSR está funcionando. La prueba real consiste en inspeccionar el HTML sin ejecutar JavaScript:
curl -s https://repositorio.example.org/entities/publication/<UUID> \
| grep -Ei '<title>|citation_title|citation_author|description|og:title'
Cuando la respuesta contiene el título, los autores y la descripción reales de la publicación, se confirma que los metadatos fueron generados en el servidor.
También es necesario verificar:
curl -I http://127.0.0.1:4000/
pm2 status
ss -ltnp | grep ':4000'
Finalmente, la configuración operativa puede persistirse con:
pm2 save
pm2 startup
La arquitectura implementada combina lo mejor de ambos modelos:
La optimización de un repositorio no consiste únicamente en mantener sus servicios levantados. También requiere comprender cómo interactúan Angular, DSpace REST, Nginx, Node, PM2, TLS, Cloudflare y los motores de búsqueda.
En INFODI abordamos estas integraciones desde una perspectiva completa: infraestructura, aplicación, seguridad, interoperabilidad y visibilidad académica. Porque una publicación correctamente almacenada es importante, pero una publicación correctamente descubierta, interpretada e indexada multiplica su alcance.