Tutorial: My first site


A step-by-step introduction to building websites with the Enonic JS framework.

 If you are primarily looking to use Enonic’s headless API. Visit the intro tutorial instead.


During this exercise you will:

  • set up a local Enonic developer environment

  • create an Enonic app

  • install and use Content Studio

  • learn about the structure of an Enonic app

  • learn about content types

  • setup pages and parts

  • use page templates

  • manage static assets

  • and then some…​


Install Enonic CLI

The Command Line Interface is an essential tool for developers.

If you have npm on your device, run this command:

npm install -g @enonic/cli

To verify that the CLI has been installed, run enonic -v. This should output the version of your installed CLI.

To see all available options, simply run enonic.

 To upgrade, use enonic latest. If there are new versions you will see instructions on how to upgrade.

Create a sandbox

A sandbox is a local developer instance of our platform - Enonic XP. Create a sandbox by running this command in your terminal:

enonic sandbox create

Give it a name, i.e. myfirstsite, and select the most recent version of Enonic XP from the list that appears.

Start the sandbox with this command:

enonic sandbox start --dev

Select the intro sandbox from the list, and it will boot up in development mode.

 Dev mode will automatically load changes in your code when developing.

Create app

From a new terminal window, run the following command to create the application.

 Use the default options when prompted.
enonic project create -r tutorial-myfirstsite

The command uses the My first site Github repo as a starter (template) for the app.

Project structure

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

build.gradle (1)
build/ (2)
code-samples/ (3)
docs/ (4)
      import/ (5)
      assets/ (6)
        content-types/ (7)
        parts/ (8)
        pages/ (9)
1gradle files are used by the app build system
2Contains output files produced by the build
3Code samples that will be used in this tutorial
4Contains the documentation you are reading now
5Contains sample content that is imported when the app starts
6Location for static assets
7Two pre-defined content types, country and city
8Part components - used to compose pages
9Page components - root component for pages

Building and Deploying

Assuming you did not change the default directory name myproject/ when creating the app - build and deploy the app with the following commands:

cd myproject
enonic project deploy

Select the myfirstsite sandbox from the list, and the app will use this from now on.


Look for a line like this in the sandbox log to verify that the app has started:

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

Install Content Studio

Content Studio is the editorial interface used to create and manage content. It is not a part of the core platform, but as you will see soon, it can easily be installed from Enonic Market.

  1. Open the sandbox admin: http://localhost:8080/admin and click Login without a user. This will bring you to the Dashboard.

  2. Open the Applications app from the top right XP menu → Applications.

    Go to Applications from the XP menu
  3. Install Content Studio: click Install button in the menu bar, scroll down to Content Studio (or use search) in the list of apps that appears and click Install next to it.

    Install the Content Studio app

Sample content

On the Dashboard, a Content Studio widget should now have appeared. It shows that we already have some sample content available.

Install the Content Studio app

A content project called My First Site andthe sample content gets imported when you application was started the first time.

Let’s have a closer look:

  1. Open Content Studio, which is now available in XP menu → Content Studio.

  2. Select the My First Site project when prompted, and you should now see the imported content.

Tree structure showing countries and cities in Content Studio

Content Types

Every content item has a specific a content type.

Enonic XP ships with a set of standard content types such as Shortcut, Folder, Site and a range of Media Types that handle different kind of files.

Additionally, an application may define custom content types. In our case, the content types Country and City already exist in the application.

In Content Studio, the City content type looks something like this:

Form for the city content type

The content type is defined in a file, looking like this:

<content-type xmlns="urn:enonic:xp:model:1.0">
  <description>Place where many people live</description>
    <input type="ImageSelector" name="photo">
      <occurrences minimum="0" maximum="1"/>
    <input type="TextLine" name="slogan">
      <label>City slogan</label>
      <occurrences minimum="0" maximum="1"/>
    <input type="Long" name="population">
      <occurrences minimum="1" maximum="1"/>

Visit these links for more information about Content types and the schema system


At the moment, we have some content, but no preview or rendering. As a headless CMS, Enonic supports use 3rd party front-ends - but Enonic also has an an embedded JavaScript runtime, and a JS framework. This can be used to customize the platform, or, like in our case render pages for a website.

