Introducing Site Initializers

Bundle Site Initializers

Introduction

It has always been a discussion, whether content creation is a development activity. Even if it's considered as not - we, as developers, often need to set up content:

- on local environments;
- on DEV servers (to demonstrate some functionality during development);
- even on PROD servers (to deliver the initial content for customers).

Manual content setup on each environment takes time, and leads to differences on them (due to the human factor) and issues like "not reproducible on my machine" ?

Content Setup Tools Overview

We had always tried to automate the process of content creation: at least, the initial content to start with. But we were missing a suitable tool for this.

Initially, we used the Resource Importer to import content together with a theme. But it had some limitations, and became deprecated after Liferay 7.x release (as it does not support content pages).

Then, we implemented a custom solution - Portal Initializer. It could create/update users and roles, sites and most of the site content based on XML descriptors. It worked fine for us, but it became a problem to support it: as Liferay started more often releases, and there were significant changes in the APIs and internal representation of portal content (especially for content pages).

Sometimes we used Upgrade Processes.  They are good to run code once, and create some assets on the portal (e.g. custom fields). But, as they require custom development, it's not a good option for a large amount of content setup, and for content updates during development.

Also, there is a LAR file import/export option. We can develop content in one environment, and export it to another. But LARs are very fragile: they stop on the first error, do not provide any useful information in logs, and can fail for any unpredictable reason. In most cases, LAR import errors are related to content. While professional developers can identify and fix such issues after some research, for regular Liferay users they can be stucking.

Finally, the Site Initializer option appeared in Liferay. 

Site Initializers Introduction

Site Initializers provide a new way for site content setup (since Liferay 7.1).

When I first met Site Initializers, the impression was not promising. They provided just an extension point, which we could use for content creation. But we still had to write our custom code for this: to create layouts, fragments, structures, web-contents, etc. This is not what I expected from Liferay: I'd expect this "content creation" code to be OOTB - so, content developers could just put content definitions into the descriptor files, with zero lines of custom code.

Then, in the first GAs of 7.4, I found new "Site Templates" in my Liferay: "Minium" and "Speedwell" (which were actually site initializers). After source code analysis I found out that such "content creation" features are already there - but they are parts of the "commerce" module. I was wondering why Liferay does not make them "public" to expose such functionality to developers, and asked that in the community slack.

Afterwards (maybe, because of my suggestion ?), a BundleSiteInitialzer was introduced in Liferay.
And new "Site Templates" (e.g. "Masterclass") were also created as examples.

This feature was exactly what I had been looking for: 
- Liferay OOTB;
- No custom code required for content creation;
- All content definitions can be put to descriptor files, and used for content setup during site creation.

Although it's not documented (yet), we still can use Liferay sources and examples as a reference, and will try to put some light on this topic.

In this blog we'll see how we can use BundleSiteInitialzer for a custom site setup.

Practice

Example Description

Last year I wrote a blog about "Code-Less" Site Building and was a speaker at /dev/24 on the same topic.  We'll continue using the same "Learning Center" example here (a little bit extended), but will automate it with a custom site initializer.

Module Setup

First, we need to create a module with the following structure:

liferay-site-initializer-demo
├──src
│  └──main
│     └──resources
│        ├── META-INF
│        │   └── resources
│        │       └── thumbnail.png
│        └── site-initializer
│            ├── ddm-templates   
│            ├── documents   
│            ├── fragments   
│            ├── layout-page-templates     
│            ├── layout-set     
│            ├── layouts     
│            ├── style-books     
│            ├── expando-columns.json     
│            └── roles.json  
├── bnd.bnd
└── build.gradle

bnd.bnd file:

Bundle-Name: Liferay Site Initializer Demo
Bundle-SymbolicName: com.liferay.site.initializer.demo
Bundle-Version: 1.0.0
Liferay-Site-Initializer-Name: Learning Center
Provide-Capability: liferay.site.initializer
Web-ContextPath: /liferay-site-initializer-demo

