← Volver al blog
Giancarlos Gaona·Publicado el 17 de marzo de 2026 · Actualizado el 3 de agosto de 2026·10 min de lectura

Guía completa de Client Extensions en Liferay DXP 7.4

LiferayJava

El problema con el desarrollo tradicional en Liferay

Durante años, extender Liferay DXP significaba escribir módulos OSGi en Java, empaquetarlos como JARs, y desplegarlos dentro del contenedor de la plataforma. Este enfoque funcionaba, pero traía fricciones reales:

  • Acoplamiento fuerte: Tu código vivía dentro del classloader de Liferay. Una actualización de la plataforma podía romper tus módulos si dependían de APIs internas o no estables.
  • Ciclo de desarrollo lento: Cada cambio requería recompilar, redesplegar y esperar a que el framework OSGi resolviera dependencias. En proyectos grandes, esto podía tomar minutos.
  • Barrera de entrada alta: Un desarrollador frontend que solo necesitaba personalizar un widget tenía que entender OSGi, ServiceBuilder, y el lifecycle de Liferay para hacer algo funcional.
  • Riesgo en producción: Un módulo defectuoso podía desestabilizar toda la instancia porque compartía el mismo proceso Java.

Liferay reconoció estos problemas y, a partir de DXP 7.4, introdujo las Client Extensions como el modelo de desarrollo recomendado para personalizaciones. Conviene tener presente el detalle de versiones: la documentación oficial marca las Client Extensions de tipo microservicio como disponibles desde DXP 7.4 U45+ / Portal 7.4 GA45+ (octubre de 2022), así que si trabajas sobre una instalación antigua de 7.4 vale la pena verificar el update exacto antes de planificar.

Qué son las Client Extensions

Una Client Extension es una unidad de personalización que se ejecuta fuera del proceso de Liferay. En lugar de desplegar código dentro del servidor, defines un contrato (qué tipo de extensión es, dónde vive, cómo se comunica) y Liferay se encarga de integrarlo.

El concepto clave es la separación de runtime: tu extensión puede ser una aplicación React corriendo en su propio servidor, un microservicio que responde a webhooks, o un archivo de configuración que Liferay consume. Lo que importa es que no comparte el classloader ni el ciclo de vida del portal.

Esto tiene implicaciones profundas:

  1. Independencia tecnológica: Puedes escribir extensiones en cualquier lenguaje o framework. React, Angular, Vue, Go, Python -- lo que tenga sentido para tu caso de uso.
  2. Despliegue independiente: Actualizar una extensión no requiere reiniciar Liferay. Despliegas tu aplicación por separado y Liferay la consume.
  3. Escalabilidad horizontal: Si una extensión necesita más recursos, la escalas independientemente sin tocar el portal.
  4. Seguridad por aislamiento: Un error en tu extensión no puede tumbar la plataforma.

Tipos de Client Extensions

Liferay organiza las Client Extensions en varias categorías según su propósito:

Frontend Client Extensions

Son las más comunes. Permiten inyectar interfaces de usuario dentro de páginas de Liferay.

  • Custom Element: Registra un web component (o cualquier aplicación frontend) como un widget que los editores pueden arrastrar a páginas. Es el tipo más versátil.
  • IFrame: Embebe una aplicación externa dentro de un iframe en la página. Más simple que Custom Element pero con las limitaciones inherentes de iframes (estilo aislado, comunicación limitada).
  • CSS (globalCSS): Añade hojas de estilo que se suman al estilado existente de la página, incluidos el tema y el style book. Es la opción adecuada para ajustes de marca sin crear un tema completo.
  • Theme CSS (themeCSS): Reemplaza por completo los archivos main.css y clay.css del tema activo. Al sustituir todo el CSS del tema, tú asumes el estilado Clay de los widgets out-of-the-box de Liferay.
  • Theme Favicon: Reemplaza el favicon del sitio.
  • JavaScript (globalJS): Inyecta JavaScript global en las páginas donde habilites la extensión. Es el tipo que usarás para librerías o lógica transversal.
  • JS Import Maps Entry (jsImportMapsEntry): Registra dependencias compartidas vía import maps para que varias extensiones reutilicen la misma librería en lugar de empaquetarla cada una.
  • Theme Sprite Map (themeSpritemap): Sustituye el sprite map SVG de iconos del tema activo.

Configuration Client Extensions

