Skip to content
ToolShedby Wasim Shaikh

Creating Your First Client Extension: A Step-by-Step Tutorial (globalJS)

Wasim · 8 Jul 2026

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.

  1. Log in to your running Liferay instance.
  2. Navigate to the page where you want the script to run, and click the Edit icon at the top.
  3. In the sidebar, open the Page Design Options menu, then click the Configuration icon.
  4. Click the Advanced tab.
  5. Scroll to the JavaScript section near the bottom.
  6. Click Add JavaScript Client Extensions, and select your deployed extension. You can choose to add it to the page head or page bottom.
  7. Click Save.
  8. Publish the page — the JavaScript only runs outside of Edit mode once the page is published.

Step 8: Verify It's Working

  1. Navigate to the published page in your browser.
  2. If you used the sample alert() script from Step 3, you should see the alert pop up immediately.
  3. Open DevTools → Console. You should see your console.log output.
  4. 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:

  1. Generated a globalJS client extension scaffold using Blade CLI
  2. Reviewed the auto-generated client-extension.yaml structure
  3. Wrote a custom JavaScript file in src/global.js
  4. Learned how to configure script element attributes and company-wide scope
  5. Deployed the extension locally, via Docker, and via Liferay Cloud
  6. Added the extension to a page through the Advanced JavaScript configuration
  7. 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:


Wasim Shaikh

About the author

Wasim Shaikh is a UI/UX developer and front-end engineer with 15+ years of experience, based in Ahmedabad, India. He specializes in Liferay, React, Angular, Next.js and Tailwind CSS.

Keep reading

Web dev · 8 Jul 2026Creating Your First Client Extension: A Step-by-Step Tutorial (globalCSS)Web dev · 8 Jul 2026Creating Your First Custom Element: A Step-by-Step Tutorial (React Weather Card)Web dev · 8 Jul 2026Sharing JavaScript Libraries Across Pages: The JS Import Map Entry Client Extension (jsImportMapsEntry)