Introduction to Client Extensions
Liferay 7.4 introduced Client Extensions (CX) as a paradigm shift in how developers extend and customize the Liferay DXP platform. Unlike traditional OSGi modules that require deployment inside the Liferay runtime, Client Extensions run outside the portal — making them language-agnostic, independently deployable, and significantly easier to maintain.
This guide walks you through building a fully functional React-based Client Extension (Custom Element) that integrates with Liferay's Headless Delivery API, and then covers every major Client Extension type available in Liferay 7.4.
Why Client Extensions? Client Extensions decouple frontend and backend concerns. You can build in React, Vue, Angular, or plain JavaScript — without touching the portal's OSGi container. Deployments become simpler, CI/CD pipelines become faster, and teams can work independently. |
Client Extension Types at a Glance
Extension Type |
Description |
Use Case |
Custom Element |
Renders a Web Component / React app on a page |
Dashboards, Widgets |
Remote App |
Embeds external iFrame apps inside Liferay |
Legacy SPA integration |
Theme CSS |
Overrides Liferay's default CSS theming |
Brand styling |
Frontend Token |
Defines CSS design tokens for Style Books |
Design system |
Global CSS |
Injects CSS globally across all pages |
Global overrides |
Global JS |
Injects JavaScript globally across all pages |
Analytics, tracking |
Editor Config |
Configures the CKEditor instance in Liferay |
Rich text customization |
Workflow Action |
Triggers external HTTP actions from workflow |
Integrations |
Object Action |
Fires HTTP calls on Liferay Object events |
Microservice triggers |
OAuth Headless Server |
Registers OAuth2 app for server-to-server |
Backend integrations |
Part 1: Building a React Custom Element
In this section, we create a production-ready Invoice Widget using React 18 and register it as a Web Component that Liferay can render on any page.
Step 1 |
Create the Project Structure |
Start by initializing a Liferay Workspace with Blade CLI targeting version 7.4:
blade init -v 7.4 my-workspace cd my-workspace |
Navigate into the client-extensions folder and scaffold a new React application:
cd client-extensions npx create-react-app invoice-react-widget cd invoice-react-widget |
Step 2 |
Modify src/index.js — The Web Component Wrapper |
Liferay renders Client Extensions as HTML Custom Elements. Replace the entire contents of src/index.js with the following Web Component wrapper that bootstraps your React app:
import
React from 'react'; import
{ createRoot } from 'react-dom/client'; import
App from './App'; class
InvoiceWidget extends HTMLElement { connectedCallback()
{ //
Collect Liferay Context (Auth Token, Site ID, etc.) const
props = { authToken:
window.Liferay?.authToken || '', siteId:
window.Liferay?.ThemeDisplay?.getGroupId() || '', elementId:
this.getAttribute('id') }; this._root
= createRoot(this); this._root.render(<App
{...props} />); } disconnectedCallback()
{ this._root.unmount(); } } const
ELEMENT_NAME = 'invoice-react-widget'; if
(!customElements.get(ELEMENT_NAME)) { customElements.define(ELEMENT_NAME,
InvoiceWidget); }
|
Step 3 |
Create App.js — The Headless API Consumer |
This component fetches data from Liferay's Headless Delivery API using the auth token provided by the Web Component wrapper:
import React, { useEffect, useState } from 'react';
function App({ authToken }) { const [invoices, setInvoices] = useState([]); useEffect(() => { // Fetching from Liferay Headless Delivery API fetch('/o/headless-delivery/v1.0/sites/20121/blog-postings', { headers: { 'x-csrf-token': authToken } }) .then(res => res.json()) .then(data => setInvoices(data.items || [])) .catch(err => console.error(err)); }, [authToken]); return ( <div style={{ padding: '20px', border: '1px solid #ccc' }}> <h2>Invoice Widget</h2> {invoices.length === 0 ? <p>No data found.</p> : ( <ul> {invoices.map(item => ( <li key={item.id}>{item.headline || item.id}</li> ))} </ul> )} </div> ); }
export default App; |
Note:
Replace
/sites/20121/ with your actual Liferay Site ID, which can be found in
Site Settings → Site Configuration.
Step 4 |
Create client-extension.yaml |
This YAML file is the heart of your Client Extension. Place it in the root of your invoice-react-widget folder. It tells Liferay how to register and serve your extension:
assemble: - from: build/static into: static invoice-react-widget: name: Invoice React Widget type: customElement htmlElementName: invoice-react-widget portletCategoryName: category.client-extensions instanceable: true useESM: true urls: - js/main.*.js cssURLs: - css/main.*.css |
Key YAML Properties Explained • type: customElement — Tells Liferay this is a Web Component • htmlElementName — Must match the tag name defined in index.js • instanceable: true — Allows multiple instances on one page • useESM: true — Uses ES Modules for better tree-shaking • urls — Glob pattern to match the React build output JS file |
Step 5 |
Build and Deploy |
5a. Build the React Application
Inside the invoice-react-widget directory, run the React production build:
npm run build |
5b. Build the Client Extension Package
From your Liferay Workspace root, trigger the Gradle build for your extension:
gradlew :modules:client-extensions:invoice-react-widget:build |
5c. Force Rebuild (if changes don't reflect)
If your latest changes are not appearing, clean the build cache and rebuild with refreshed dependencies:
# Full clean rebuild with dependency refresh gradlew clean :client-extensions:invoice-react-api-widget:build --refresh-dependencies
# Skip test cases to speed up the build gradlew :client-extensions:invoice-react-api-widget:build -x test |
5d. Deploy to Liferay
Copy the generated .zip file to your Liferay bundle's osgiclient-extensions/ directory:
# Option 1: Manual copy # Copy from: client-extensions/invoice-react-widget/dist/*.zip # Copy to: {liferay-home}/osgi/client-extensions/
# Option 2: Use Gradle deploy task gradlew :client-extensions:react-invoice-widget:deploy |
Step 6 |
Add the Widget to a Page |
Follow these steps to add your newly deployed Client Extension widget to a Liferay page:
Log in to your Liferay portal as an Administrator.
Go to Applications (Global Menu) → Custom Apps → Client Extensions.
Verify that "Invoice React Widget" shows status as Published.
Navigate to a Site Page and click Edit (the pencil icon in the top bar).
Open the Widgets panel in the right sidebar.
Search for "Invoice React Widget" under the Client Extensions category.
Drag and drop the widget onto your page layout.
Click Publish to make the changes live.
Note: If the widget does not appear in the search, ensure the .zip was correctly placed in osgi/client-extensions/ and Liferay's auto-deploy has processed it (watch the console log for deployment confirmation).
Part 2: All Client Extension Types in Liferay 7.4
Beyond Custom Elements, Liferay 7.4 provides a full suite of Client Extension types. Below is a comprehensive guide to each type with project structure, YAML configuration, and deployment steps.
1. Theme CSS Client Extension
Override Liferay's default theme styling without building a full theme. This is the recommended approach for brand customization in Liferay 7.4.
Project Structure
client-extensions/ └── my-theme-css/ ├── client-extension.yaml └── src/ ├── clay.css ← Override Clay/Bootstrap variables └── main.css ← Custom styles |
client-extension.yaml
assemble: - from: build into: static
my-theme-css: name: My Brand Theme CSS type: themeCSS clayURL: css/clay.css mainURL: css/main.css |
Build & Deploy
gradlew :modules:client-extensions:my-theme-css:build gradlew :modules:client-extensions:my-theme-css:deploy |
Note: After deployment, go to Site Builder → Pages → Design → Theme and select your custom Theme CSS from the dropdown.
2. Frontend Token Definition Client Extension
Define CSS custom properties (design tokens) that power Liferay Style Books. This enables non-developers to adjust brand colors, typography, and spacing through the Style Book editor UI.
client-extension.yaml
assemble: - from: build into: static my-frontend-tokens: name: My Design Tokens type: frontendTokenDefinition frontendTokenDefinitionURL: token-definition.json |
token-definition.json (sample)
{ "frontendTokenCategories": [ { "frontendTokenSets": [ { "frontendTokens": [ { "defaultValue": "#1B4F8E", "editorType": "ColorPicker", "mappings": [{"type": "cssVariable", "value": "--primary-color"}], "label": "Primary Color", "name": "primaryColor", "type": "String" } ], "label": "Brand Colors", "name": "brandColors" } ], "label": "Brand", "name": "brand" } ] } |
3. Global CSS Client Extension
Inject CSS across every page of your Liferay site. Useful for third-party widget styles, global resets, or utility classes.
client-extension.yaml
assemble: - from: build into: static my-global-css: name: Global CSS type: globalCSS url: css/global.css |
Note: Global CSS is applied to ALL pages sitewide. For page-specific styles, use a Custom Element that injects a <style> tag.
4. Global JavaScript Client Extension
Inject JavaScript globally across all pages. Common uses include analytics integrations (Google Tag Manager, Hotjar), chat widgets, and global event listeners.
client-extension.yaml
assemble: - from: build into: static my-global-js: name: Global Analytics JS type: globalJS url: js/analytics.js scriptLocation: bottom # top | bottom |
Sample analytics.js
(function() { // Google Tag Manager initialization window.dataLayer = window.dataLayer || []; function gtag(){ dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'GTM-XXXXXXX');
// Liferay page view tracking Liferay.on('endNavigate', function(event) { gtag('event', 'page_view', { page_path: event.path }); }); })(); |
5. Object Action Client Extension
Triggers an HTTP call to an external endpoint when a Liferay Object record is created, updated, or deleted. This is the primary integration pattern for connecting Liferay Objects to microservices.
client-extension.yaml
my-object-action: name: Invoice Created Action type: objectAction url: https://your-microservice.com/api/invoice-created |
Configuration in Liferay UI
Go to Control Panel → Objects → select your Object (e.g., Invoice).
Click on the Actions tab.
Add Action → select Trigger: On After Add.
Set Action type to "Client Extension" and choose your registered action.
Save. The webhook fires automatically on every new record.
Note: The external endpoint receives a JSON payload with the full Object record. Your microservice can then sync data to SAP, Salesforce, or any external system.
6. Workflow Action Client Extension
Fires an HTTP request to an external service when a Kaleo Workflow reaches a specific action node. Use this to trigger document approvals, send notifications, or call external APIs from workflow.
client-extension.yaml
my-workflow-action: name: Approval Notification Action type: workflowAction url: https://your-notification-service.com/notify |
Usage in Kaleo Workflow Designer
In the Workflow Designer (Process Builder), add an Action node and select Client Extension as the action type. Choose your registered workflow action. Liferay will POST workflow context data (asset ID, current user, status) to your endpoint.
7. OAuth Headless Server Client Extension
Registers a server-to-server OAuth2 application in Liferay, enabling backend microservices to call Liferay's Headless APIs without user interaction using the Client Credentials grant flow.
client-extension.yaml
my-oauth-headless-server: name: Invoice Service OAuth type: oAuthApplicationHeadlessServer scopes: - Liferay.Headless.Delivery.everything |
Using the Token in Your Microservice
// Spring Boot example — Client Credentials Token Exchange @Bean public WebClient liferayWebClient() { return WebClient.builder() .baseUrl("http://liferay:8080") .filter(new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager, repo)) .build(); }
// Then call Liferay Headless API webClient.get() .uri("/o/headless-delivery/v1.0/sites/{id}/blog-postings", siteId) .retrieve() .bodyToMono(BlogPostingPage.class); |
8. Editor Config Client Extension
Customize the CKEditor configuration used in Liferay's rich text fields — Web Content, Blog entries, and Object fields. Add custom plugins, toolbar buttons, or content filters.
client-extension.yaml
my-editor-config: name: Custom CKEditor Config type: editorConfigContributor url: js/editor-config.js editorConfigurationKeys: - CKEDITOR |
editor-config.js
window.MyEditorConfig = { apply: function(config) { // Add custom toolbar group config.toolbarGroups = [ { name: 'document', groups: ['mode', 'document', 'doctools'] }, { name: 'basicstyles', groups: ['basicstyles', 'cleanup'] }, { name: 'paragraph' } ];
// Set max content height config.height = '400px';
// Restrict allowed content for security config.allowedContent = 'p h1 h2 h3 b i u a[href]; ul ol li;'; } }; |
Best Practices & Production Tips
Project Organization
Keep all Client Extensions in the client-extensions/ folder of your Liferay Workspace for unified Gradle builds.
Use a monorepo approach — one workspace, multiple extension projects, one CI/CD pipeline.
Version your YAML files alongside your source code in Git.
Build & CI/CD
Always run npm run build before the Gradle build — Gradle packages whatever is in the build/ directory.
Use gradlew -x test in CI pipelines to skip slow unit tests during development iterations.
Use --refresh-dependencies only when you suspect a cached dependency is stale, not on every build
Liferay Context & Security
Always pass authToken from window.Liferay.authToken and include it in x-csrf-token headers for Headless API calls.
Never hardcode site IDs — read them dynamically from window.Liferay.ThemeDisplay.getGroupId().
Use HTTPS endpoints in all Object Action and Workflow Action URLs in production.
Troubleshooting
Widget not visible in sidebar search Ensure the .zip was deployed to osgi/client-extensions/ and Liferay's log shows a successful deployment message. Try restarting the portal if hot-deploy is not enabled. |
CSS not applying Verify the cssURLs glob pattern in client-extension.yaml matches your actual build output filename. Check browser DevTools to confirm the file is loaded. |
API returning 401 Unauthorized Confirm the authToken is passed correctly in the x-csrf-token header. Token validity is session-scoped — refresh the page to get a new token if the session has expired. |
Changes not reflecting after rebuild Run: gradlew clean :client-extensions:<your-extension>:build --refresh-dependencies to fully invalidate the Gradle cache and force a clean build. |
Comments
Post a Comment