This covers the documentation on how to create content elements in Contao 4.6
and up. In previous Contao version, Content elements must extend from \Contao\ContentElement
and then be registered via the $GLOBAL['TL_CTE']
array.
In Contao, Content Elements are the fundamental content blocks. In its simplest form it is a fragment controller which receives data in form of a content model and returns a response.
These elements are implemented as so called fragment controllers which Contao then renders into the main content, using their defined renderer. See the caching documentation for more information.
Creating a content element is very similar to creating front end modules.
To create a new content element, the following things must be defined and implemented:
Fragment Controller
The actual implementation of the content element is done via a class that extends
from AbstractContentElementController
of the Contao core.
Service Tag
To identify the controller as a Contao content element, the service must be tagged
with service tag contao.content_element
.
Type
The type of a content element is a specific string which is used to identify
the element’s template and DCA palette. The type
can be set in the service
tag. If omitted the type will be automatically generated by converting the
class name of the controller from pascal case to snake case and removing a possible
Controller
postfix.
Category
All content elements are categorised within the type dropdown of the content element’s
palette. A category
must be defined in the service tag for each content element.
Template
The template name follows the naming convention mentioned beforehand. It prepends
the type of the element with the prefix ce_
.
Usually a content element is based on a specific palette in the tl_content
DCA configuration.
// contao/dca/tl_content.php
$GLOBALS['TL_DCA']['tl_content']['palettes']['example_element'] =
'{type_legend},type;{text_legend},text'
;
This very simple palette enables a back end user to fill the (pre-existing) field
text
via the create and edit view of this content element.
The controller for this content element could look like this:
// src/Controller/ContentElement/ExampleElementController.php
namespace App\Controller\ContentElement;
use Contao\ContentModel;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[AsContentElement(category: 'texts')]
class ExampleElementController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
$template->text = $model->text;
return $template->getResponse();
}
}
In this example the service tag was implemented via PHP attributes (see the different registration types below).
Using the naming convention for templates mentioned above, the final template name
for this content element will be ce_example_element
:
<!-- contao/templates/ce_example_element.html5 -->
<div class="my-content-element">
<?= $this->text; ?>
</div>
A template instance of this template will automatically be generated and passed to the controller’s main method. The controller returns the parsed template as a response.
As mentioned previously a content element is registered by registering a controller as a service and tagging it with the
contao.content_element
service tag. The service tag supports the following options:
Option | Type | Description |
---|---|---|
name | string | Must be contao.content_element . |
type | string | Optional: The type mentioned in Type can be customized. |
category | string | Defines in which option group this content element will be placed in the content element selector. |
template | string | Optional: Override the generated template name. |
renderer | string | Optional: The renderer can be changed to inline or esi . Defaults to forward . See Caching Fragments for more details. |
method | string | Optional: Which method should be invoked on the controller. |
nestedFragments | bool /array | Optional: Allows nested fragments for this content element. |
Applying the service tag can either be done via PHP attributes, annotations or via the YAML configuration.
since 4.13 A content element can be registered using the AsContentElement
PHP attribute.
// src/Controller/ContentElement/ExampleController.php
namespace App\Controller\ContentElement;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
use Contao\ContentModel;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[AsContentElement(category: 'texts')]
class ExampleController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
The above example only defines the category
attribute (which is actually optional for the PHP attribute - the default
category is miscellaneous
). If you wish you can also define the other options of the service tag:
// src/Controller/ContentElement/ExampleController.php
namespace App\Controller\ContentElement;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
use Contao\ContentModel;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[AsContentElement('example', 'texts', 'ce_example', '__invoke', 'forward')]
class ExampleController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
However, it is recommended to only define what you need and otherwise leave the defaults.
since 4.8 A content element can be registered using the ContentElement
annotation. The annotation can be used on the class of the content element,
if the class is invokable (has an __invoke
method) or extends from the AbstractContentElementController
. Otherwise the annotation can be
used on the method that will deliver the response.
// src/Controller/ContentElement/ExampleController.php
namespace App\Controller\ContentElement;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\ServiceAnnotation\ContentElement;
use Contao\ContentModel;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @ContentElement(category="texts")
*/
class ExampleController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
The above example only defines the mandatory category
attribute. If you wish you can also define the other options of
the service tag:
// src/Controller/ContentElement/ExampleController.php
namespace App\Controller\ContentElement;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\ServiceAnnotation\ContentElement;
use Contao\ContentModel;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @ContentElement("example", "texts", "ce_example", "__invoke", "forward")
*/
class ExampleController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
However, it is recommended to only define what you need and otherwise leave the defaults.
since 4.8 A content element can be registered using the contao.content_element
service tag.
# config/services.yaml
services:
App\Controller\ContentElement\ExampleController:
tags:
-
name: contao.content_element
category: texts
// src/Controller/ContentElement/ExampleController.php
namespace App\Controller\ContentElement;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\ContentModel;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class ExampleController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
The above example only defines the mandatory category
attribute. If you wish you can also define the other options of
the service tag:
# config/services.yaml
services:
App\Controller\ContentElement\ExampleController:
tags:
-
name: contao.content_element
category: texts
template: ce_example
renderer: forward
method: __invoke
However, it is recommended to only define what you need and otherwise leave the defaults.
You can also use class constants within attributes and annotations. This can be helpful to make the module’s type a reusable reference:
#[AsContentElement(ExampleController::TYPE)]
class ExampleController extends AbstractContentElementController
{
public const TYPE = 'my_element';
}
// contao/dca/tl_content.php
use App\Controller\ContentElement\ExampleController;
$GLOBALS['TL_DCA']['tl_content']['palettes'][ExampleController::TYPE] =
'{type_legend},type;{text_legend},text'
;
// contao/languages/en/default.php
use App\Controller\ContentElement\ExampleController;
$GLOBALS['TL_LANG']['CTE'][ExampleController::TYPE] = [
'My Example Element',
'A Content Element for testing purposes.',
];
In order to have a nice label in the back end, we also need to add a translation for our content element - otherwise it will only be named my_content_element. The translation needs to be set as follows:
// contao/languages/en/default.php
$GLOBALS['TL_LANG']['CTE']['my_content_element'] = [
'My Content Element',
'A Content Element for testing purposes.',
];
If you used a custom category for your content element, its label can also be translated there.
This feature is available in Contao 4.9.10 and later.
If your fragment extends from AbstractContentElementController
(or just AbstractFragmentController
)
you can use $this->getPageModel()
in order to receive the \Contao\PageModel
object of the currently rendered page of Contao’s site structure.
// src/Controller/ContentElement/MyContentElementController.php
namespace App\Controller\ContentElement;
use Contao\ContentModel;
use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement;
use Contao\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[AsContentElement(category: 'texts')]
class MyContentElementController extends AbstractContentElementController
{
protected function getResponse(Template $template, ContentModel $model, Request $request): Response
{
$page = $this->getPageModel();
// Get some information about the current page
$template->rootTitle = $page->rootPageTitle ?: $page->rootTitle;
return $template->getResponse();
}
}
In Contao there are special content elements called “wrappers” which you insert before and after one or a group of content elements. These
wrappers affect the back end view, indicating that all elements contained within the two wrappers are descendants of the parent wrapper. The
wrapper content elements typically consist of a start
and stop
element, though there are also wrappers of type single
and separator
.
The start
element typically opens a specific HTML tag, while the stop
element will close it again.
In order to define that a content element is a wrapper of a specific type, it needs to be registered in the $GLOBALS['TL_WRAPPERS']
array
in your contao/config/config.php
. The $GLOBALS['TL_WRAPPERS']
array holds the element types for each type of wrapper. For example:
// contao/config.php
$GLOBALS['TL_WRAPPERS']['start'][] = 'my_start_element';
$GLOBALS['TL_WRAPPERS']['stop'][] = 'my_stop_element';
This feature is available in Contao 5.3 and later.
An alternative approach to the aforementioned wrapper elements are so called “nested fragments”. These allow you to nest other content elements within a parent content element for which nested fragments are enabled. The following screenshot shows the Element group content element of the Contao Core:
Just as with page articles, news archives, etc. this content element allows you to edit its children via which then works the same way as within the article of a page.
Allowing nested fragments for your content element works via the nestedFragments
option in the service tag:
#[AsContentElement(nestedFragments: true)]
class ExampleController extends AbstractContentElementController
{
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
/**
* @ContentElement(category="miscellaneous", nestedFragments=true)
*/
class ExampleController extends AbstractContentElementController
{
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
services:
App\Controller\ContentElement\ExampleController:
tags:
-
name: contao.content_element
nestedFragments: true
With nested fragments it is also possible to restrict the fragment’s children to specific content element types. Say you
want to implement a specific slider content element which should only allow images and videos as its children. Then
instead of defining true
for the tag’s nestedFragments
option you can instead pass an additional option called
allowedTypes
:
#[AsContentElement(nestedFragments: ['allowedTypes' => ['image', 'video']])]
class ExampleController extends AbstractContentElementController
{
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
/**
* @ContentElement(category="miscellaneous", nestedFragments={"allowedTypes" = {"image", "video"}})
*/
class ExampleController extends AbstractContentElementController
{
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
return $template->getResponse();
}
}
services:
App\Controller\ContentElement\ExampleController:
tags:
-
name: contao.content_element
nestedFragments:
allowedTypes: ['image', 'video']
Now all you have to do is render these fragments in your template. The AbstractContentElementController
from which
your content element likely extends from automatically passes the available nested fragments to your template within
the nested_fragments
template variable. This will be a collection of ContentElementReference
objects which you can
render via the content_element()
Twig function:
{# templates/content_element/example.html.twig #}
{% extends "@Contao/content_element/_base.html.twig" %}
{% block content %}
{% for fragment in nested_fragments %}
{{ content_element(fragment) }}
{% endfor %}
{% endblock %}