In the bnd.bnd file we need to declare liferay.site.initializer provide-capability, and define the site initializer name.

build.gradle file:

dependencies {
   compileOnly group: "com.liferay.portal", name: "release.portal.api"
}

build.gradle file is standard, nothing special here.

After module deployment we should see an additional item in site template selection during site creation:


Note: change thumbnail.png file to put a different preview image.

Not let's  see how we can define content for our site initializer.

Roles Definition

To create roles in Liferay - we just need to put role definitions into the roles.json file, sample:

[
   {
      "actions": [
      ],
      "name": "Learning Center Publisher",
      "scope": 1,
      "type": 1
   }
]

This will create a  "Learning Center Publisher" regular role on portal after site creation from the template.

Fragments Definition

We need two fragments for our site:  lc-header and lc-footer (for header and footer). To create them we need to define the following structure (under the site-initializer folder):

site-initializer
└── fragments
    └── group
        └── learning-center
            ├── fragment   
            │    ├── lc-footer   
            │    │   ├── fragment.json   
            │    │   ├── index.css         
            │    │   ├── index.html
            │    │   ├── index.js 
            │    │   ├── index.json
            │    │   └── thumbnail.png
            │    │  
            │    └── lc-header   
            │        └── ...
            ├── resources   
            │   └── lc-logo.svg
            └── collection.json

collection.json file defines fragment collection name and description:

{
   "description": "Learning Center Initializer Fragments",
   "name": "Learning Center"
}

Fragment files are put inside the fragment folder. fragment.json file defines fragment-specific settings, sample:

{
   "configurationPath": "index.json",
   "cssPath": "index.css",
   "htmlPath": "index.html",
   "jsPath": "index.js",
   "name": "LC Footer",
   "thumbnailPath": "thumbnail.png",
   "type": "component"
}

Other files are used to define fragment's HTML, CSS, JS, configuration and icon.

Optionally, we can add fragment resources into resources folder and reference them in fragment's code, sample:

<img alt="Liferay Logo" class="h-50"
    src="[resources:lc-logo.svg]"
    data-lfr-editable-type="image"
    data-lfr-editable-id="liferay-logo"
/>

Once site is created from initializer template - fragments should be created on site:

Custom Fields Definition

If we need to define a custom field - we can also do it with site initializer, using the expando-columns.json file. For example, we can create a custom field on Layout to store icon URLs for pages:

[
  {
    "dataType": 15,
    "modelResource": "com.liferay.portal.kernel.model.Layout",
    "name": "Icon URL"
  }
]

Note: constants for "dataType" field can be found in ExpandoColumnConstants class.

Documents Definition

Documents creation is pretty easy with site initializer: we just need to put files into site-initializer/documents/group folder (or site-initializer/documents/company if we want to create them in a global context). We can also use folders inside, and the structure will be repeated in the Documents and Library on portal.

Widget Templates Definition

In our example we have two custom widget templates for menu navigation: Left Navigation and Chapter Navigation. To create them with a site initializer - we need the following structure:

site-initializer
└── ddm-templates
    └── group
         ├── chapters-navigation
         │   ├── ddm-template.ftl
         │   └── ddm-template.json
         └── left-navigation
             ├── ddm-template.ftl
             └── ddm-template.json

Widget template settings are defined in the ddm-template.json, sample: 

{
   "className": "com.liferay.portal.kernel.theme.NavItem",
   "ddmTemplateKey": "CHAPTER_NAV",
   "name": "Chapters Navigation",
   "resourceClassName": "com.liferay.portlet.display.template.PortletDisplayTemplate"
}

Template code is defined in the ddm-template.ftl file.

Defined widget templates should be automatically created after site creation:


 

Style Books Definition

We can create a custom Style Book and override the token definition values (defined in Classic Theme). In our case we can define our own branding colors in a custom style book. To do this, we need to define the following structure:

site-initializer
└── style-books
    └── lc-style-book
         ├── frontend-tokens-values.json
         ├── style-book.json
         └── thumbnail.png

