alengo/sulu-content-extra-bundle
Extends Sulu CMS 3.x Pages and Articles with an Additional Data tab, built-in entities, configurable mapping for localized/unlocalized fields, auto entity registration, navigation link markers, and sortable template groups. PHP 8.2+, Symfony 7.
Installation:
composer require alengo/sulu-content-extra-bundle
Register the bundle in config/bundles.php:
Alengo\SuluContentExtraBundle\AlengoContentExtraBundle::class => ['all' => true],
Define Forms:
Create a form XML file (e.g., config/forms/page_additional_data.xml) with your desired fields. Example:
<form xmlns="...">
<key>page_additional_data</key>
<properties>
<property name="template_theme" type="select" mandatory="false">
<meta><title lang="en">Theme</title></meta>
<params><param name="values" type="collection"><param name="default" type="collection"><param name="title" value="Default"/><param name="name" value="default"/></param></param></params>
</property>
</properties>
</form>
Configure Field Mapping (optional):
Create config/packages/alengo_content_extra.yaml to define which fields are unlocalized/localized:
alengo_content_extra:
page:
unlocalized_keys:
- template_theme
localized_keys:
- notes
Clear Cache:
php bin/console cache:clear
Add a theme selector to Pages:
template_theme as an unlocalized field (shared across languages).{{ page.additionalData.template_theme }}.Form Integration:
The bundle auto-registers a tab in the Sulu admin for Pages/Articles (configurable via tab_title).
Define fields in config/forms/{form_key}.xml (e.g., page_additional_data.xml).
Example fields:
<property name="campaign_id" type="text" mandatory="false">
<meta><title lang="en">Campaign ID</title></meta>
</property>
<property name="seo_overrides" type="textarea" mandatory="false">
<meta><title lang="en">SEO Overrides (JSON)</title></meta>
</property>
Field Localization:
Use unlocalized_keys/localized_keys in config to control storage:
alengo_content_extra:
page:
unlocalized_keys: [template_theme, campaign_id] # Shared across languages
localized_keys: [notes, seo_overrides] # Per-language
Page/Article entity.PageDimensionContent/ArticleDimensionContent.Accessing Data in Templates:
{# Page template #}
<div class="theme-{{ page.additionalData.template_theme }}">
{{ page.additionalData.notes[locale] }} {# Localized notes #}
</div>
{# Article template #}
{% if article.additionalData.seo_overrides[locale] is not empty %}
<meta name="description" content="{{ article.additionalData.seo_overrides[locale].description }}">
{% endif %}
Enable for Link-Type Pages:
The bundle adds sourceLink (bool) and sourceUuid (string) to link-type pages.
Example config (auto-enabled by default):
alengo_content_extra:
page:
enabled: true
Use in Templates:
Access via the navlink content section (bypasses Sulu’s TemplateResolver):
{# Check if a page is a redirect #}
{% if navlink.sourceLink %}
<div class="redirect-indicator">This page redirects to: {{ navlink.sourceUuid }}</div>
{% endif %}
{# Dynamic logic based on source #}
{% if navlink.sourceUuid == 'product-landing-page-uuid' %}
<script>trackRedirect('product_landing');</script>
{% endif %}
alengo_content_extra:
page:
page_class: App\Entity\CustomPage
entity_class: App\Entity\CustomPageDimensionContent
// src/Entity/CustomPage.php
namespace App\Entity;
use Alengo\SuluContentExtraBundle\Entity\Page as BasePage;
class CustomPage extends BasePage {}
metadata_group_provider to order admin tabs by translation keys.sulu_admin.template_group.* in translations:
# translations/admin+intl-icu.en.yaml
sulu_admin:
template_group:
basic: Basic Information
additional_data: Additional Data {# Will appear after "basic" #}
seo: SEO Settings
Validation:
Add validation to forms or use Symfony’s constraints in XML:
<property name="campaign_id" type="text" mandatory="false">
<meta><title lang="en">Campaign ID</title></meta>
<constraints>
<constraint name="Regex" options='{"pattern": "/^[A-Za-z0-9_-]+$/"}' />
</constraints>
</property>
Migrations:
If upgrading or adding new fields, create a migration to handle the additionalData JSON column:
// src/Migration/VersionYYYYMMDDHHMMSS.php
public function up(SchemaManagerInterface $sm): void
{
$sm->getConnection()->getSchemaManager()->createTable('pa_page_dimension_contents', function (CreateTable $table) {
$table->json('additional_data')->nullable()->default(null);
});
}
API Exposure:
Expose additional data via Sulu’s API by extending the PageResource/ArticleResource:
// src/Serializer/Normalizer/PageNormalizer.php
use Alengo\SuluContentExtraBundle\Model\AdditionalDataInterface;
class PageNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = []): array
{
$data = parent::normalize($object, $format, $context);
if ($object instanceof AdditionalDataInterface) {
$data['additionalData'] = $object->getAdditionalData();
}
return $data;
}
}
Event Listeners: React to changes in additional data via Sulu’s events:
// src/EventListener/AdditionalDataListener.php
use Alengo\SuluContentExtraBundle\Model\AdditionalDataInterface;
use Sulu\Bundle\CoreBundle\Event\UpdateContentEvent;
class AdditionalDataListener
{
public function onUpdateContent(UpdateContentEvent $event): void
{
$content = $event->getContent();
if ($content instanceof AdditionalDataInterface) {
$data = $content->getAdditionalData();
if (isset($data['campaign_id'])) {
// Trigger analytics or other logic
}
}
}
}
Register the listener in services.yaml:
services:
App\EventListener\AdditionalDataListener:
tags:
- { name: kernel.event_listener, event: sulu_core.update_content, method: onUpdateContent }
Proxy Generation in Production:
config/packages/prod/doctrine.yaml:
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
cache:warmup (part of deploy).Field Name Conflicts:
title, published), data may be overwritten or lost.custom_title_override).Localization Mismatches:
locale dimension is active in Sulu’s admin and that the field is marked as localized in the config.Navigation Markers Not Appearing:
sourceLink/`sourceHow can I help you explore Laravel packages today?