Outputting Structured Data in the FE Template

MM data can be supplemented in the source code with so-called “structured data” to make the content easier to analyse — for example by search engines. One of the most well-known catalogues for such markup is available at Schema.org.

Schemas can be created in the encodings RDFa, Microdata, and JSON-LD and embedded into the page. Up to Contao 4.9, the encoding Contao used was Microdata — since Contao 4.12, JSON-LD is used.

To add the markup, create a custom template based on metamodels_prerendered.html5 and adapt it as shown in the following examples for a job posting — see JobPosting.

The markup can be validated with tools such as:

More on this topic can be found at Search Engine Optimisation (SEO).

Markup with JSON-LD

Note

This is only available from Contao 4.13 with MM 2.3.

 1<?php
 2
 3use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager;
 4use Contao\System;
 5use Spatie\SchemaOrg\JobPosting;
 6use Spatie\SchemaOrg\Organization;
 7use Spatie\SchemaOrg\Place;
 8use Spatie\SchemaOrg\PostalAddress;
 9use Spatie\SchemaOrg\PropertyValue;
10
11$jsonLdGraph     = null;
12$responseContext = System::getContainer()->get('contao.routing.response_context_accessor')->getResponseContext();
13if ($responseContext && $responseContext->has(JsonLdManager::class))
14{
15    /** @var JsonLdManager $jsonLdManager */
16    $jsonLdManager = $responseContext->get(JsonLdManager::class);
17    $jsonLdGraph   = $jsonLdManager->getGraphForSchema(JsonLdManager::SCHEMA_ORG);
18}
19?>
20<?php if (count($this->data)): ?>
21    <div class="layout_full">
22        <?php foreach ($this->data as $arrItem): ?>
23            <?php
24            // Build Schema.org data.
25            $schemaData = (new JobPosting())
26                ->identifier((new PropertyValue())->propertyID('jobId')->value($arrItem['raw']['id']))
27                ->hiringOrganization((new Organization())->name($arrItem['text']['corporation_name']))
28                ->title($arrItem['text']['name'])
29                ->datePosted(date('Y-m-d', $arrItem['raw']['created_date']))
30                ->jobLocation((new Place())->address((new PostalAddress())->addressCountry($arrItem['text']['country'])))
31                ->description($arrItem['text']['description']);
32            ?>
33            <div class="item <?= $arrItem['class'] ?>">
34                <h2 itemprop="title"><?= $arrItem['text']['title'] ?></h2>
35                <div>
36                    <p><strong>Location:</strong><?= $arrItem['text']['city'] ?> <?= $arrItem['text']['region'] ?>
37                    </p>
38                </div>
39                ...
40                <div class="actions">
41                    <?php if (null !== ($href = $arrItem['actions']['jumpTo']['href'] ?? null)) {
42                        $schemaData->url($href);
43                    } ?>
44                    <?php foreach ($arrItem['actions'] as $action): ?>
45                        <?php $this->insert('mm_actionbutton', ['action' => $action]); ?>
46                    <?php endforeach; ?>
47                </div>
48            </div>
49            <?php /* Add Schema.org data. */ $jsonLdGraph?->add($schemaData, 'job-' . $arrItem['raw']['id']); ?>
50        <?php endforeach; ?>
51    </div>
52<?php else : ?>
53    <?php $this->block('noItem'); ?>
54    <p class="info"><?= $this->noItemsMsg ?></p>
55    <?php $this->endblock(); ?>
56<?php endif; ?>

Embedding via JSON-LD does require a few extra lines of code, but the markup is separated from the HTML source used for browser rendering. This makes it easier to adapt existing templates or extend them with additional markup.

When adding multiple records to the graph — e.g. in an MM list output — passing a unique identifier is required: $jsonLdGraph?->add($schemaData, <Unique-ID>).

Markup with Microdata

Microdata markup requires more extensive template changes — embedding as JSON-LD is therefore recommended.

 1<?php if (count($this->data)): ?>
 2    <div class="layout_full">
 3        <?php foreach ($this->data as $arrKey => $arrItem): ?>
 4            <div class="item <?= $arrItem['class'] ?>" itemscope itemtype="https://schema.org/JobPosting">
 5                <h2 itemprop="title"><?= $arrItem['text']['title'] ?></h2>
 6                <div>
 7                    <p><strong>Location:</strong> <span itemprop="jobLocation" itemscope
 8                                                        itemtype="https://schema.org/Place">
 9                            <span itemprop="address" itemscope itemtype="https://schema.org/PostalAddress">
10                            <span itemprop="addressLocality"><?= $arrItem['text']['city'] ?></span>
11                                <span itemprop="addressRegion"><?= $arrItem['text']['region'] ?></span>
12                            </span>
13                        </span>
14                    </p>
15                </div>
16                ...
17            </div>
18        <?php endforeach; ?>
19    </div>
20<?php else : ?>
21    <?php $this->block('noItem'); ?>
22    <p class="info"><?= $this->noItemsMsg ?></p>
23    <?php $this->endblock(); ?>
24<?php endif; ?>