and define the branding colors in the frontend-tokens-values.json file. Token and CSS variable names can be taken from frontend-token-definition.json file:

In the style-book.json file we can define Style Book settings:

{
   "defaultStyleBookEntry": true,
   "frontendTokensValuesPath": "frontend-tokens-values.json",
   "name": "Learning Center Style Book Entry",
   "thumbnailPath": "thumbnail.png"
}

With defaultStyleBookEntry=true property Style Book becomes applied for site pages by default. 

Finally, we can use our custom colors in the "Brand Colors" section: 

Layout Set Definition

We may define the default options for a Layout Set using the following file structure:

site-initializer
└── layout
    └── public
        ├── css.css
        ├── js.js
        ├── logo.png
        └── metadata.json

Here we defined the configuration for public pages. If you need to define configuration for private pages - create a private folder (with the same structure as public). 

Note: if you need both public and private pages - enable "Private Pages" functionality in the Release Feature Flags.

In the css.css file we can define custom CSS code for the Layout Set (it will be visible in Look & Feel settings on the created site). In a similar way we can put custom Javascript code into a js.js file (will appear in Advanced -> Javascript configuration of the Layout Set). We can also define a custom logo with logo.png file. In the metadata.json file we can define the required theme settings, sample:

{
   "settings": {
      "lfr-theme:regular:show-footer": false,
      "lfr-theme:regular:show-header": false,
      "lfr-theme:regular:show-header-search": false,
      "lfr-theme:regular:show-maximize-minimize-application-links": false,
      "lfr-theme:regular:wrap-widget-page-content": false
   },
   "themeName": "Classic"
}

Master Pages Definition

We have two Master pages: LC_DEFAULT (for welcome and search page) and LC_LEFT_NAV (for other pages, which have navigation on the left). So, we need to define the following structure:

site-initializer
└── layout-page-templates
    └── master-pages
         ├── lc-default
         │   ├── master-page.json
         │   ├── page-definition.json
         │   └── thumbnail.png
         └── lc-left-nav
             ├── master-page.json
             ├── page-definition.json
             └── thumbnail.png

master-page.json file defines the name of Master page:

{
   "name": "LC_DEFAULT"
}

page-definition.json file defines the structure of the Master page (see Page Content Definition chapter below). At the starting point, Master page should contain only the DropZone element:

{
  "pageElement": {
    "pageElements": [
      {
        "definition": {
          "fragmentSettings": {
            "unallowedFragments": []
          }
        },
        "type": "DropZone"
      }
    ],
    "type": "Root"
  }
}

thumbnail.png file is used as a preview image.

After site creation we can see two Master pages in the "Page Templates" section: 


 

Pages Templates Definition

We can also create page templates to simplify individual pages creation on the site. Page Template definitions can be also put to layout-page-templates folder (as well, as Master pages), but inside page-templates subfolder:

site-initializer
└── layout-page-templates
    └── page-templates
         ├── lc-chapter-template
         │   ├── page-definition.json
         │   └── page-template.json
         ├── lc-content-template
         │   ├── page-definition.json
         │   └── page-template.json
         ├── ...

page-template.json file defines the name of the Page Template:

{
  "name": "LC Chapter"
}

page-definition.json file defines the structure of the Page Template (see Page Content Definition chapter below).

Once site is created from the initializer - defined page templates should be created automatically:


 

Pages Definition

To define pages (Layouts) we need to create page descriptors inside site-initializer/layouts folder:

Folder names ("1_welcome", "2_home", etc.) do not matter, but they define the order and nesting of site pages. 

page.json file defines the page information, sample:

{
   "hidden": false,
   "private": false,
   "name_i18n": {
      "en_US": "Welcome"
   },
   "friendlyURL_i18n": {
      "en_US": "/welcome"
   },
   "system": false,
   "type": "content",
   "typeSettings": [
      {
         "key": "collectionType",
         "value": "com.liferay.item.selector.criteria.InfoListItemSelectorReturnType"
      }
   ]
}

