Parts - flexible components for your pages


Parts are the visual building blocks to make pages more interesting.

The country part

It’s about time we show some details from the Country content type.

Rather than making another page component, lets use parts. Like the Text component, parts can be added to regions.

A descriptor, controller and view have already been added to the src/main/resources/site/parts/ folder:

<part xmlns="urn:enonic:xp:model:1.0">
  <description>Show simple details of the country</description>
import { Content } from '@enonic-types/core';
import type { Response } from '/index.d';

// @ts-expect-error no-types
import {render} from '/lib/thymeleaf';
import {getContent} from '/lib/xp/portal';

type ContentCountry = { description?: string, wikipedia?: string }

// Specify the view file to use
const VIEW = resolve('./country.html');

// Handle the GET request
export function get(): Response {

    // Get the country content as a JSON object
    const content = getContent<Content<ContentCountry>>();

    // Prepare the model object with the needed data from the content
    const model = {
        name: content.displayName,
        description: || "Missing description",

    // Prepare the response object
    const response: Response = {
		body: render(VIEW, model)

    return response;
        <li data-th-if="${description}" data-th-text="${description}">Nice city indeed</li>
        <li data-th-if="${wikipedia}">
            <a data-th-href="${wikipedia}">Wikipedia</a>

Add the part to the country page by drag’n’dropping Part from the right-hand-side. Then select the Country component from the list.

The resulting page should look something like this:

Result of inserting the country part on the columbia country content item
You may also use the page form input (visible on the left hand , or floating above the page editor) to manage the components on your page.

Configurable part

Let’s try something slightly more advanced. The City list part will:

  • list cities within a country,

  • show an image of each city,

  • let you choose the image cropping format.

The part already exists in your app via the following files:

<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>
import { Content, PartComponent } from '@enonic-types/core';
import type { Response } from '/index.d';

// @ts-expect-error no-types
import {render} from '/lib/thymeleaf';
import {query} from '/lib/xp/content';
import {getComponent, getContent, imageUrl, ImageUrlParams} from '/lib/xp/portal';

type PartDescriptor = "company.starter.myfirstsite:city-list"
type PartConfig = { crop: 'ORGINAL' | 'WIDESCREEN' | 'SQUARE' }
type ContentCity = { photo?: string, slogan?: string, population: string }

// Specify the view file to use
const VIEW = resolve('./city-list.html');

// Handle the GET request
export function get(): Response {

    // Get the part configuration for the map
    const config = getComponent<PartComponent<PartDescriptor, PartConfig>>().config;
    const countryPath = getContent()._path;

    // Get all child item cities's  
    const result = query<Content<ContentCity>>({
        start: 0,
        count: 100,
        contentTypes: [ + ':city'],
        query: "_path LIKE '/content" + countryPath + "/*'",
        sort: "modifiedTime DESC"

    // Create a crop based on configuration
    const scale = getImageScale(config.crop)

    // Map results to cities
    const cities = (result.hits || []).map(item=> ({
        name: item.displayName,
        photo: && imageUrl({ id:, scale: scale }),
        caption: getCitySlogan(,

    // Set the model object
    const model = {cities};

    // Server log to inspect the model object'Model object %s', JSON.stringify(model, null, 4));

    // Prepare the response object
    const response: Response = {
		body: render(VIEW, model)

    return response;

function getImageScale(crop: string = 'ORIGINAL'): ImageUrlParams['scale'] {
    if (crop == 'SQUARE') { 
        return 'square(1080)';
    if (crop == 'WIDESCREEN') { 
        return 'block(1080,300)';

    return 'width(1080)';

function getCitySlogan(slogan: string, population?: string): string {
    return [slogan, population ? 'population: ' + population : '']
        .join(' - ');
<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>
            <img data-th-if="${}" data-th-src="${}"/>
            <figcaption data-th-if="${city.caption}" data-th-text="${city.caption}">Best city every</figcaption>
  1. Add the city list part to the same page as before, and verify that it is working.

  2. Configure the part by changing the settings in the right hand panel, e.g. choose widescreen in the "Image crop" dropdown. Click Apply button to see the result.

Result of adding city list part to page and configure it to show widescreen images

Image service

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, with desired sizes and cropping.


The controller above also outputs the model object to the log. Check your sandbox log after visiting a country page to see what is going on.


You’ve been introduced to parts, a powerful concept that will enable you to build customizable visual blocks for your pages.

To learn more about parts, visit the parts documentation.

In the next chapter you’ll learn how to reuse pages through page templates.