Starter: My first site


A step-by-step tutorial for building your first website with Enonic XP


This guide will take you through the steps of creating a site, using the basic building blocks of Enonic XP and it’s MVC JavaScript framework.

If you are looking for XP’s headless API? Visit the Headless CMS intro to get started.

During this exercise you will:

  • learn about content types

  • setup pages and parts

  • create page templates

  • contribute scripts to pages

  • provide configuration from file

  • and more…​


Create project

To setup a project locally, simply run the following command:

enonic project create -r starter-myfirstsite

Remember to create a new XP sandbox when completing the project wizard.

Don’t have the Enonic CLI? Visit the Getting started guide to install it.

Project structure

The project folder created should now contain a project structure, looking something like this:

build.gradle (1)
build/ (2)
    java/ (3)
    resources/ (4)
      import/ (5)
        content-types/ (6)
        parts/ (7)
        pages/ (8)
        site.xml (9)
1 gradle files are used by the app build system
2 Contains output files produced by the build
3 Optional folder for Java code (used to import sample site in this starter)
4 Main location for XP specific project code
5 The sample site data are located here and imported when the app starts
6 The starter ships with two content types, country and city
7 Part components - used for building pages
8 Page components - root component for pages
9 Site descriptor, specifying this is a site app

Building and Deploying

To build and deploy the app, run this command from your shell:

enonic project deploy

Accept starting the sandbox

To verify that your app started successfully, you should find an entry similar to this in the sandbox log:

2019-04-09 13:40:40,765 INFO ... Application [] installed successfully

Sample Site

For convenience, the starter includes a Hello World sample site with some relevant content (countries and cities). The items are imported the first time the application starts.

To see the site and content, launch Content Studio by selecting it from the main menu in XP’s admin console:

If you cannot see Content Studio in the menu, it has not been installed. Install it through the Welcome tour, or install directly from the Applications app found in the main menu.

Tree structure showing countries and cities in Content Studio

Content Types

All item created by the cms and content studio are based on a content type. Content types can be considered the core concept of the XP CMS.

XP ships with a set of standard content types such as Site, Folder and a set of specialized types to handle files, also called Media Types. Additionally, apps may contain custom content types. In our case, the content types Country and City are pre-defined in the starter.

The City content type looks something like this for an editor:

Form for the city content type

While the descriptor file looks like this: .src/main/resources/site/content-types/city/city.xml

<content-type xmlns="urn:enonic:xp:model:1.0">
  <description>Place where many people live</description>
    <input type="GeoPoint" name="location">
      <occurrences minimum="1" maximum="1"/>
    <input type="Long" name="population">
      <occurrences minimum="0" maximum="1"/>

For a better UI, you may also include an icon (svg or png are recommended) for your content types. Icons make it easier for editors to use Content Studio.

More information about Content types and the schema system are available in the XP reference documentation.


We now have some content, but no preview or rendering. To perform server side rendering, XP provides a Javascript framework. Page components are essentially different JavaScript controllers used by the CMS to perform such rendering.

Page component

A default page component is included in your application. It is located in your project under: src/main/resources/site/pages/hello/.

It consists of two files, a controller and a view:

The Controller: src/main/resources/site/pages/hello/hello.js
var portal = require('/lib/xp/portal'); // Import the portal library
var thymeleaf = require('/lib/thymeleaf'); // Import the Thymeleaf library

