Blog » How to create custom tags in eZ Publish 5

How to create custom tags in eZ Publish 5

By Ernesto Buenrostro  | March 10, 2016  |  eZ Publish development tips

Custom tags are very useful for adding custom functionality to rich text fields beyond simple formatting and embeds. Here is a walkthrough of how to implement custom tags in eZ Publish 5.x compared to an eZ Publish 4.x / legacy install.

With the eZ Publish 5.x "new stack" the way to turn editorial data in custom tags into front-end HTML is to use XSLT. Let's start with a simple example: embedding YouTube videos.

Currently both eZ Publish kernels (legacy and new stack) are involved; we need the legacy kernel to make our new YouTube video custom tag available in the Administration Interface and the new stack for the front-end display.

In the legacy stack we need to edit overrides of content.ini and ezoe_attributes.ini files to let eZ Publish know what information (name and fields) needs to be stored. We'll use a simple use case, where the editor just needs to paste in the video ID and there will be a fixed height and width.

In an override of content.ini.append.php, we define the custom tag as follows:

CustomTagsDescription[youtube_video]=YouTube Video
ClassDescription[video_id]=Video ID

In an override of ezoe_attributes.ini.append.php, we configure the video ID field:

Name=YouTube video ID
Title=YouTube video ID

We should then have the option to use this new custom tag in a rich text / XML block field:

Now let's get to the front-end display of the YouTube video. In eZ Publish legacy, we would use a template that looks like this:

<iframe type="text/html" width="720" height="405" frameborder="0" allowfullscreen="" src="{$video_id}"></iframe>

In the new stack, we have to first define the path to our custom XSL file in an ezpublish.yml file (loaded in a bundle or in the main configuration folder).

                        - { path: %kernel.root_dir%/../src/test/BaseBundle/Resources/custom_tags.xsl, priority: 10 }

Presuming our bundle is called "test", we then create src/test/BaseBundle/Resources/custom_tags.xml with the following code:

<?xml version="1.0" encoding="UTF-8"?>
    exclude-result-prefixes="xhtml custom image">
    <xsl:output method="html" indent="yes" encoding="UTF-8"/>
    <xsl:template match="custom[@name='youtube_video']">
        <iframe type="text/html" width="720" height="405" frameborder="0" allowfullscreen="">
            <xsl:attribute name="src">
                <xsl:value-of select="concat('', @custom:video_id)"/>

With this configured, the custom tag will automatically be rendered whenever it is used in a rich text field, such as an article body:

{{ ez_render_field( content, 'body' ) }}

The HTML output for the custom tag will thus look like this:

<iframe type="text/html" width="720" height="405" frameborder="0" allowfullscreen="" src=""></iframe>
Embed YouTube video

Getting the ID of the host article

Sometimes XSL is not enough. Let's suppose the custom tag needs to know the ID of the article in which it is used. One example of this is if we want to display a list of related articles within the article, to provide a "more like this" widget based on the current article. To keep the example simple we'll just grab 5 other articles of the same content type, although there are much more sophisticated ways to do this.

A custom tag is used so that the editor can determine the placement of this related article list within the body of an article. In eZ Publish legacy, we had direct access to the content node by using the $#node variable. In the new stack, we need to implement a filter (called xml_convert) to pass the content object containing the custom tag. This filter will get the necessary information before passing it to the default eZ Publish Twig function xmltext_to_html5 that renders a rich text field. The template code will look something like this:

