Hosting a Vue.js App in Bloomreach CMS
Damir Arh
2019-07-30
Bloomreach Experience Manager wasn't designed for web application development but for website development. While you could theoretically develop an application as a Bloomreach component, you would miss a lot of tooling that you are taking for granted today. As an alternative, you could develop a single page application (SPA) using any modern JavaScript framework and then serve the resulting static files (HTML, JS and CSS) from Bloomreach. In this post, I will describe my configuration for developing an application in Vue.js.
Setting up the Vue.js application
I started by creating a new Vue.js application in the root folder of my Bloomreach CMS project using the Vue CLI:
vue create vue-app
During development, it's most convenient to use the serve
command to have the application running all the time with live rebuild and refresh every time a source file changes:
npm run serve
Bloomreach CMS and Vue.js happen to use the same default port for serving the application: 8080. If you start Bloomreach CMS first, then Vue.js will automatically use the next available port instead (usually 8081). However, if you happen to start Vue.js first, then Bloomreach CMS will fail with the following error:
org.codehaus.cargo.container.ContainerException: Port number 8080 (defined with the property cargo.servlet.port) is in use. Please free it on the system or set it to a different port in the container configuration.
To avoid the inconvenience, it's best to reconfigure Vue.js to always use a different port. To do that, you can create a file named vue.config.js
in your Vue.js application folder with the following contents:
module.exports = {
devServer: {
port: 8081
}
};
Now, Vue.js will always use port 8081 and both applications will happily coexist, no matter which one you start first.
Serving Vue.js static files from Bloomreach CMS
In Bloomreach CMS, static files belong in the repository-data/webfiles
submodule. I decided to have them in the vue-app
subfolder. The static files will be generated using the build
command:
npm run build
You can easily change the output folder with another entry in vue.config.js
:
module.exports = {
outputDir: '../repository-data/webfiles/src/main/resources/site/vue-app',
devServer: {
port: 8081
}
};
Bloomreach CMS doesn't automatically serve all the files in the repository-data/webfiles
submodule. Because I created a new folder at the root level, I need to whitelist it by adding another entry for my folder to the hst-whitelist.txt
file:
css/
fonts/
js/
vue-app/
Customizing the application start page
By default, the Vue.js application entry page is a standalone index.html
file. When hosting the application from Bloomreach CMS, you'll probably want to keep the standard header and footer. This means that instead of a static HTML file, you'll need to generate a FreeMarker template file.
Bloomreach CMS always adds a dynamic hash to the path of static files to avoid unwanted caching in browsers. This means that the paths to the referenced JS and CSS files must be generated using the hst.webfile
tag. To put the CSS files in the head of the generated HTML page, the hst.headContribution
helper must be used.
The resulting template for generating the final .ftl
will end up being similar to the following. I named it index.ftl
and put in the public
folder next to the default index.html
file:
<#include "../freemarker/include/imports.ftl">
<base href="<@hst.webfile path='/vue-app/index.html' />">
<noscript>
<strong>We're sorry but vue-app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<% _.forEach(htmlWebpackPlugin.files.js, function(js) { %>
<script src="<@hst.webfile path='/vue-app/<%= js %>' />"></script>
<% }) %>
<% _.forEach(htmlWebpackPlugin.files.css, function(css) { %>
<@hst.headContribution category="vue-head">
<link rel="stylesheet" href="<@hst.webfile path='/vue-app/<%= css %>' />" />
</@hst.headContribution>
<% }) %>
The Vue.js project is preconfigured for Lodash templates. That's the templating syntax in the snippet above which you might not have recognized. There are a couple more things to mention about the template above:
- The
include
tag at the top points to Bloomreach's defaultimport.ftl
file which is required to make available thehst
tags I used. - I added a
base
element so that all relative links generated by the Vue.js app will be correctly treated as relative to the root folder with the generated Vue.js application files. Otherwise, all URLs for Vue.js static assets and router links would be incorrect. Bloomreach CMS uses absolute paths for its links so these won't be affected. - I decided to ignore the Vue.js application files marked for preload or prefetch. The application will work correctly without them and I don't care about the optimization aspect enough yet to be bothered by this.
- For the
hst.headContribution
entries, I used thevue-head
category. Modify it as necessary for your setup to have them put in the head of the HTML page. With Bloomreach's default configuration, that's what happens with all unrecognized categories.
To use the new index.ftl
file for generating the entry page and to process it correctly, additional modifications are required in the vue.config.js
file. Below are its final contents:
module.exports = {
publicPath: '', // use relative paths for Vue.js assets and links
indexPath: 'index.ftl', // output filename for the generated FreeMarker template
outputDir: '../repository-data/webfiles/src/main/resources/site/vue-app',
chainWebpack: config => {
config.plugin('html').tap(options => {
// custom config for generating the FreeMarker template
if (process.env.NODE_ENV === 'production') {
options[0].template = 'public/index.ftl';
options[0].inject = false;
options[0].minify = false;
}
return options;
});
},
devServer: {
port: 8081
}
};
I added comments for all the new entries. The first two are pretty self-explanatory:
- The
publicPath
property switches Vue.js from absolute paths to relative paths. This is required because the page is not served from the root of the domain. This change works in combination with thebase
element in the template above. - I changed the filename of the generated file from
index.html
toindex.ftl
so that Bloomreach CMS will process the template correctly.
The most important change is the customized configuration for Webpack's HTML plugin which is responsible for generating the index.html
file (or index.ftl
in my case):
- I had to disable the default injection of CSS and JS files which was putting the corresponding elements at the bottom of the
head
orbody
element as necessary. Instead, I now use Lodash markup to inject them where I need them. - I also had to disable minification which failed because of FreeMarker markup in the generated file.
- The final change specifies the path to my input template (instead of the default
public/index.html
value).
I only apply all of these changes for production
environment. This makes sure that the new configuration is only used with the build
command and keeps the behavior of the serve
command unchanged.
Rebuilding Vue.js application on every change
To update the Vue.js files served by Bloomreach CMS, the build
command must be invoked. Hopefully, you will be able to do most of your development using the serve
command. Still, when doing the final testing inside Bloomreach CMS, having to manually call the build
command for every change is not very convenient.
To avoid that you can use the npm-watch
package:
npm install npm-watch --save-dev
In your package.json
file, you can add a script for it and configure it to trigger the build
command whenever a file in the src
folder changes:
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"watch": "npm-watch"
},
"watch": {
"build": {
"extensions": "vue,ts,png",
"patterns": [
"src"
]
}
},
Now you can simply run the watch
script and have it automatically rebuild the Vue.js application whenever you make a change to it:
npm run watch
Once the files in the repository-data/webfiles
submodule change, Bloomreach CMS will automatically reload the page in your browser, making the experience similar enough to using the serve
command.
Serving the Vue-js application as a page in Bloomreach CMS
To serve the Vue.js application as a page in Bloomreach CMS, the following nodes must be added through the CMS Console (to the individual subnodes of the configuration node for your Bloomreach CMS project, e.g. /hst:hst/hst:configurations/hippovue
):
-
To register the template, an appropriately named (e.g.
vue-app
) node of typehst:template
must be added to thehst:templates
subnode with the following property value pointing at the generatedindex.ftl
file:hst:renderpath
with valuewebfile:/vue-app/index.ftl
-
To register the page, an appropriately named (e.g.
vua-app
again) node of typehst:component
must be added to thehst:pages
subnode. Its properties and subnodes will depend on how you are structuring your pages. With default Bloomreach CMS setup, the following are required:hst:referencecomponent
property with valuehst:abstractpages/base
- subnode of type
hst:component
namedmain
withhst:template
property value matching the previously created template node (i.e.vue-app
).
-
To add the page to the sitemap, a node of type
hst:sitemapitem
must be added to thehst:sitemap
subnode, either directly or indirectly. The exact position and name will depend on the desired URL. To add a page at the root of the site, a node with an appropriate name (e.g.vue-app
) must be added as a direct child of thehst:sitemap
subnode. The following properties are required:hst:componentconfigurationid
pointing at the previously created page, i.e.hst:pages/vue-app
hst:pagetitle
containing the title of the page node, e.g.Vue application
hst:refId
with a unique ID value, e.g.vue-app
With the configuration described, the Vue.js application will be served at http://localhost:8080/site/vue-app.
Building the Vue.js application with Maven
To build the Bloomreach CMS site as a whole with a single command, the Vue.js build
command will need to be invoked from Maven. The frontend-maven-plugin
plugin can be used for that.
Since the plugin is designed to only work with a single package.json
file, it's best to create a separate Maven module corresponding to the Vue.js application. If you ever want to serve multiple Vue.js applications from a single Bloomreach CMS instance, you will then be able to simply repeat the same steps without having to change anything regarding the existing application.
Create a pom.xml
file with the following contents in your Vue.js application folder:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<!-- match info from your parent module -->
<groupId>com.damirscorner.blog.samples</groupId>
<artifactId>hippo-vue</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<name>Hippo Vue App</name>
<description>Hippo Vue App</description>
<artifactId>hippo-vue-vue-app</artifactId>
<packaging>pom</packaging> <!-- no Java artifacts -->
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<!-- Use the latest released version:
https://repo1.maven.org/maven2/com/github/eirslett/frontend-maven-plugin/ -->
<version>1.7.5</version>
<configuration>
<workingDirectory>.</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<!-- See https://nodejs.org/en/download/ for latest node (lts) version -->
<nodeVersion>v10.15.3</nodeVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
There are comments explaining the key parts. The frontend-maven-plugin
is used to download Node.js and NPM locally, install the NPM packages and run the build
command. The Maven submodule has the Bloomreach CMS root module specified as its parent.
A submodule entry for this new module must be added to the root pom.xml
file:
<modules>
<module>repository-data</module>
<module>cms</module>
<module>site</module>
<module>essentials</module>
<module>vue-app</module> <!-- newly added submodule -->
</modules>
In the repository-data/webfiles
submodule, the new Vue.js module must be added as a dependency to ensure correct build order:
<dependencies>
<!-- other dependencies skipped -->
<dependency>
<groupId>com.damirscorner.blog.samples</groupId>
<artifactId>hippo-vue-vue-app</artifactId>
<version>0.1.0-SNAPSHOT</version>
<!-- no Java artifacts -->
<type>pom</type>
</dependency>
</dependencies>
The Vue CLI build
command will now be invoked before the build of the existing repository-data/webfiles
submodule. This way, the files from the freshly built Vue.js application will already be placed inside it before all the web files are further packaged.
This blog was originally published on my own blog "Damir's Corner".