Here we can define if a page is hidden or not, if it's public or private, specify the page type and define the localized names and friendly URLs.

page-definition.json file defines the structure of the Page Template (see Page Content Definition chapter below).

Page Content Definition

Well, page content definition is the most tricky part here. If we look at any page-definition.json files in our example or in Liferay sources - we'll see that they are large, and not very obvious. Writing these files by hand could be the nightmare. 

Fortunately, there is a "magic trick", which we can use for page content definition. We need to follow the next steps.

1. Create Page Content in the UI

Yes, first time we need to create the content manually. For example, for LC_DEFAULT Master page we can add custom header and footer, define colors and other styling, put and configure links and social navigation in the footer, etc. As a result - we'll get a page like this.


During content setup we need to put all the content inside a wrapping container. Using new Liferay features, we can define custom names for different page elements, e.g.:


We can also define custom CSS classes for elements, if needed:

Once page content is ready, we can move forward to the next step.

2. Save Fragment Composition

Now we need to save fragment's composition for the main (wrapper) element:


Define the fragment name, check "Save Inline Content" and "Save Mapping Configuration and Link" checkboxes, and save the configuration:


Finally, a new fragment should appear in site fragments:


 

3. Export Fragment

Export created fragment:


Extract files from archive, and open the fragment-composition-definition.json file:

4. Update Page Definition

Put content from the exported fragment-composition-definition.json file to the appropriate page-definition.json file in the site-initialzer module. In our case we need to put it to site-initializer/layout-page-templates/master-pages/lc-default/page-definition.json:


Once the page-definition.json file is updated - re-deploy the site-initializer module, and re-create the site using updated initializer template: page content should be automatically created now. 

Generally, content creation using this flow becomes a cycle:


We need to create content for a certain page, update page definition (using exported JSON), re-deploy initializer and re-create the site. Then we can move to the next page, and make the same steps. Finally, we'll get to a point, where we can create the whole site with all the content in "one click":


 Impressive, right? ?

Other Content Types Definition

There are other content types supported by BundleSiteInitializer, but not covered in this blog: categories and tags, web content, objects and  various commerce entries, etc. All of them can be found with the source code analysis of this class and examples, provided by Liferay.

Source Code 

Code samples used in this blog checked-in to Github repository.

Also, Liferay examples can be checked as a reference, e.g.:

- Masterclass;

- RayLife;

- Liferay Learn.

Pros and Cons

While this approach for content creation seems to be amazing, it has its  drawbacks:

1. No update support

Unlike regular Site Templates (where changes in template can be propagated to sites), site initializers do not support updates: they work only for the very first site initialization. When changes are made to initializers - the site should be re-created again. This works only for non-production environments, and only in cases when there is no manually inserted content.

2. Not all content types are supported

Most of Liferay assets are covered by the bundle site initializer. But there are some types of content, which are not supported: Blogs, Wikis, Forms. I have opened a ticket for Forms support already. 

3. Additional Time Required

Preparing content descriptor files may take some time, especially for content pages. For sites with predefined pages/content structure this time can be compensated by lower efforts for local environments/servers configuration. But for dynamic sites with frequent updates and not clear enough requirements this time can increase significantly.

On the other site, we get the following benefits using this approach:

1. Content Version Control

As long as we use file descriptors for content definitions and a Git repository - we can easily rollback to a previous version, re-deploy initializer, re-create a site - and will get a previous version of site.

2. Identical Environments

As all the content is defined in descriptor files - site content after initialization should be the same on each environment. No "not reproducible on my machine" issues anymore.

 3. Single-Click Deploy

No need to create all the pages/content on each environment, or deal with LAR files. Just deploy the initializer, create a site from template - and that's it!  

Conclusions

