Blogs
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;
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 ?