Creating Your First Client Extension: A Step-by-Step Tutorial (globalJS)
What You'll Build
In this tutorial, you'll create a globalJS client extension — the JavaScript counterpart to the globalCSS extension type. By the end, you'll have a custom JavaScript file running on a Liferay page, deployed entirely outside Liferay's core, with no theme modification required.
globalJS lets you run your own JavaScript on any page in Liferay without worrying about dependencies on Liferay code or building a custom theme. It's the standard way to inject polyfills, custom scripts, third-party tracking snippets, or global variables into your instance.
Reading time: 10-12 minutes · Difficulty: Beginner · Audience: Developers, Frontend Engineers, Site Administrators
Prerequisites
- Liferay Workspace set up locally
- Java installed (JDK 8 or JDK 11, depending on your Liferay version — check the compatibility matrix)
- Blade CLI installed
- A running Liferay DXP instance (local or Docker) to test against
If you don't yet have a workspace, initialize one with Blade CLI before continuing.
Step 1: Generate the Client Extension with Blade CLI
Unlike manually scaffolding a globalCSS extension by hand, Blade CLI can generate a globalJS client extension for you interactively.
From the root of your Liferay Workspace:
cd client-extensions
blade create -t client-extension
Blade CLI will prompt you with a list of client extension types. Use the arrow keys to highlight globalJS and press Enter, then provide a name for your extension when prompted (e.g., my-global-js).
Blade CLI creates a new subfolder inside client-extensions/ with two files already in place:
client-extensions/
└── my-global-js/
├── client-extension.yaml
└── src/
└── global.js
global.js is the JavaScript file that runs automatically whenever a page configured to use this extension loads.
Step 2: Review the Generated client-extension.yaml
Open the generated file. It will look similar to this:
# client-extension.yaml
assemble:
- from: src
into: static
my-global-js:
name: My Global JS
type: globalJS
url: global.js
Field breakdown:
| Field | Purpose |
|---|---|
assemble |
Copies files from src/ into the deployable package under static/ |
name |
Human-readable label shown in the Liferay admin UI |
type |
Must be exactly globalJS |
url |
Path to the JavaScript resource file. Runs on each page load where the extension is enabled |
Step 3: Write Your JavaScript
Open src/global.js and replace the placeholder content with your own script. A simple starting example:
// src/global.js
window.addEventListener('DOMContentLoaded', function () {
console.log('My Global JS client extension is running.');
// Example: a simple welcome alert
alert('Welcome — this page is running a custom JS client extension.');
});
Keep the first version intentionally small. It's much easier to confirm one console.log or alert fired correctly than to debug a larger script on the first deploy.
Step 4: (Optional) Add Script Element Attributes
You can control how the generated <script> tag behaves using scriptElementAttributes — for example, marking it async, or adding custom data attributes:
my-global-js:
name: My Global JS
type: globalJS
url: global.js
scriptElementAttributes:
async: true
data-attribute: "value"
fetchpriority: "low"
Boolean attributes that are true render without a value in the generated HTML (e.g., async), and are omitted entirely if false. This is useful when you need to control load timing or mark a script for tools like Liferay's SPA navigation (Senna.js) to track across page transitions.
Step 5: (Optional) Set the Scope to Company-Wide
By default, a globalJS client extension is page-scoped — it only runs on pages where you've explicitly added it. If you want a script to run on every page across the entire instance (including administrative pages), set scope: company:
my-global-js:
name: My Global JS
scope: company
scriptLocation: head
type: globalJS
url: global.js
Important: The scope property can only be set through a workspace-based, YAML-defined client extension. If you create a globalJS extension manually through the Liferay admin UI instead of deploying it via Workspace, it's automatically page-scoped and cannot be changed to company scope.
Step 6: Build and Deploy
Deploying to a Local Instance
From your client extension's root folder:
blade gw deploy
Deploying to Liferay in a Docker Container
blade gw deploy -Ddeploy.docker.container.id=$(docker ps -lq)
This builds and deploys the extension into your Docker container's deploy/ folder.
Deploying to Liferay Cloud
If you're deploying to a Liferay Experience Cloud environment, use the Liferay Cloud Command-Line Tool instead:
lcp deploy
Confirm the deployment succeeded by checking your Liferay instance's server console output.
Step 7: Configure a Page to Use the Extension
Deploying the extension registers it with Liferay, but it won't run anywhere until you explicitly add it to a page.
- Log in to your running Liferay instance.
- Navigate to the page where you want the script to run, and click the Edit icon at the top.
- In the sidebar, open the Page Design Options menu, then click the Configuration icon.
- Click the Advanced tab.
- Scroll to the JavaScript section near the bottom.
- Click Add JavaScript Client Extensions, and select your deployed extension. You can choose to add it to the page head or page bottom.
- Click Save.
- Publish the page — the JavaScript only runs outside of Edit mode once the page is published.
Step 8: Verify It's Working
- Navigate to the published page in your browser.
- If you used the sample
alert()script from Step 3, you should see the alert pop up immediately. - Open DevTools → Console. You should see your
console.logoutput. - Open DevTools → Elements, and inspect the
<head>or bottom of<body>(depending on where you placed the script) to confirm the<script>tag is present, pointing to a URL resembling:
/o/client-extension/my-global-js/static/global.js
If nothing appears, work through this checklist:
- Confirm the extension deployed without server errors
- Confirm you added the extension to the page in Step 7 (deploying alone isn't enough)
- Confirm you published the page — unpublished changes won't run outside Edit mode
- Hard-refresh the browser (Ctrl/Cmd + Shift + R) to bypass cached scripts
- If you edited and redeployed the extension, you may need to remove it from the page and re-add it to see changes
globalJS vs. JS Import Maps: When to Use Each
Both extension types deliver JavaScript globally, but they solve different problems:
| Aspect | globalJS |
jsImportMapsEntry |
|---|---|---|
| Purpose | Run a script directly on page load | Register a module for import statements elsewhere |
| Scoping | Page-scoped by default, or company scope |
Not scoped — available system-wide once registered |
| Best for | Polyfills, tracking snippets, one-off page behavior, global variables | Shared libraries (React, jQuery, animation libraries) reused across multiple custom elements |
| Loading model | Injected via <script> tag before/as the page renders |
Loaded only when a page element explicitly imports it |
| Typical use case | A third-party analytics snippet needed everywhere | A shared React runtime consumed by several custom elements |
If your goal is simply "run this script on this page (or every page)," globalJS is the right tool. If your goal is "make this library available so multiple custom elements can import it without each bundling their own copy," a JS import map entry is more efficient — it only loads the resource when something on the page actually references it.
Common Beginner Mistakes
1. Wrong type Value
# Incorrect
type: js
# Correct
type: globalJS
2. Deploying but Forgetting to Add It to a Page
Deployment only registers the extension with Liferay — it does not automatically attach it to any page. You must explicitly add it through the page's Advanced → JavaScript configuration, or (for company-scoped extensions) rely on the scope setting.
3. Forgetting to Publish the Page
JavaScript added via a client extension only executes outside of Edit mode once the page is published. Testing in Edit mode without publishing can make a correctly configured extension appear broken.
4. Assuming Manual UI-Created Extensions Support Company Scope
If you create a globalJS extension directly through the Liferay admin UI (rather than deploying one from a workspace), it is always page-scoped. Company-wide scope requires a YAML-based, workspace-deployed extension.
5. Browser Caching Masking Real Issues
Before assuming a deploy failed, hard-refresh or test in an incognito window. Cached JavaScript is one of the most common false alarms when verifying script changes.
What Gets Created Behind the Scenes
| Component | Result |
|---|---|
| Deployable package | Archive containing your assembled JavaScript |
| Registration | Extension appears in the Client Extensions list in the admin UI |
| Delivery | Script served via a Liferay-managed static URL |
| Application | Script runs on any page (or instance-wide, if scope: company) where it's been added |
| Upgrade safety | Extension is untouched by Liferay version upgrades |
Next Steps
Once globalJS feels familiar, the same YAML-driven pattern extends naturally to:
jsImportMapsEntry— Register a shared JavaScript module (like a common React runtime) for use across multiple custom elements, without loading it repeatedly; see the dedicated tutorial for the full walkthrough- Custom Elements — Build full interactive React or Vue components using
type: customElement, deployed with the same Blade CLI workflow themeCSS/globalCSS— Pair your JavaScript behavior with matching visual styling; see the globalCSS tutorial for that companion extension type
Each of these follows the same underlying workflow covered here: scaffold with Blade CLI, define the extension in client-extension.yaml, build, and deploy.
Summary
In this tutorial, you:
- Generated a
globalJSclient extension scaffold using Blade CLI - Reviewed the auto-generated
client-extension.yamlstructure - Wrote a custom JavaScript file in
src/global.js - Learned how to configure script element attributes and company-wide scope
- Deployed the extension locally, via Docker, and via Liferay Cloud
- Added the extension to a page through the Advanced JavaScript configuration
- Verified the script executed and troubleshot common issues
This is the same foundational client extension pattern used across all frontend types in Liferay — only the type value, generated files, and the code inside src/ change between globalCSS, globalJS, jsImportMapsEntry, and custom elements.
References
This tutorial draws on the following official Liferay Learn documentation and courses:
- Using a JavaScript Client Extension — Customizing Liferay's Look and Feel, Liferay Learn
- JavaScript YAML Configuration Reference — Liferay Learn
- Use JavaScript with Client Extension on Pages — Liferay Learn knowledge base
- Mastering Liferay Frontend Client Extensions: Implementing Custom Functionality — Course module, Liferay Learn
- Building Frontend Applications with JavaScript in Liferay: Global Libraries and Dependencies — Course module, Liferay Learn
- Client Extensions — Overview, Liferay Learn
Related Reading
- Sharing JavaScript Libraries Across Pages: jsImportMapsEntry — how to share one copy of React, jQuery, or any library across multiple custom elements instead of bundling it repeatedly.
- Creating Your First Client Extension: A Step-by-Step Tutorial (globalCSS) — the CSS counterpart to this tutorial, and the simplest place to start with client extensions.
- Creating Your First Liferay Object: A Step-by-Step Tutorial — a no-code, step-by-step walkthrough for Liferay Objects.
- Liferay Hosting Explained: SaaS vs. PaaS vs. Self-Hosted — SaaS plans limit customization to Client Extensions only; worth knowing before you plan a deployment.
- Browse all Liferay tools — generators and reference utilities for Liferay developers.
