Extending Jahia UI

January 31, 2023

To declare a new module, you have two options:

  • A simple module with very little to no JavaScript (for example, a legacy site settings)
  • More complex modules with React and using Jahia moonstone design system

Common step

Declare your module

Create a jahia.json file under src/main/resources/javascript/apps/jahia.json with the following content:

  "jahia": {
    "apps": {
      "jahia": "javascript/apps/register.js"

Let's decrypt the most important part "jahia": "javascript/apps/register.js". The key here is the app you want to extend and the value is the path to the file that will load/register your app. This file will be the entry point that will contain the initialization part of your module.

Load/Register your app

You will need to create the JS file you declared in the jahia.json file, in this example register.js, and add the logic you need inside. The module can register different extensions in the application. In a simple JS module, this file can be written manually. In complex modules, it will be generated by a JS bundler like webpack. This file should remain small, as the application won't start until it's fully loaded and executed.

You can find a simple example of such jahia.json and regirstration files in the Event module


The registrations need to be done in the register.js file (the one specified in the jahia.json file)

All extensions are added into the registry by calling the registry.add function. The registry object can be found by importing registry from @jahia/ui-extender. The add function task has at least 3 parameters: the type of extension to register, its unique id, and an object describing the extension.

To ease this process when doing simple JS, we expose a few things in the window.jahia object. As of today in the jahia object you can find:

  • uiExtender, which contains the registry
  • moonstone, which contains React components and utility functions from the moonstone design system
  • i18n, which contains all the necessary function to handle translations


React routes can be added in the application as extensions. For example, the following can be used to declare a new route/linkchecker (path) that will display a linkchecker (render) in the main area (target) :

window.jahia.uiExtender.registry.add('route', 'route-jcontent', {
      targets: ['main:2'],
      path: `/linkchecker`, // Catch /linkchecker urls
      render: function () {           // The render function for my route in this example we want to display an iframe which contains our legacy site settings, to do that we pass the URL to the `getIframeRenderer` function
        return window.jahia.uiExtender.getIframeRenderer(window.contextJsParameters.contextPath + '/cms/editframe/default/sites/$site-key.linkChecker.html');

Primary nav items

Items in the primary nav bar can be added as primary-nav-item. Here, add a link to /linkchecker (path) into the main primary nav (target) with a label and an icon.

// Call the add method from the registry which is in uIExtender to add a menu entry to point to my module
// `primary-nav-item` is the type I want to register and `linkcheckerRoute` is the key (must be unique), the last parameter is an object with the necessary options
window.jahia.uiExtender.registry.add('primary-nav-item', 'linkcheckerRoute', {
    targets: ['nav-root-top:21'],       // Which menu I want to extend, it can take multiple values, each value can be ordered `target:position`
    path: '/linkchecker',               // Path to call when clicking on my link
    label: 'linkchecker:label.title',   // RB to use to display the name of my link `namespace:key`
    icon: window.jahia.moonstone.toIconComponent('Feather')  // Icon to use with my link, we must use the `toIconComponent` function to make sure we return an Icon Component

Admin panels

Administration panels can be registered with a single entry, that will add an item in the tree and the appropriate route at the same time.

window.jahia.uiExtender.registry.add('adminRoute', 'linkchecker', {
    targets: ['jcontent:10'],
    label: 'linkchecker:label.title',
    isSelectable: true,
    requiredPermission: 'siteAdminLinkChecker',
    requireModuleInstalledOnSite: 'linkchecker',
    iframeUrl: window.contextJsParameters.contextPath + '/cms/editframe/default/$lang/sites/$site-key.linkChecker.html'

See more in settings documentation.


You can add actions in the application anywhere that buttons or menu entries display.

window.jahia.uiExtender.registry.add('action', 'downloadFile', {
    buttonIcon: window.jahia.moonstone.toIconComponent('CloudDownload'),
    buttonLabel: 'jcontent:label.contentManager.contentPreview.download',
    targets: ['contentActions:10'],
    onClick: context => {

Translation namespaces

If you want to use translated label in your module, you need to declare your module namespace, which must be your module name :

// Declare the namespace to find the translations for my module, put the name of your module and reuse it in your labels

This will download the translations from JSON files under the following path src/main/resources/javascript/locales/<lang>.json:

  "label" : {
    "title": "Link Checker"

Such label can be used in your code with: {moduleId}:{label.path}
For instance, to retrieve the title label for the linkchecker module, you will need to use:


More complex modules


Add a maven dependency on app-shell to your project with the classifier manifest:


And use copy plugin:


This provides the manifest that contains all shared packages while building the application or extension.

Set webpack configuration to use DLL and CopyWebpackPlugin to provide package.json

The output exposed of the extension must be a single file named *.bundle.js

    output: {
        path: path.resolve(__dirname, 'src/main/resources/javascript/apps/'),
        filename: 'content-editor-ext.bundle.js',
        chunkFilename: '[name].content-editor.[chunkhash:6].js'

And the plugin configuration:

    plugins: [
        new webpack.DllReferencePlugin({
            manifest: require('./target/dependency/app-shell-1.0.0-SNAPSHOT-manifest')
        new CopyWebpackPlugin([{ from: './package.json', to: '' }])