{{ ez_field_value(content, 'body').xml|xml_convert(|xmltext_to_html5 }}

We still need to edit an override of content.ini.append.php to make the custom tag available to editors:

CustomTagsDescription[more_like_this]=More like this

Next, let's create a new Twig extension in our bundle at src/Test/BaseBundle/Twig/TestTwigExtension.php. We start by registering some new services:

        class: Test\BaseBundle\Twig\TestTwigExtension
        arguments: [@service_container, @ezpublish.templating.global_helper]
            - { name: twig.extension }
        class: Test\BaseBundle\Services\XMLConverter
        arguments: [@service_container, @ezpublish.templating.global_helper]

Then in TestTwigExtension.php we define our Twig filter xml_convert that simply calls our pre-converter:

namespace Test\BaseBundle\Twig;
use \Symfony\Component\DependencyInjection\Container;
 * Collection of Twig helpers
class TestTwigExtension extends \Twig_Extension
    /** @var Container */
    protected $container;
    public function __construct(Container $container)
        $this->container = $container;
    public function getName()
        return 'test_twig_extension';
     * Returns a list of filters to add to the existing list.
     * @return array An array of filters
    public function getFilters()
        return array(
            new \Twig_SimpleFilter( 'xml_convert', array($this, 'xmlConvertFilter') ),
     * Renders a XMLBlock passing the content object id
     * @return array An array of filters
    public function xmlConvertFilter( $XMLDoc, $contentId )
        if (!is_numeric($contentId) || $contentId <= 0) {
            $contentId = false;
        $xmlConverter = $this->container->get('test.ezxml.converter'); /* @var $xmlConverter \Test\BaseBundle\Services\XMLConverter */
        return $xmlConverter->convert($XMLDoc, $contentId);

The pre-converter does the search query for related articles based on the current article. The results of the query are formatted as individual tags each named more_like_this_item.

namespace Test\BaseBundle\Services;
use \Symfony\Component\DependencyInjection\ContainerInterface;
use \eZ\Publish\API\Repository\Values\Content\LocationQuery;
use \eZ\Publish\API\Repository\Values\Content\Query\Criterion;
 * eZXML pre-processor class
class XMLConverter
    /** @var ContainerInterface */
    private $container;
    /** @var \eZ\Publish\API\Repository\Repository */
    private $repository;
     * Class constructor
     * @param ContainerInterface $container The container to be used
    function __construct( ContainerInterface $container )
        $this->container = $container;
        $this->repository = $this->container->get( 'ezpublish.api.repository' );
        $this->contentService = $this->repository->getContentService();
     * This method pre-processes a eZXML Document
     * @param \DOMDocument $XMLDoc Document to process
     * @param int $contentId Content Object Id
     * @return \DOMDocument Pre-processed document
    public function convert( \DOMDocument $XMLDoc, $contentId )
        $xpath = new \DOMXPath( $XMLDoc );
        $content = $this->contentService->loadContent( $contentId );
        if ($contentId) {
            $elements = $xpath->query("//custom[@name='more_like_this']");
            if ($elements->length) {
                $moreLikeThisNode = $elements->item(0);
                $searchService = $this->repository->getSearchService();
                $query = new LocationQuery();
                $query->query = new Criterion\LogicalAnd([
                    new Criterion\ContentTypeId( $content->contentInfo->contentTypeId ),
                    new Criterion\Location\IsMainLocation(Criterion\Location\IsMainLocation::MAIN),
                $query->limit = 5;
                $searchResult = $searchService->findLocations( $query );
                foreach ( $searchResult->searchHits as $searchHit ) {
                    $itemLocation = $searchHit->valueObject; /* @var $itemLocation \eZ\Publish\API\Repository\Values\Content\Location */
                    $moreLikeThisElement = $XMLDoc->createElement('custom');
                    $moreLikeThisElement->setAttribute('name', 'more_like_this_item');
                    $moreLikeThisElement->setAttribute('title', $itemLocation->contentInfo->name);
                    $moreLikeThisElement->setAttribute('url', $this->container->get('router')->generate($itemLocation));
       return $XMLDoc;

Finally, in our custom_tags.xsl file we can convert the data elements into HTML!

<xsl:template match="custom[@name='more_like_this']">
        <xsl:apply-templates />
<xsl:template match="custom[@name='more_like_this_item']">
    <h2>Related articles</h2>
            <xsl:attribute name="href">
                <xsl:value-of select="@url" />
            <xsl:value-of select="@title" />

Here is an example of the "more like this" widget:

More Like This custom tag

Related Blog Posts

First draft preview in eZ Publish 5

The eZ Publish 5 series has been a successful step forward for eZ Publish with the adoption of the Symfony stack and the ability to run dual kernels with...

Read more »

JavaScript and CSS in eZ Publish 5

A key component of a content management system or web application is the handling of JavaScript and CSS files, specifically around loading, combining,...

Read more »

Reading object states in eZ Publish 5

Object states are used in many ways in eZ Publish, from workflows to menu management to controlling SEO tags. In eZ Publish 5, object state information...

Read more »

ezurl() links in eZ Publish 5

In eZ Publish 4 / legacy, formatting link URLs is handled by the well-known ezurl() template operator. This is especially useful when you have multiple...

Read more »

Version history limit in eZ Publish 5

Whenever you edit content in the eZ Publish Administration Interface, eZ Publish stores a new version. eZ Publish has a built-in feature to limit the...

Read more »

An introduction to fetching content in eZ Publish 4 and eZ Publish 5

Fetching content in eZ Publish 4 (the "legacy" stack) and eZ Publish 5 (the "new" stack) follow many of the same principles, which is not surprising since...

Read more »


blog comments powered by Disqus

Hi, we're Mugo Web - Nice to meet you!

We're a group of web experts who solve complex web problems.

Learn more about us »



Yes - we can do that.

Many years of experience with complex websites allows us to offer total solutions.

Learn more about what we can do »

We love our clients (and they love us too)

We've solved problems across North America and around the world.

Learn more about what we've done »

We tweet too

Follow us on Twitter for the latest Mugo happenings


© 2008 - 2016 Mugo Web. All rights reserved.