Hosting a Vue.js App in Hippo CMS

April 19th 2019 Hippo CMS Vue.js Maven

Hippo CMS wasn't designed for web application development. While you could theoretically develop an application as a Hippo 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 Hippo. 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 Hippo 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

Hippo CMS and Vue.js happen to use the same default port for serving the application: 8080. If you start Hippo 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 Hippo 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 Hippo CMS

In Hippo 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
  }
};

Hippo 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 Hippo 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.

Hippo 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 Hippo's default import.ftl file which is required to make available the hst 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. Hippo 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 the vue-head category. Modify it as necessary for your setup to have them put in the head of the HTML page. With Hippo'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 requires because the page is not served from the root of the domain. This change works in combination with the base element in the template above.
  • I changed the filename of the generated file from index.html to index.ftl so that Hippo 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 or body 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 Hippo 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 Hippo 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, Hippo 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 Hippo CMS

To serve the Vue.js application as a page in Hippo CMS, the following nodes must be added through the CMS Console (to the individual subnodes of the configuration node for your Hippo CMS project, e.g. /hst:hst/hst:configurations/hippovue):

  • To register the template, an appropriately named (e.g. vue-app) node of type hst:template must be added to the hst:templates subnode with the following property value pointing at the generated index.ftl file:

    • hst:renderpath with value webfile:/vue-app/index.ftl
  • To register the page, an appropriately named (e.g. vua-app again) node of type hst:component must be added to the hst:pages subnode. Its properties and subnodes will depend on how you are structuring your pages. With default Hippo CMS setup, the following are required:

    • hst:referencecomponent property with value hst:abstractpages/base
    • subnode of type hst:component named main with hst: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 the hst: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 the hst: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 Hippo 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 Hippo 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 Hippo 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.

Copyright
Creative Commons License