Modifican el comportamiento de Liferay sin código:

  • OAuth User Agent: Configura autenticación OAuth2 para que la extensión pueda llamar a las APIs headless de Liferay de forma segura.
  • Instance Settings: Define configuraciones a nivel de instancia.

Batch Client Extensions

Ejecutan operaciones masivas sobre datos:

  • Batch: Importa o exporta datos en lote usando las APIs headless. Ideal para migraciones de contenido, creación de estructuras, o configuración inicial de un sitio.

Microservice Client Extensions

Ejecutan tu propio código fuera de Liferay en respuesta a eventos del sistema (disponibles desde DXP 7.4 U45+ / Portal 7.4 GA45+):

  • Object Action (objectAction): Se ejecuta cuando ocurre un evento en un Object Definition (crear, actualizar, eliminar). Funciona como un webhook que Liferay invoca hacia tu servicio externo.
  • Workflow Action (workflowAction): Se integra con el motor de workflows para ejecutar lógica personalizada en transiciones de estado.
  • Notification Type (notificationType): Delega el envío de notificaciones a un manejador externo cuando ocurren eventos específicos.

La categoría incluye además Object Validation, Object Entry Manager y CAPTCHA, que siguen el mismo modelo de handler externo.

Crear una Custom Element Client Extension con React

Vamos a construir una extensión de tipo Custom Element que muestra un dashboard de estadísticas. Este es el tipo más representativo y el que demuestra mejor el modelo de desarrollo.

Estructura del proyecto

mi-dashboard-extension/
  client-extension.yaml
  package.json
  src/
    index.js
    App.jsx
    components/
      StatCard.jsx
      Chart.jsx
  build/
    static/
      js/
        main.js
      css/
        main.css

Configuración en client-extension.yaml

Este archivo es el corazón de cualquier Client Extension. Define el tipo, las propiedades y los recursos que Liferay necesita conocer:

assemble:
  - from: build/static
    into: static
mi-dashboard:
  cssURLs:
    - css/main.css
  friendlyURLMapping: mi-dashboard
  htmlElementName: mi-dashboard-widget
  instanceable: true
  name: Mi Dashboard de Estadisticas
  portletCategoryName: category.client-extensions
  type: customElement
  urls:
    - js/main.js
  useESM: true

Detallemos cada campo:

  • assemble: Indica cómo empaquetar los archivos. Aquí le decimos que copie el contenido de build/static en una carpeta static dentro del paquete final.
  • htmlElementName: El nombre del custom element que Liferay buscará en el DOM. Tu aplicación React debe registrarse con este nombre.
  • instanceable: Si es true, se pueden colocar múltiples instancias del widget en la misma página.
  • portletCategoryName: La categoría donde aparecerá el widget en el panel de widgets del editor de páginas.
  • urls: Los archivos JavaScript que Liferay debe cargar.
  • cssURLs: Las hojas de estilo asociadas.
  • useESM: Habilita ES modules en lugar de scripts clásicos.

El punto de entrada de la aplicación

El archivo index.js debe registrar un custom element que Liferay pueda instanciar:

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

class MiDashboardWidget extends HTMLElement {
  connectedCallback() {
    const root = createRoot(this);
    root.render(
      <React.StrictMode>
        <App
          instanceId={this.getAttribute('id')}
          companyId={Liferay.ThemeDisplay.getCompanyId()}
          siteGroupId={Liferay.ThemeDisplay.getScopeGroupId()}
        />
      </React.StrictMode>
    );

    this._root = root;
  }

  disconnectedCallback() {
    if (this._root) {
      this._root.unmount();
    }
  }
}

if (!customElements.get('mi-dashboard-widget')) {
  customElements.define('mi-dashboard-widget', MiDashboardWidget);
}

Algunos detalles importantes:

  • El nombre en customElements.define debe coincidir con htmlElementName en el YAML.
  • Liferay.ThemeDisplay es un objeto global que Liferay expone en el cliente con información del contexto actual (compañía, sitio, usuario, idioma, etc.).
  • Se implementa disconnectedCallback para limpiar el árbol de React y evitar memory leaks cuando el widget se remueve del DOM.

Componente principal con llamadas a la API headless

import { useState, useEffect } from 'react';
import StatCard from './components/StatCard';

const API_BASE = '/o/headless-delivery/v1.0';