// Handle the GET request
exports.get = function(req) {

    // Prepare the model that will be passed to the view
    var model = {
        displayName: portal.getContent().displayName

    // Specify the template file to use
    var view = resolve('hello.html');

    // Return the merged view and model in the response object
    return {
        body: thymeleaf.render(view, model)
And a View: src/main/resources/site/pages/hello/hello.html
<!DOCTYPE html>
        <title data-th-text="${displayName}">Sample title</title>
        <h1 data-th-text="${displayName} + ' - of views'">Sample header</h1>
The name of the controller JavaScript file must be the same as the component directory.

Setup page

Using Content Studio, we will now map the "hello" page component to a content item:

  1. Select the site content "Hello World" and click btn:[edit]

  2. From the preview panel on the right, choose the "Hello" page controller:


XP also includes a page editor. In addition to choose a controller, the editor also lets you compose pages by drag’n dropping other components into it.

To enable drag’n drop support for a page, we must use regions.

Lets start by creating a new page component, with a single region called main.

  1. Create the folder src/main/resources/site/pages/hello-region/ in your project

  2. Add the controller and template files below:

    var portal = require('/lib/xp/portal'); // Import the portal library
    var thymeleaf = require('/lib/thymeleaf'); // Import the Thymeleaf library
    // Handle the GET request
    exports.get = function(req) {
        // Get the content that is using the page
        var content = portal.getContent();
        // Prepare the model that will be passed to the view
        var model = {
          displayName: portal.getContent().displayName,
        // Specify the view file to use
        var view = resolve('hello-region.html');
        // Return the merged view and model in the response object
        return {
            body: thymeleaf.render(view, model)
    <!DOCTYPE html>
        <title data-th-text="${displayName}">Sample title</title>
        <h1 data-th-text="${displayName} + ' - with regions'">Sample header</h1>
        <div data-portal-region="main">
            <div data-th-if="${mainRegion}" data-th-each="component : ${mainRegion.components}" data-th-remove="tag">
                <div data-portal-component="${component.path}" data-th-remove="tag"></div>
  3. Additionally, we’ll add a descriptor file - specifying the name of the region.

    <page xmlns="urn:enonic:xp:model:1.0">
      <display-name>Hello Regions!</display-name>
      <description>Drag'n drop enabled</description>
        <region name="main"/>
  4. Redeploy your app once again!

    enonic project deploy
  5. Select, and edit your favourite country in the list

  6. Activate the page editor and choose the Hello Region! controller from the list.

Try drag’n dropping components into the region placeholder.


Its time to show some more data from our Country content type.

Rather than making another page, we will now create a "part". Parts are essentially components that can be added to regions.

  1. Create the folder src/main/resources/site/parts/country/ in your project.

  2. Add the part controller and template files below into the folder:

    var portal = require('/lib/xp/portal');
    var thymeleaf = require('/lib/thymeleaf');
    // Handle the GET request
    exports.get = function(req) {
        // Get the country content as a JSON object
        var content = portal.getContent();
        // Prepare the model object with the needed data from the content
        var model = {
            name: content.displayName,
            population: || "Unknown",
            description: || "Missing description",
        // Specify the view file to use
        var view = resolve('country.html');
        // Return the merged view and model in the response object
        return {
            body: thymeleaf.render(view, model)
        <div data-th-if="${population}" data-th-text="'Population: ' + ${population}">5 gazillions</div>
        <div data-th-if="${description}" data-th-text="${description}">Nice city indeed</div>
  3. Build and deploy your app once more

    enonic project deploy
  4. Finally, add the part to the country page created earlier:

Page Templates

With our current approach, we would have to configure a new page for every country added. A more efficient approach is to use page templates. Page templates can be used to render multiple content items of specific content types.

Lets create a page template from the country page we just made:

  1. Edit the country page we produced earlier, select the page and choose "Save as Page template"

  2. A new browser tab will open with the template, rename it and save. Please note the "Supports" field. This declares which content types the template supports.

You should now be able to verify that the template applies to the other country items as well.

Toggle template

Your favorite country still uses a “hardcoded” page - so let’s change it to use the template as well.

  1. Edit your favourite country, open the page editor and open the page inspection tab

  2. Change the "Template" setting from "Custom" to "Automatic"

You can select another template, or customize the presentation of a single content at any time.

Advanced part

Lets try something slightly more advanced.

In this task, we will add a new part that does the following:

  • Lists cities within a country.

  • Uses Google Maps to show each city on the map, and it can be configured.

  • Uses page processors to contribute javascript to the page’s header.

  • Allows the part to be configured

    1. Create a new part src/main/resources/site/parts/city-list.

    2. Add the part controller and template files below:

      var contentLib = require('/lib/xp/content');
      var portal = require('/lib/xp/portal');
      var thymeleaf = require('/lib/thymeleaf');
      // Handle the GET request
      exports.get = function (req) {
          // Get the part configuration for the map
          var config = portal.getComponent().config;
          var zoom = parseInt(config.zoom) || 10;
          var mapType = config.mapType || 'ROADMAP';
          // Get the key from configuration file $XP_HOME/config/<>.cfg
          var googleApiKey = app.config['maps.apiKey'] || "";
          // Script that will be placed on the page
          var googleMaps = '<script src="' + googleApiKey + '"></script>';
          var renderMaps = '';
          var countryPath = portal.getContent()._path;
          var componentPath = portal.getComponent().path;
          // Get all the country's cities
          var result = contentLib.query({
              start: 0,
              count: 100,
              contentTypes: [
         + ':city'
              query: "_path LIKE '/content" + countryPath + "/*'",
              sort: "modifiedTime DESC"
          var cities = [];
          var hits = result.hits;
          if (hits.length > 0) {
              renderMaps += '<script>function initialize() {';
              // Loop through the contents and extract the needed data
              for (var i = 0; i < hits.length; i++) {
                  var city = {};
         = hits[i].displayName;
                  city.location = hits[i].data.location;
                  city.population = hits[i].data.population ? 'Population: ' + hits[i].data.population : null;
                  if (city.location) {
                      city.mapId = componentPath + '/googleMap' + i;
                      renderMaps += `
      var position${i} = new google.maps.LatLng(${city.location});
      var map${i} = new google.maps.Map(document.getElementById("${componentPath}/googleMap${i}"), { center:position${i}, zoom:${zoom}, mapTypeId:google.maps.MapTypeId.${mapType}, scrollwheel: false });
      var marker = new google.maps.Marker({ position:position${i} }); marker.setMap(map${i});`
              renderMaps += '} google.maps.event.addDomListener(window, "load", initialize);</script>';
          // Specify the view file to use
          var view = resolve('city-list.html');
          // Prepare the model object that will be passed to the view file
          var model = {
              cities: cities,
              script: renderMaps
          // Return the response object
          return {
              body: thymeleaf.render(view, model),
              // Places the map javascript into the head of the document
              pageContributions: {
                  headEnd: googleMaps
      <div class="cities" style="min-height:100px;">
          <p data-th-if="${#lists.isEmpty(cities)}">No cities found</p>
          <div data-th-unless="${#lists.isEmpty(cities)}" class="city" data-th-each="city : ${cities}">
              <h3 data-th-text="${}">Utopia</h3>
              <div data-th-if="${city.population}" data-th-text="${city.population}">Alotapeople</div>
              <div data-th-if="${city.mapId}" data-th-text="'Map of ' + ${}" data-th-id="${city.mapId}" style="width:100%;height:300px;">Map shown here when rendered!</div>
              <div data-th-remove="tag" data-th-utext="${script}">Maps script replaces this div</div>
    3. Also add a descriptor file. It includes a form, similar to a content type. The part controller above extracts the form configuration and uses it.

      <part xmlns="urn:enonic:xp:model:1.0">
        <display-name>City list</display-name>
        <description>Show all cities with Google Maps</description>
          <input type="ComboBox" name="mapType">
            <label>Map type</label>
            <occurrences minimum="0" maximum="1"/>
              <option value="ROADMAP">ROADMAP</option>
              <option value="SATELLITE">SATELLITE</option>
              <option value="HYBRID">HYBRID</option>
              <option value="TERRAIN">TERRAIN</option>
          <input type="TextLine" name="zoom">
            <label>Zoom level 1-15</label>
            <occurrences minimum="0" maximum="1"/>
    4. Build and deploy your app once more

      enonic project deploy
    5. Finally, add this part to the "country" template we created earlier. Effectively applying it to all countries.

Thanks to the descriptor file, you may optionally configure the part by using the context menu of the page editor.

Config management

Our app is using the Google Maps API. Heavy usage of Googles services requires a valid API key.

Hardcoding an API key into your app is far from ideal. Luckily, XP provides a simple way to inject configuration, like the API key, without having to change your app.

  1. Start by getting yourself a Google API key

  2. Create a file called <>.cfg i.e. "" with the following content:

    maps.apiKey = <your google api key>
  3. Copy this file into your sandbox' config folder. The sandbox is located in your users home folder at .enonic/sandboxes/<name-of-sandbox>/home/config/

  4. After placing the file in the sandbox, you should now see a line from the sandbox log showing something like this:

    2019-05-05 20:07:39,462 INFO  c.e.x.s.i.config.ConfigInstallerImpl - Loaded config for [<>]

    To verify that the configuration is in use, check the source code for a country page to see the following:

    <script src=">"></script></head>

Go Online

Now that the setup of our "Hello World" site is completed, it’s time to publish.

  1. Select the "Hello World" site in the content browse panel

  2. Select Publish from the toolbar menu. The Publishing Wizard will now appear

  3. Tick the Tree icon to the left and verify that all desired items are included.

  4. click Publish!

When working in Content Studio, you always work in and see the items of the draft branch. When publishing, the included items are merged from draft to the master branch

You can see the published site and the master branch on this url: http://localhost:8080/site/default/master/hello-world.

Well done!

The Enonic team congratulates you on building your first XP site :)

Nice to know

Setting up vhosts

If you want to go live on a production server, you will need to configure a vhost. Vhosts map the internal XP URI to a public facing URL i.e. → /site/default/master/hello-world.

Read more about vhost configuration in the XP docs.

Adding more apps

A site can be created from a single app, but you may also add more. SEO Meta Fields, Disqus comments and Google Analytics are just a few of the many apps that can instantly add new features to your site.

To add an app, install an app from the "Applications" tool in the main menu. Once installed, edit your site and add the application in the apps field.


While developing an app, it can be helpful to see the structure of objects returned by library functions. A way to do this is by creating up a small utility script.

exports.log = function (data) {'Utilities log %s', JSON.stringify(data, null, 4));

Next, call the log function in any controller like the example below and then check the log after refreshing the page.

var util = require('utilities');
var content = portal.getContent();

This function and many more are included in the Util Library