There are different ways of content setup in Liferay. One of them is "Site Initializer". Bundle Site Initializer provides us with the possibility to define site content using file descriptors, without any custom Java code. Using this approach we can unify different environments and simplify the deployment process. This feature is not documented yet, but examples in Liferay sources are self-explanatory. This approach has different pros and cons: we may use it during development for sites with pre-defined pages structure and content, but it's not recommended to set up content this way on production environments,  or other environments with frequent manual content updates.

 

Leave your comments and feedback.

Stand with Ukraine 

Enjoy ?

Vitaliy Koshelenko

Liferay Architect at Aimprosoft

v.koshelenko@aimprosoft.com


 

 

 

Blogs

Nice summary!

For me the main disadvantage of site initializers is still that it is not able to create content/tempates etc. for the default "Liferay" site. So it is not possible to prepopulate a new Liferay instance with content. Therefore we still keep up with the resources importer (we wrote a custom extended version which is able to import more content types...)

Thanks!

Actually, we try to avoid using the default site (especially, when there are mupliple sites in the instance). We can define a custom landing page to point to the required site, or implement a LoginPostAction for more complex logic.

That is an awesome tool and this article is really interesting. But "update" support is more than necessary. In the real word we absolutely need to control at least structures and templates from version control and deploy it automatically with CI / CD. Site Initializers are not really usefull until some update feature is supported. Please consider it.

Yea, missing "update" support is a big disadvantage, and I pointed it as #1 in my blog.

But we still can use this feature during development, when there is no manualy inserted content, and we can hit the "Delete" button on a site without any fear :) 

I'd not say, that initializers are not really useful: we're usign this tool successfully on the current project with a bunch of pages, fragments, content, etc. - the deployment process and local env setup became much more easier,  issues with environment differences are eliminated. But yeah, repeatable site re-creation is a little bit annoying. And, once we go to Production and customer starts inserting the content manually - we'll forget about this tool. On the other hand - insering PROD content programatically is not a good idea, too.

Anyways, I have opened a ticker for "update" support - hope, Liferay team will take a look: https://issues.liferay.com/browse/LPS-162000 

Hi, I'm trying to generate a Content Page containing a Web Content Article. I've generated de Page Template and exported successfully to my SI however, when I create the site, it comes out the Web Content Article does not show anything (actually displaying a message about the content I se no longer exist, which is false, it does exist).

I think is probably becase in the page-definition.json the articleId and groupId changes every time the site is created. Any advice about how to avoid the articleId and groupId changes?

Thanks in advance.

Nevermind, the workaround is leave the assetEntryId and groupId empty.

Well, Done !! vary helpful. Can you please suggest a solution for the structure and templates for the web content article?

Thanks!  As for web content structures/templates - you can follow an example from site-initializer-masterclass (https://github.com/liferay/liferay-portal/tree/master/modules/apps/site-initializer/site-initializer-masterclass/src/main/resources/site-initializer):

1) Create "ddm-structures" folder and put XML definitions for structures;

2) Create "ddm-templates" folder and a subfolder for each template with files:

- ddm-template.json - metadata (name/ddmStructureKey/ddmTemplateKey);

- ddm-template.ftl - the template code.

 

Optionally, you can also create a "journal-articles" folder with pre-defined articles.

Thanks, Vitaly !! What if we want to create a widget template for Asset Publisher Template then what would be the JSON value for it? F.E. We used the below-mentioned JSON for the menu display item. {   "className": "com.liferay.portal.kernel.theme.NavItem",   "ddmTemplateKey": "HEADER_NAVIGATION",   "name": "Header Navigation",   "resourceClassName": "com.liferay.portlet.display.template.PortletDisplayTemplate" } Then, what we want to create for Asset Publisher Template?

If you need to define a widget template for the Asset Publisher  - you need to change the className from 

com.liferay.portal.kernel.theme.NavItem 

to

com.liferay.asset.kernel.model.AssetEntry 

The ddmTemplateKey and name should be defined as in sample above.

The resourceClassName should be still "com.liferay.portlet.display.template.PortletDisplayTemplate" as it's a widget template.