Pages are essentially composed from one or more components. Using the Enonic framework, each component will have a JavaScript controller which is responsible for rendering it.

Page component

The app includes a pre-defined page component. In addition to the controller, this component also uses an html template - aka the view in the Model View Controller (MVC) pattern. They look like this:

 The JavaScript controller file must use the same name as the component directory.
const portal = require('/lib/xp/portal'); // Import the portal library
const 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
    const model = {
        displayName: portal.getContent().displayName

    // Specify the template file to use
    const view = resolve('hello.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} + ' - we made it!'">Sample header</h1>
 The view is plain HTML, but also uses a specific syntax known as the Thymeleaf templating language. The Enonic runtime also supports many other options, such as React, Mustache and Freemarker

Create page

To actually have the hello page component render something, it must be mapped to a content item:

  1. Select the site content Hello World and click edit

  2. From the preview panel on the right, select the Hello page component in the list. Your changes will automatically save, and the page preview will render the result.

Demonstrating the process of setting up a page


Content Studio’s page editor also supports adding more components to a page, using drag’n drop.

To support this, the page component must define one or more regions.

Complete the steps below to add a single region called main to the hello page component.

  1. Start off by adding a component descriptor. The descriptor statically declares 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"/>
  2. Update your controller and view with the code below to support the new region:

    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,
          mainRegion: content.page.regions.main
        // Specify the view file to use
        var view = resolve('hello.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>
        <main 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>
        <footer>My first site - powered by Enonic</footer>  

    Your sandbox should automatially pickup the changes, as we are running in dev mode.

  3. Back in Content Studio, select, and edit one of the contry content items.

  4. Activate the page editor (from the top right monitor icon) and select the Hello component.

    Setting up a page with a region

You now have a page with a region. Try adding a text component to the region. It is available from the right hand side as drag-n-drop, or by using the right click menu.


You’re now ready to present some more information from the Country content type.

Rather than making another page component, let’s 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 view files below to 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: content.data.population || "Unknown",
            description: content.data.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. When done, the part can be added to the page by drag’n dropping a part component from the right-hand-side, then select the Country component from the list.

    Inserting the new country part

Configurable part

Let’s try something slightly more advanced.

In this task, you’ll add a new part that does the following:

  • Lists cities within a country.

  • Supports configuation for image cropping format

  • Includes a real-time cropped image of each city

Complete the steps below to try it out:

  1. Create the City list part by adding the controller and view files:

    var contentLib = require('/lib/xp/content');
    var portal = require('/lib/xp/portal');
    var thymeleaf = require('/lib/thymeleaf');
    // Specify the view file to use
    const view = resolve('city-list.html');
    // Handle the GET request
    exports.get = function (req) {
        // Get the part configuration for the map
        const config = portal.getComponent().config;
        const countryPath = portal.getContent()._path;
        const componentPath = portal.getComponent().path;
        // Get all child item cities's  
        const result = contentLib.query({
            start: 0,
            count: 100,
            contentTypes: [
                app.name + ':city'
            query: "_path LIKE '/content" + countryPath + "/*'",
            sort: "modifiedTime DESC"
        const cities = [];
        // Create a crop based on configuration
        const crop = config.crop || 'ORIGINAL';
        let scale; 
        if (crop == 'SQUARE') { scale = 'square(1080)'}
        else if (crop == 'WIDESCREEN') { scale = 'block(1080,300)'}
        else { scale = 'width(1080)'}
        if (result.hits.length > 0) {
            // Loop through the contents and extract the needed data
                const city = {};
                city.name = item.displayName;
                city.photo = item.data.photo ? portal.imageUrl({
                    id: item.data.photo,
                    scale: scale,
                }) : null;
                const population = item.data.population ? 'population: ' + item.data.population : '';
                city.caption = [item.data.slogan, population].filter(Boolean).join(' - ');
        // Specify the view file to use
        const model = {cities};
        log.info('Model object %s', JSON.stringify(model, null, 4));
        // Return the response object
        return {
            body: thymeleaf.render(view, model)
    <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="${city.name}">Utopia</h3>
                <img data-th-if="${city.photo}" data-th-src="${city.photo}"/>
                <figcaption data-th-if="${city.caption}" data-th-text="${city.caption}">Best city every</figcaption>
  2. This time, also add a component descriptor. This descriptor includes a form (similar to a content type). The form allow editors to configure the part, once it is placed on the page.

    <part xmlns="urn:enonic:xp:model:1.0">
      <display-name>City list</display-name>
      <description>Show all cities in a country</description>
        <input type="ComboBox" name="crop">
          <label>Image crop</label>
          <occurrences minimum="0" maximum="1"/>
            <option value="ORIGINAL">Original</option>
            <option value="WIDESCREEN">Widescreen</option>
            <option value="SQUARE">Square</option>
  3. Add the new part to the page you created earlier.

  4. Finally, configure the part by selecting it and changing the settings in the right hand panel.

     The controller is also configured to log model passed to the view, check sandbox log after visiting a country page to see what is happening.
Add city list to page and configure it to show widescreen images
 The images are scaled and cropped in real-time by the Image service. The part controller uses the imageUrl() function to create a link to the desired image.

Page Templates

With our current approach, you would have to configure a new page for every country in the list. A slightly more efficient approach is to use page templates.

By creating a page template, and map it to a specific content types, all items of this type - i.e. countries - will be rendered by the page template.

Let’s create a page template from the country page you just made:

  1. Edit the country page made in the previous step, in the visual editor - select the page and choose "Save as Page template" from the right hand component panel.

  2. A new browser tab will open with the new template. Rename it and save.

     The Supports field defines which content type(s) the template will apply to.
    Creating a page template from the country page
 Try clicking the other countries to verify that the template actually works as intended.

Toggle template

Your originally edited country still uses the custom page you created - you can change it to use the new page template as well.

  1. Edit the initial country, open the page editor and select the page.

  2. From the right hand component view, change the "Template" setting from "Custom" to "Automatic"

 For each content item, you can select a specific template, or customize the presentation at any time.

Static assets

So far, the site is made up of dynamic HTML and images. Let’s have a look at how to handle static assets like graphics, CCS and JavaScript files.

Enonic supports an "out-of-the-box" solution for this through the Asset service. By simply placing files in the src/resources/assets folder - they are instantly available on a specific URL.

Your application already includes the sample asset assets/mvp.css. Let’s put it to work by adding the CSS to the page component:

Update your page component view with the following content:

<!DOCTYPE html>
<html color-mode="user">
    <title data-th-text="${displayName}">Sample title</title>
    <link rel="stylesheet" data-th-href="${portal.assetUrl({'_path=mvp.css'})}" href="../assets/mvp.css" type="text/css" media="all"/>    
      <h1 data-th-text="${displayName} + ' - with regions'">Sample header</h1>
    <main 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>
    <footer>My first site - powered by Enonic</footer>  

There are only two changes in this file, one line that includes the css file, and a small attribute on the html element to activate dark mode support.

If you’re using dark mode, the result will look something like this:

Countries page in dark mode after adding the css

Perfect cache

In production, every asset will include cache headers that allow CDNs and browsers to cache it - and never need to download it again.

How does it work? The generated asset URL will contain a prefix based on a signature of the files in the app. The signature will change with every new version of the app. The portal.assetUrl() function uses this signature when generating the URL, ensuring that clients always get the most recent version.

 When running in dev mode (like you do now), cache headers are deactivated

Lib static

If you’re using advanced build-tools like Webpack, assets may be chunked and served incrementally. Changing the URL to all assets for every re-deploy is no longer optimal.

For this purpose, you can take full control over asset serving by adding Lib static to your app.

We will not go into details in this tutorial, but lib-static is pre-bundled with selected app starters like Webpack Starter and React4XP starter.

Go Online

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

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

  2. Select Publish Tree from the top right action menu. The Publishing Wizard will now appear

  3. Several In progress items are blocking publishing - click Mark as ready to fix this.

  4. click Publish!

Demonstrating how to publish all items in the project

What just happened? In Content Studio, you work in the draft branch. When publishing, the selected 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/myfirstsite/master/hello-world.

Well done!

🎉 Congratulations on building your first Enonic site - The Enonic team.


Below are some nice to know features and tricks:

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. mydomain.com → /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, Google Tag Manager and Siteimprove 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 XP menu → Applications app. Once installed, edit your site and add the application to the list of linked apps.


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) {
  log.info('Utilities log %s', JSON.stringify(data, null, 4));

Then, 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