Neos Workshop Part 3 - Creating Your Own Content Elements

Thanks to the powerful ContentRepository and the tools that Neos CMS provides us with—such as Eel and FlowQuery—there are no limits to the design of our own content elements.

Ich liebe es wenn ein Plan funktioniert!

Daniel Lienert
Daniel ist immer auf der Suche nach technologisch innovativen aber dennoch nachhaltig stablilen Lösungen für unsere Kunden.
Reading duration: approx. 7 Minutes

Welcome to the third part of our Neos tutorial. In the first part, we showed how to easily set up a local development environment for Neos using Vagrant. In the second part , we created a site package for our own site. In this part, we’ll focus on the structure and configuration of custom content types.

Well-Structured Content - The Neos Content Repository

To define your own content types, it’s important to first understand the data structure behind Neos, so this section begins with a bit of (fascinating) theory.

All content in Neos is stored as a node in a tree structure.  You can think of this structure as similar to a computer’s file system. Each node has a unique name and a type—similar to the type of a file in the file system—which defines the node’s properties.

The node’s data is stored in its properties. Of course, each node can have additional child nodes. The root of the tree is a special node called “Site.”

This is best illustrated using a simple website with a hierarchical menu structure. Each page is represented by a node of the “Document” type—subpages on the second menu level are child nodes of the first level.

The page’s content, such as text or image elements, are also nodes that are attached as child nodes to the respective page. Content—such as a two-column container—can in turn have its own child nodes. Content can thus be nested to any depth. Additionally, each node can have any number of attributes, referred to here as “properties.”

The image shows such a tree with a “Home” page and a “Wines from Around the World” page. The pages can contain simple standard content such as text or images, or a special content element, which in our example represents a wine. We will create such an element in this part of the workshop.

Diagram of the Tree Structure (Neos CMS Content Repository Tree)

Defining a New Content Type

Every new content type begins with the definition of its own NodeType. The data structures of the NodeType are defined in the YAML markup language. Meta-information such as editing options, help texts, and relationships to other types is also defined here. NodeType definitions can be inherited as needed, and multiple inheritance is also possible. This makes it possible to define simple, reusable data types that can then be combined with their properties to form larger definitions.

Neos already defines a number of abstract base types that specify the basic behavior of a data element.

  • Neos.Neos:Node: is the foundation of all other defined types.
  • Neos.Neos:Document: defines the basis for all nodes that behave like pages.
  • All content on a page inherits from the types Neos.Neos:Content or Neos.Neos:ContentCollection. While a ContentCollection is used to structure content—for example, for a multi-column container—Content is the basis of any simple content, such as a heading or an image.

With this knowledge, we can set out to create our own simple content type: tailored to the page, this will be a wine presentation featuring a title, description, image, and grape variety. All files we create in the following will be stored in the Site Package directory. The second part of the workshop illustrates the basic structure of the Site Package. 

It has proven useful to spread the NodeType definition across multiple files to maintain a clearer overview. Therefore, we’ll place our definition in a new file, `NodeTypes.Wine.yaml`, in the ` Configuration` directory—it’s important that the filename begins with “NodeTypes.” so that Neos can automatically include it. Structures in YAML are defined solely through indentation with two spaces.

We’ll start with a simple definition of an element with two properties, Title and Description, as shown in the listing. The first line specifies the name of the NodeType. The `superTypes` in the line below specify which other NodeTypes it should inherit from. Since this is intended to be a content element, it inherits from the base type `ContentElement`.

Properties that describe the user interface and controls are always located under the “ui” key. The “Label” property gives the element a meaningful name. A recognizable icon helps the editor make a selection—any icon from the FontAwesome icon set can be used here.

'WL.WeinLaden:Wine':
  superTypes:
    'Neos.Neos:Content': true
  ui:
    label: Wein
    icon: icon-glass
    inspector:
      groups:
        wine:
          label: Wein
  properties:
    title:
      type: string
      ui:
        label: 'Name'
        inlineEditable: true
        aloha:
          placeholder: 'Titel des Weins'
    description:
      type: string
      ui:
        label: 'Beschreibung'
        inlineEditable: true
        aloha:
          placeholder: 'Beschreibung des Weins'