function App({ siteGroupId }) {
  const [stats, setStats] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      try {
        const [postsRes, docsRes] = await Promise.all([
          Liferay.Util.fetch(
            `${API_BASE}/sites/${siteGroupId}/blog-postings?pageSize=0`,
            { method: 'GET' }
          ),
          Liferay.Util.fetch(
            `${API_BASE}/sites/${siteGroupId}/documents?pageSize=0`,
            { method: 'GET' }
          ),
        ]);

        const posts = await postsRes.json();
        const docs = await docsRes.json();

        setStats({
          totalPosts: posts.totalCount || 0,
          totalDocuments: docs.totalCount || 0,
        });
      } catch (error) {
        console.error('Error fetching stats:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [siteGroupId]);

  if (loading) return <div className="dashboard-loading">Cargando...</div>;

  return (
    <div className="dashboard-grid">
      <StatCard
        label="Entradas de blog"
        value={stats.totalPosts}
        icon="blog"
      />
      <StatCard
        label="Documentos"
        value={stats.totalDocuments}
        icon="document"
      />
    </div>
  );
}

export default App;

Nota que usamos Liferay.Util.fetch en lugar de fetch nativo. Este wrapper incluye automáticamente las cabeceras de autenticación (CSRF token, session) necesarias para llamar a las APIs headless de Liferay.

Flujo de despliegue

El despliegue de una Client Extension sigue estos pasos:

  1. Build del frontend: Ejecutas npm run build para generar los archivos estáticos optimizados en build/static/.

  2. Empaquetado: Liferay Workspace incluye una tarea Gradle que lee el client-extension.yaml, toma los archivos indicados en assemble, y genera un archivo .zip listo para desplegar.

# Desde el workspace de Liferay
./gradlew :client-extensions:mi-dashboard:build
  1. Despliegue: El .zip resultante se sube a Liferay a través de la interfaz de administración (Panel de Control > Client Extensions) o se copia al directorio osgi/client-extensions/ del servidor.

  2. Activación: Liferay procesa el paquete, registra la extensión, y la hace disponible en el editor de páginas. No requiere reinicio.

Si usas Liferay Cloud o Liferay SaaS, el despliegue se integra directamente con el pipeline CI/CD de la plataforma.

Ventajas sobre el desarrollo tradicional

Después de trabajar con ambos modelos, las diferencias se sienten en el día a día:

| Aspecto | Módulos OSGi | Client Extensions | |---|---|---| | Lenguaje | Java obligatorio | Cualquiera | | Ciclo de desarrollo | Compilar + desplegar (minutos) | Hot reload local (segundos) | | Riesgo en producción | Comparte proceso con Liferay | Aislado | | Actualizaciones de Liferay | Posibles roturas de API | Contrato estable | | Curva de aprendizaje | OSGi + ServiceBuilder + Liferay APIs | Web standards + REST APIs | | Escalabilidad | Vertical (más recursos al portal) | Horizontal (escalar extensión aparte) |

El cambio más significativo es cultural: los equipos frontend pueden trabajar con sus herramientas habituales (Vite, webpack, testing con Jest) sin depender del toolchain de Java. Los equipos backend pueden exponer servicios como microservicios independientes que Liferay consume vía Object Actions o Workflow Actions.

Limitaciones y consideraciones

Las Client Extensions no son una solución universal. Hay casos donde los módulos OSGi siguen siendo necesarios:

  • Modificaciones profundas del core: Si necesitas alterar el comportamiento interno de Liferay (hooks a nivel de kernel, filtros de servlet, interceptores de servicio), los módulos OSGi siguen siendo la vía.
  • ServiceBuilder: Para modelos de datos complejos con relaciones, vistas personalizadas y finders optimizados, ServiceBuilder sigue siendo más potente que los Object Definitions.
  • Performance crítica: Si tu lógica necesita acceso directo a la base de datos o al cache de Liferay sin overhead de red, un módulo en el mismo proceso será más rápido.

La recomendación de Liferay es clara: usa Client Extensions como primera opción, y recurre a OSGi solo cuando el caso de uso lo exija.

Conclusión

Las Client Extensions representan el futuro del desarrollo en Liferay DXP. No se trata solo de un cambio técnico, sino de un cambio de paradigma que abre la plataforma a un ecosistema más amplio de desarrolladores y tecnologías. Si estás empezando un proyecto nuevo en Liferay 7.4+, las Client Extensions deberían ser tu punto de partida por defecto.

GG
Giancarlos Gaona

Ingeniero de Software y Consultor Senior especializado en Liferay DXP, con más de 8 años de experiencia liderando proyectos enterprise para banca, retail, energía e instituciones públicas.