DSpace híbrido: CSR para velocidad y SSR para que Google comprenda cada publicación

Por Staff INFODI
10 minutos
DSpace híbrido: CSR para velocidad y SSR para que Google comprenda cada publicación

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.

El problema: una página visible, pero un HTML incompleto

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.

La decisión: no todo necesita SSR

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.

Enrutamiento selectivo mediante Nginx

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.

PM2 y la disponibilidad del servicio 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.

El desafío adicional: Cloudflare Origin CA

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.

Validación del resultado

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

El resultado: rendimiento, indexación y estabilidad

La arquitectura implementada combina lo mejor de ambos modelos:

  • Navegación rápida mediante CSR.
  • HTML documental completo mediante SSR.
  • Mejor exposición de metadatos bibliográficos.
  • Compatibilidad con páginas de ítems y entidades DSpace-CRIS.
  • Menor consumo de recursos que un SSR aplicado a todo el repositorio.
  • Procesos Node supervisados y persistentes mediante PM2.
  • Comunicación interna segura, sin desactivar la validación TLS.
  • Menor dependencia de Cloudflare para solicitudes internas.

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.