Configuration/NodeTypes.Wine.yaml

The "Inspector" section contains properties that apply to the controls in the right-hand sidebar. The controls can be organized into groups. A group labeled "Wine" is created here.

Next, the properties of the node are defined. Content of the "string" type can be edited directly "inline" on the page, which is specified using the property inlineEditable:true. "Aloha" is the name of the editor used for this purpose, which can be configured here. The placeholder text defined here is displayed if no custom text has been specified yet, thereby assisting the editor in editing the page.

Now let's move on to the display: By convention, Neos expects the template for a NodeType to be located in the directory Resources/Private/Templates/NodeTypes/<nodeTypeName>.html —in our case, "Wine." 

{namespace neos=Neos\Neos\ViewHelpers}
<div {attributes -> f:format.raw()}>
	<neos:contentElement.editable property="title" tag="h3" />
	<neos:contentElement.editable property="description" tag="p" />
</div>

In the first line, we import the ViewHelpers from the neos namespace, which we need to display the editable elements. Using the "contentElement.editable" ViewHelper, we create editable HTML elements for the previously defined properties. The heading is rendered using the H3 tag, and the text is rendered as a paragraph.

And there you have it—your first custom content element, which can now be integrated into the page.


Screenshot of the integration of a new content element
A screenshot shows how headings and titles can be edited inline

Let's move on to the second round—in this one, we want to expand the "Wine" content element to include the grape variety and an image.

The grape variety should not be entered as free text, but rather selected from a list of predefined varieties. Since this cannot be done directly on the page, the Inspector in the right-hand sidebar is configured starting at line 9.
The new field is added to the "wine" group. The editor should be of the “SelectBoxEditor” type, which displays a drop-down list for the values configured under “values.”

Next, the image. This is specified using the “ImageInterface” type. Specifying this type also ensures that an editor is available for uploading or selecting an image from the media module.

This editor can also be customized to meet specific requirements. In our case, we want all images to be displayed with the same aspect ratio. If an image with a different aspect ratio is uploaded, it should be automatically cropped.

The editor can individually determine which part of the image is displayed. To do this, we define a crop under `editorOptions` with a fixed aspect ratio of 3 to 2.

    grape:
      type: string
      ui:
        label: Rebsorte
        help:
          message: Hier bitte die Rebsorte wählen
        reloadIfChanged: true
        inspector:
          group: wine
          editor: Neos.Neos/Inspector/Editors/SelectBoxEditor
          editorOptions:
            values:
              Merlot:
                label: Merlot
              Syrah:
                label: Syrah
              Chardonnay:
                label: Chardonnay
    image:
      type: Neos\Media\Domain\Model\ImageInterface
      ui:
        label: Bild
        reloadIfChanged: true
        inspector:
          group: wine
          editorOptions:
            crop:
              aspectRatio:
                locked:
                  width: 3
                  height: 2

In this case, the easiest way to render the image element in the template is to use the Neos Image partial. To use this, you must set the PartialRootPath to the correct directory via Fusion. To do this, create a new file named Wine.fusion under Resources/Private/Fusion/NodeTypes/ and insert the following lines:

prototype(WL.WeinLaden:Wine) < prototype(Neos.Neos:Content) {
    partialRootPath = 'resource://Neos.NodeTypes/Private/Templates/NodeTypes/Partials'
}

Resources/Private/Fusion/NodeTypes/ Wine.fusion

This defines a new Fusion prototype for our content element, which is used for rendering. By inheriting from the base type "Content," we adopt some basic settings for displaying a content element, which we can now override or extend for our own element.

We include this file (as well as all other files in the NodeTypes directory) with the following line at the end of theRoot.fusion file:

include: NodeTypes/*

The template for our content element can then be expanded to include the additional data. To display the image, we now use the Image Partial. This requires, as a minimum, the Image object and text for the alt tag.

{namespace neos=Neos\Neos\ViewHelpers}
<div {attributes -> f:format.raw()}>
    <div class="thumbnail">
        <f:render partial="Image" arguments="{image:node.properties.image, alt:node.properties.title, maximumWidth:1600}" />
        <div class="caption">
            <neos:contentElement.editable property="title" tag="h3" />
            <neos:contentElement.editable property="description" tag="p" />
            <div class="row">
                <div class="col-md-6">Rebsorte:</div><div class="col-md-6"><b>{node.properties.grape}</b></div>
            </div>
        </div>
    </div>
</div>

Resources/Private/Templates/NodeTypes/Wine.html

The images in our content element are initially displayed at their full original resolution. However, you can use the additional arguments `maximumWidth ` and `maximumHeight ` to limit the resolution of the `image` partial to a size that makes sense for our use case.

Customize Existing NodeTypes - Multi-column containers with Bootstrap markup

Screenshot of a multi-column container

Our new content element would look great in a column of a multi-column container. Neos already includes containers for 2, 3, and 4 columns by default. However, to ensure they are rendered in a way that matches the markup and CSS classes used by the Bootstrap framework implemented here, a few minor adjustments need to be made.
These adjustments are also a great way to learn a little more about the functionality of Eel and FlowQuery.

We override the NodeType definition for the multi-column container that comes with Neos in our own file, NodeTypes.BootstrapAdjustments.yaml.

In the options for the column layout, remove the existing entries using a tilde (~) and replace them with the settings for the Bootstrap 12-column grid.

In the next step, we’ll also override the Fusion prototypes for the multi-column container and the individual columns to render the CSS classes for the Bootstrap grid. 

The "attributes" array is present in all content types that inheritfrom the Neos.Neos:Contentdefinition. By convention, "attributes" is rendered in the outer HTML tag of the content element.

In the Neos.NodeTypes:MultiColumn prototype, we simply set the CSS class “row” here. In the Neos.NodeTypes:MultiColumnItem node type, things get a bit more interesting. Each column requires a class that defines its width. Based on the selected layout value “9-3,” the CSS class “col-md-9” should be generated for the first column and “col-md-3” for the second column. To do this, we split the layout value using the Eel helper ` String.split() ` at the “-” and use the value with the corresponding column index for each column.

'Neos.NodeTypes:TwoColumn':
  properties:
    'layout':
      defaultValue: '9-3'
      ui:
        inspector:
          editorOptions:
            values:
              '50-50': ~
              '75-25': ~
              '25-75': ~
              '66-33': ~
              '33-66': ~
              '9-3':
                label: '75% / 25%'
              '6-6':
                label: '50% / 50%'
              '3-9':
                label: '25% / 75%'

'Neos.NodeTypes:ThreeColumn':
  properties:
    layout:
      defaultValue: '4-4-4'
      ui:
        reloadIfChanged: true
        label: ''
        inspector:
          editorOptions:
            values:
              '33-33-33': ~
              '50-25-25': ~
              '25-50-25': ~
              '25-25-50': ~
              '4-4-4':
                label: '33% / 34% / 33%'

Configuration/NodeTypes.BootstrapAdjustments.yaml

prototype(Neos.NodeTypes:MultiColumn) {
    attributes.class = 'row'
    columns.iterationName = 'multiColumnIteration'
}

prototype(Neos.NodeTypes:MultiColumnItem) {
    attributes.class = ${'col-md-' + String.split(q(node).parent().property('layout'), '-')[multiColumnIteration.index]}
}

Fusion/NodeTypes/BootstrapAdjustments.fusion

In this part of the workshop, we saw how to define custom NodeTypes in YAML and render them using Fusion and Fluid, as well as how to easily extend and customize existing NodeTypes. The complete state of the site package can be viewed in the workshop’s GitHub repository .

Share:

More articles

Wer nichts wagt, kann auch nichts gewinnen!
Marco Schiffmann, Digital Consultant at punkt.de
Working at punkt.de