trk / sulu-block-suite-bundle
Component block props, visual presets, reusable global blocks, and template layout engine for Sulu CMS
Package info
github.com/trk/SuluBlockSuiteBundle
Type:symfony-bundle
pkg:composer/trk/sulu-block-suite-bundle
Requires
- php: >=8.3
- doctrine/orm: ^3.0
- psr/log: ^3.0
- sulu/sulu: ^3.0
- symfony/config: ^7.0
- symfony/console: ^7.0
- symfony/dependency-injection: ^7.0
- symfony/event-dispatcher: ^7.4
- symfony/form: ^7.0
- symfony/framework-bundle: ^7.0
- symfony/http-foundation: ^7.0
- symfony/http-kernel: ^7.0
- symfony/messenger: ^7.0
- symfony/routing: ^7.0
- symfony/security-core: ^7.4
- symfony/service-contracts: ^3.5
- symfony/translation-contracts: ^3.0
- symfony/ux-twig-component: ^2.13 || ^3.0
- symfony/yaml: ^7.0
- twig/twig: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- mcp/sdk: ^0.8.1
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.0 || ^12.0
- symfony/property-access: ^7.0
Suggests
- sulu/mcp-bundle: Enables Sulu Block Suite MCP tools (sulu_block_suite_blocks, sulu_block_suite_presets, sulu_block_suite_compose_block) for AI content management
- sulu/sulu-mate-extension: Enables Symfony AI Mate auto-discovery of Block Suite instructions and MCP tools
- trk/sulu-multi-webspace-bundle: Enables multi-webspace permission filtering and content override integration for Page Layouts, Reusable Blocks, and Block Presets
Provides
None
Conflicts
None
Replaces
None
README
A 100% UI-framework-agnostic block infrastructure engine for Sulu CMS 3.x that cleanly separates block infrastructure (Core), UI component frameworks (such as Preline UI, Bootstrap, UIkit, or custom design systems), and project-level block composition:
- Compile-Time Native XML Block Tabs (
BlockSuiteFormMetadataVisitor+<TabSection>): Define block fields using standard Sulu XML<section>elements (content,settings,general, or custom sections) and automatically inherit sharedDynamic ContentandAdvancedtabs at form metadata compilation time—with zero runtime XHR schema requests. - Automatic
props/*Namespacing & Default Value Resolution (BlockSuiteMetadataResolver): Fields insidecontentstay at the block root (block.title,block.items), while fields in non-contenttabs (settings,general,dynamic_content,advanced) are automatically namespaced underprops/*(block.props.tag,block.props.preset), with XML<param name="default_value">defaults applied automatically during website rendering. - Inline Field Option Groups (
withBlockFieldOptions+<params><param type="collection">): Attach native SuluArrowMenu+ArrowMenu.SingleItemSectionicon option menus directly to standard Sulu fields (text_line,link,single_icon_selection,single_media_selection,text_area,text_editor,color,number,email,phone,url) on theContenttab—storing selected values inprops/<target>(or<target>on sub-items), resolving defaults via hidden backing fields, and automatically pruning duplicate fields fromsettings. - Dynamic Content / Smart Content Engine & Mixed Static/Dynamic Sub-Items: Bind any block or repeater block to live CMS data sources (
pages,articles,categories,media_collection,search, SmartContent providers, or customConfigurableDynamicContentSourceProviderInterfaceservices) with canonical field-type auto-mapping,{{field}}inline token interpolation, custom/computed outputs, mixed static & dynamic sub-items (is_dynamic_template), and collection summary/empty-state/pagination rendering. - Two-Way Live Preview Block Focus & Sibling Auto-Collapse (
initPreviewBlockFocus): Click any rendered block (including deeply nestedsection > rows > columns > blocks > itemshierarchies andreusableblocks) in the Sulu Live Preview iframe to expand its ancestor tabs/accordions, scroll to and highlight the block in the Admin form, or click a block in the Admin form to scroll and pulse-highlight it in the Live Preview iframe. - Preset Gallery, Wireframe Registry & Reusable Global Blocks: Browse YAML and CMS-managed presets in a visual gallery (
preset_selection), register custom React wireframe previews viapresetWireframeRegistry, save blocks as presets or reusable snippets directly from theAdvancedtab, and render central page layouts (layout_render()) with automatic Sulu HTTP cache reference invalidation. - Deep Core Modules for Layout, Navigation, Media & AI/MCP (
FlatFlowLayoutGrouper,SuluNavigationTreeNormalizer,ResponsiveWidth,AbstractXmlBlockAiCatalog,BlockXmlSchemaExtractor,McpToolRegistrar): Framework-agnostic flat-flow layout grouping (section_break/row/column_break), Sulu Webspace navigation tree normalization, responsive slot-width and<img sizes>calculation, Sulu FormBundleFormViewre-render cloning,tabler_icon_selectionproperty resolution, and 3-tier AI/MCP block schema extraction. - Full Internationalization (i18n): 6 admin languages supported out of the box (
en,tr,de,ru,es,fr).
Table of Contents
- Architecture & Responsibility Boundaries
- Requirements
- Installation
- Configuration Reference
- Core Data Contract
- Twig Functions & Filters Reference
- CLI Commands
- Extension Points
- Feature Guides
- 1. Native Sulu XML Block Tabs &
sulu_block_suite.tabsTag - 2. Inline Field Option Groups (
ArrowMenuvia<params>) - 3. Dynamic Content Engine, Mixed Sub-Items & Token Interpolation
- 4. Two-Way Live Preview Block Focus & Auto-Collapse
- 5. Preset Gallery, Wireframe Registry & Reusable Block Actions
- 6. Scoped
.el-elementCSS & Sanitized Custom Attributes - 7. Reusable Global Blocks, Page Layouts & Fallback Chains (
|)
- 1. Native Sulu XML Block Tabs &
- Writing a Framework Package
- Composing Blocks in a Project
- Internationalization (i18n)
- Verification & Quality Assurance
- License
Architecture & Responsibility Boundaries
| Layer | Responsibility (Deep Modules & Seams) | Outside This Seam |
|---|---|---|
sulu-block-suite-bundle (Core Engine) |
Transform pipeline (BlockTransformPipeline), flat-flow layout state machine (FlatFlowLayoutGrouper, SectionFlattener), Sulu Webspace navigation & active URL engine (SuluNavigationTreeNormalizer, ActiveUrlMatcher), responsive slot-width engine (ResponsiveWidth), base component engine (AbstractBlockComponent), data & prop normalizers (SuluDataNormalizer, PropReader), XML schema & AI/MCP engine (BlockXmlSchemaExtractor, AbstractXmlBlockAiCatalog, McpToolRegistrar), inline field option enhancer (withBlockFieldOptions), Live Preview block focus (initPreviewBlockFocus), preset/reusable/layout engines (YamlBlockPresetProvider, PageLayoutProvider), admin registries (presetWireframeRegistry, tabIconRegistry), property resolvers (TablerIconPropertyResolver, SingleReusableBlockSelectionPropertyResolver), security utilities (AttributeSanitizer, CssScoper), and BlockSuiteFormMetadataVisitor / BlockSuiteMetadataResolver |
UI block XML definitions (except reusable), framework styling fields (margin_*, padding_*, hover_image, hover_video, blend, parallax), CSS framework token maps, or snippet template auto-registration |
UI Framework Adapter (e.g. sulu-preline-blocks-bundle) |
Block XML schemas (config/templates/blocks/*.xml), styling tabs (general via native xi:include), Twig components extending AbstractBlockComponent, CSS token maps (PrelineClassMap, PropsResolver), thin collection/AI adapters (PrelineBlockCollectionRenderer, PrelineBlocksAiCatalog), framework translations |
CMS workflow, metadata resolution, flat-flow grouping state machine, Sulu navigation tree walking, or persistence logic provided by Core |
Project (./config, ./templates) |
Sulu-native <types><type ref="..."/></types> block composition, project snippet XMLs (page_layout, reusable_block, block_preset), project-specific blocks, responsive image srcset_formats, layout defaults, YAML presets |
Core or UI framework adapter internals |
Requirements
- PHP:
>= 8.3(strictly typed PHP 8.5 supported) - Sulu CMS:
~3.0.10 - Symfony:
^7.4 - Doctrine ORM:
^3.0
Installation
1. Composer Package
composer require trk/sulu-block-suite-bundle
2. Enable the Bundle
// config/bundles.php return [ // ... Totoglu\SuluBlockSuiteBundle\SuluBlockSuiteBundle::class => ['all' => true], ];
3. Register Admin Frontend Assets
-
Add the package dependency in
assets/admin/package.json:{ "dependencies": { "sulu-block-suite-bundle": "file:../../vendor/trk/sulu-block-suite-bundle/assets/admin" } } -
Import the bundle in
assets/admin/app.js(orassets/admin/index.js):import 'sulu-block-suite-bundle';
-
Build the admin assets:
npm --prefix assets/admin install npm --prefix assets/admin run build
4. Initialize Project Snippet Templates (sulu:block-suite:init)
Core does not auto-register snippet templates so that each project retains full Sulu-native control over which blocks are allowed in page_layout, reusable_block, and block_preset. Run the initialization command to scaffold the starter templates into your project without overwriting existing files:
php bin/console sulu:block-suite:init
Configuration Reference
# config/packages/sulu_block_suite.yaml sulu_block_suite: multi_webspace: # null (default): auto-enabled when Totoglu\SuluMultiWebspaceBundle\SuluMultiWebspaceBundle is registered; # when false, hides webspace selector fields in snippets and enforces global template uniqueness. enabled: null presets: enabled: true enable_snippet_presets: true # String or list of directories containing <block_type>.yaml preset definitions yaml_dir: - '%kernel.project_dir%/config/blocks/presets' reusable_blocks: enabled: true layout: enabled: true # String or list of directories containing <layout_key>.yaml layout definitions yaml_dir: - '%kernel.project_dir%/config/blocks/layouts' defaults: sidebar: 'none' header_variant: 'solid' container_width: 'boxed' content_padding: 'normal' mappings: {} rendering: # Priority-keyed map of Twig template namespaces (higher integer = higher priority). # Multiple framework bundles can prepend their own namespace without overwriting each other. template_namespaces: blocks: 1000 '@SuluBlockSuite/blocks': 0 media: # Map of Sulu image format keys to pixel widths for responsive <field>_srcset generation. srcset_formats: col-sm: 400 col-md: 640 col-lg: 960 col-xl: 1280 col-2xl: 1600 preview_tags: # Additional field names excluded from collapsed block preview summaries in Sulu Admin excluded_fields: [] # Additional text_line field names given primary title priority (1024) in collapsed block previews primary_text_fields: []
Core Data Contract
When a block passes through BlockTransformPipeline (ReusableBlockTransformer at priority 110 → DynamicContentTransformer at 100 → DefaultAttributesTransformer at 90 → FormBlockTransformer at 85 → PresetBlockVisitor at 80), Core enriches the normalized block array with a deterministic, framework-agnostic contract:
| Key | Type | Source & Behavior |
|---|---|---|
_id |
string |
Explicit props.id sanitized via AttributeSanitizer::sanitizeId(), or an auto-generated deterministic el-<8-hex> ID when props.css contains .el-element rules (or bare CSS declarations). |
_sulu_block_id |
string |
Native Sulu block identifier (_id generated by BlockIdGenerator) preserved across transformation and propagated down nested container hierarchies (rows, columns, blocks, items) for Live Preview focus. |
_reusable_parent_block_id |
string |
Set by ReusableBlockTransformer when expanding a reusable block so clicking any expanded inner block in Live Preview focuses the parent reusable block in Sulu Admin. |
_name |
string |
Trimmed human-readable block label from props.name (when configured in the Advanced tab). |
_class / _classes |
string |
Trimmed custom CSS class string from props.class / props.custom_class. |
_attributes |
array<string, string> |
Parsed key="value" pairs from props.attributes, sanitized by AttributeSanitizer::parse() (blocks on* inline event handlers, style, id, class, and javascript:/vbscript:/data: URIs). In Sulu Live Preview mode, automatically includes data-block-id="<_sulu_block_id>" for page/article blocks (and omits it when _in_page_layout is true). |
_attributes_string |
string |
Escaped HTML attribute string assembled from _attributes (ready for raw insertion on a wrapper element when not using AbstractBlockComponent). |
_scoped_css |
string |
Sanitized CSS from props.css processed by CssScoper::scope(), rewriting .el-element (or wrapping bare declarations) to #<_id> and stripping <script>/<style> tags, expression(), @import, and javascript: URLs. BlockRenderer automatically prepends <style>...</style> when non-empty. |
_props |
array<string, mixed> |
Normalized mirror of props with root-level props/* keys merged cleanly. |
<field>_url |
string |
Automatically derived for every media-type (single_media_selection, media_selection) and link-type (link) field in the block's Sulu form metadata (e.g. image → image_url, link → link_url). |
<field>_srcset |
string |
Automatically derived responsive srcset string for every media-type field using sulu_block_suite.media.srcset_formats (e.g. image_srcset). |
<field>_focus_style |
string |
Automatically derived CSS object-position: X% Y%; focal point string from Sulu media focus coordinates for every media-type field (e.g. image_focus_style). |
_summary |
array<string, mixed> |
Present when props.dynamic_show_summary is enabled on a Dynamic Content collection block (total, filtered_total, showing, from, to, page, pages, is_filtered, text). |
_empty_state |
array<string, mixed> |
Present when a Dynamic Content collection query returns zero items (is_empty: true, message: string). |
_pagination |
array<string, mixed> |
Present when props.dynamic_pagination is enabled on a Dynamic Content collection block (current_page, per_page, total_items, total_pages, has_previous, has_next, previous_page, next_page, page_param). |
Twig Functions & Filters Reference
Block Rendering & Transformation (DynamicContentTwigExtension)
| Function / Filter | Kind | Description |
|---|---|---|
block_render(block, context = []) |
Function | Runs the block through BlockTransformPipeline, resolves the highest-priority template from rendering.template_namespaces ({namespace}/{type}.html.twig), injects data-block-id on the root HTML element in Live Preview, and prepends _scoped_css + optional _summary / _empty_state output. |
blocks_render(blocks, context = []) |
Function | Renders a list of blocks sequentially through BlockCollectionRendererInterface (BlockRenderer::renderBlocks()). |
block_transform(block, context = []) |
Function & Filter | Runs a single block array through BlockTransformPipeline and returns the normalized array (?array). |
hydrate_blocks(blocks, context = []) |
Function & Filter | Hydrates a flat list of blocks through BlockTransformPipeline (skipping suppressed null blocks). |
hydrate_block_tree(blocks, context = []) |
Function & Filter | Recursively transforms a block tree and its nested blocks children through BlockTransformPipeline. |
sulu_user_loggedin_and_preview() |
Function | Returns true when rendering inside Sulu Admin Live Preview (attributes.preview == true or request preview flag). |
sulu_preview_block_focus_script() |
Function | Returns the idempotent <script> snippet for Live Preview click-to-focus and highlight indicators (automatically appended once at depth 1 by BlockRenderer). |
Layouts & Reusable Blocks (LayoutTwigExtension)
| Function | Description |
|---|---|
layout_render(layoutKey = 'default', context = []) |
Resolves a page_layout CMS snippet or YAML layout definition (supporting pipe-delimited fallback chains like `'article |
layout_load(layoutKey = 'default', locale = null) |
Loads the raw block array for the first matching layout in a pipe-delimited fallback chain (list<array<string, mixed>>). |
layout_exists(layoutKey = 'default', locale = null) |
Returns true if any layout in the fallback chain exists and contains at least one block. |
layout_has_block(blockType, layoutKey = 'default', locale = null) |
Recursively checks whether the resolved layout contains a block of the given blockType (inspecting nested rows, columns, blocks, items, and referenced reusable snippets). |
layout_classes(settings = [], groups = null) |
Resolves layout settings through LayoutResolverInterface::resolveClasses() into a space-separated CSS class string. |
layout_class(group, value = null) |
Resolves a single layout setting group/value through LayoutResolverInterface::resolveClass(). |
layout_attributes(settings = []) |
Resolves data-layout-* HTML attributes (data-layout-sidebar, data-layout-header-variant, etc.) from layout settings. |
reusable_block_load(uuid, locale = null) |
Loads the blocks from a published reusable_block snippet by UUID and registers its reference in Sulu's HTTP cache ReferenceStoreInterface. |
CLI Commands
| Command | Description |
|---|---|
php bin/console sulu:block-suite:init |
Scaffolds starter snippet templates (page_layout.xml, reusable_block.xml, block_preset.xml), shared block type includes, and example YAML presets into the project without overwriting existing files (--dry-run, --no-presets, --force, --source). |
php bin/console sulu:block-suite:export-presets [block-type] |
Exports CMS-managed block_preset snippets to YAML files in sulu_block_suite.presets.yaml_dir (--locale=en, --overwrite). |
Extension Points
BlockTransformerInterface(Totoglu\SuluBlockSuiteBundle\Transformer\BlockTransformerInterface):- Autoconfigured via DI tag
sulu_block_suite.block_transformer. - Implement
transform(array $block, array $context = []): ?arrayandgetPriority(): intto inspect, enrich, or suppress (return null) blocks during rendering.
- Autoconfigured via DI tag
NestedBlockFlattenerInterface(Totoglu\SuluBlockSuiteBundle\Transformer\NestedBlockFlattenerInterface):- Autoconfigured via DI tag
sulu_block_suite.nested_block_flattener. - Implement
supports(array $block): boolandflatten(array $block, callable $expandChild): arrayso framework-specific container blocks (such assection > rows > columns > blocks) are cleanly unwrapped into leaf blocks when areusableblock is rendered inside an already nested column/slot.
- Autoconfigured via DI tag
DynamicContentSourceProviderInterface&ConfigurableDynamicContentSourceProviderInterface:- Autoconfigured via DI tag
sulu_block_suite.dynamic_source_provider. - Extend
AbstractDynamicContentSourceProviderto expose custom single or collection data sources, dynamic admin config fields (getConfigFields(),getSingleConfigFields()), custom/computed outputs (registerCustomOutput()), and collection metadata (resolveCollectionMeta()).
- Autoconfigured via DI tag
AbstractBlockComponent(Totoglu\SuluBlockSuiteBundle\Twig\Component\AbstractBlockComponent):- Deep base module for Symfony UX Twig Components consuming block arrays via
BlockComponentPreRenderSubscriber. - Override
protected function defaults(): array,protected function dataKeys(): array,protected function capabilityMap(): array, andprotected function capabilities(): array. - Automatically forwards Live Preview
data-block-id,_id,_class,_attributes, and_scoped_cssto the component's rootComponentAttributes({{ attributes }}). - Built-in Sulu CMS capabilities:
- Media & Focal Point:
getImageUrl(),getImageSrcSet(),getImageFocusPointStyle(),getImageAlt(),getVideoUrl(),resolveItemImageUrl(),resolveItemImageSrcSet(),getImageLoading(),getImageFetchPriority(),getIntrinsicImageDimensions(). - Responsive Slot Width (
ResponsiveWidth):getSlotWidth(),getMediaSlotWidth(),getImageSizes(),getChildContext(). - Links & Active URL Matching (
ActiveUrlMatcher):getLinkUrl(),getLinkTarget(),getLinkRel(),isUrlActive(),isInternalUrlActive(),hasActiveNestedEntries(),getPageUuid(). - Sulu FormBundle Re-Render Cloning:
resolveReusableFormView(),getFormEntity(),isSubmitted(),getSuccessText(). - Lightbox & Collection Tags:
isLightboxEnabled(),getLightboxOptions(),getLightboxConfigJson(),getFilterTags(),getItemTagSlugs(). - Content & Icon Accessors:
getTitle(),getSubtitle(),getMeta(),getResolvedContent(),getIconClass().
- Media & Focal Point:
- Deep base module for Symfony UX Twig Components consuming block arrays via
FlatFlowLayoutGrouper(Totoglu\SuluBlockSuiteBundle\Layout\FlatFlowLayoutGrouper):- Converts flat Sulu block arrays containing
section_break,row_break/row, andcolumn_breakcontrol blocks into structuredsections -> rows -> columns -> blockstrees (buildTopLevelEntries(),groupIntoSections(),extractRowColumns()). - UI framework collection renderers pass CSS class callbacks to
FlatFlowLayoutGrouperwhile keeping all state-machine transitions centralized in Core.
- Converts flat Sulu block arrays containing
SuluDataNormalizer&PropReader(Totoglu\SuluBlockSuiteBundle\Normalizer\SuluDataNormalizer,Totoglu\SuluBlockSuiteBundle\Util\PropReader):SuluDataNormalizer: Resolves Sulu media URLs/srcsets/focal points, links, snippet UUIDs, Tabler icons (resolveIconClass()), collection item media/icon/link fields (normalizeItemMediaAndLinks()), safe HTTPS Google Maps / OpenStreetMap embed URLs (sanitizeMapEmbedUrl()), ISO-8601 dates (resolveIsoDate()), and Unicode initials (extractInitials()).PropReader: Type-safe prop and collection-item readers (string(),nullableString(),bool(),int(),float(),stringList(),itemValue(),itemString(),itemNullableString(),itemBool(),itemInheritedString()).
SuluNavigationTreeNormalizer(Totoglu\SuluBlockSuiteBundle\Navigation\SuluNavigationTreeNormalizer):- Normalizes raw Sulu Webspace navigation trees (
sulu_navigation_root_tree/sulu_navigation_flat) into recursive link/children structures with active-trail detection.
- Normalizes raw Sulu Webspace navigation trees (
AbstractXmlBlockAiCatalog,BlockXmlSchemaExtractor&McpToolRegistrar:- Extend
AbstractXmlBlockAiCatalogin UI framework bundles to expose 3-tier AI block discovery (listBlockTypes(),listPresets(),inspectBlockSchema(),composeBlockPayload(),preserveTechnicalProps()). - Use
McpToolRegistrar::registerOptionalTags()in DI extensions to register MCP tools and resources without hard runtime dependencies onmcp/sdk.
- Extend
presetWireframeRegistry&tabIconRegistry(Admin JS):- Exported from
sulu-block-suite-bundle(assets/admin/registries/presetWireframeRegistryandassets/admin/registries/tabIconRegistry). - Register custom React wireframe preview components per block type (
presetWireframeRegistry.add('my_block', MyWireframeComponent)) and custom Sulu Admin tab icons (tabIconRegistry.registerPrefix('my_domain_', 'su-tag')).
- Exported from
Feature Guides
1. Native Sulu XML Block Tabs & sulu_block_suite.tabs Tag
- Properties in
<section name="content">(or<section name="media">and top-level properties outside a section) remain at the root of the block payload. - Any other
<section>(settings,general,style,layout, etc.) becomes its own horizontal tab in the exact order defined, and its fields are automatically namespaced underprops/*. - Core appends
dynamic_content(config/forms/block_suite_dynamic.xml) andadvanced(config/forms/block_suite_advanced.xml) at compile time unless customized via<tag name="sulu_block_suite.tabs" .../>:exclude="dynamic_content,advanced": Opt out of specific Core-injected tabs.enabled="false": Disable tab wrapping entirely for a block type (e.g.reusable.xml).shared="false": Wrap the block's own<section>elements into tabs without injecting Core's shareddynamic_contentandadvancedtabs.namespace_props="false": Keep fields in non-contentsections at the root level instead of prefixing them withprops/.
- Automatic Collapsed Block Preview Tags:
BlockSuiteFormMetadataVisitor::enhanceBlockPreviewTags()automatically assignssulu.block_previewpriorities when a block does not declare explicit preview tags: Media (2048) → Primary Text (1024..768) → Rich Text (512) → Child Blocks (256).
2. Inline Field Option Groups (ArrowMenu via <params>)
Standard Sulu fields (text_line, link, single_icon_selection, single_media_selection, text_area, text_editor, color, number, email, phone, url) can attach one or more Icon + ArrowMenu.SingleItemSection Popover option groups directly via <params><param name="..." type="collection">:
<section name="content"> <property name="title" type="text_line"> <meta> <title lang="en">Title</title> </meta> <params> <param name="headline" value="true"/> <param name="tag" type="collection"> <param name="_default" value="h2"/> <param name="_icon" value="heading"/> <param name="h1"><meta><title lang="en">H1</title></meta></param> <param name="h2"><meta><title lang="en">H2</title></meta></param> <param name="h3"><meta><title lang="en">H3</title></meta></param> <param name="h4"><meta><title lang="en">H4</title></meta></param> <param name="div"><meta><title lang="en">div</title></meta></param> </param> <param name="title_color" type="collection"> <param name="_default" value="default"/> <param name="_icon" value="brush"/> <param name="default" value="Default"/> <param name="primary" value="Primary"/> <param name="muted" value="Muted"/> </param> </params> </property> </section>
How Inline Field Option Groups Work
- Storage Path:
- On a tabbed block (
namespaceProps = true), selecting an option writes toprops/<param_name>(e.g.props.tag = "h2",props.title_color = "primary"), while the field itself (title) keeps its native Sulu value (string, link object, media object, etc.). - On a flat sub-item inside
<block name="items">(namespaceProps = false), selecting an option writes directly to<param_name>on the sub-item (e.g.item.button_style = "solid").
- On a tabbed block (
- Meta Parameters inside
<param type="collection">:_default(ordefault_value): Default option value (falls back to the first selectable option if omitted)._icon(oricon): Trigger button icon (heading,brush,size,style,alignshorthands, or anysu-*/fa-*icon name). If omitted, inferred automatically from the option group name._target(ortarget): Override the target property name if it differs from the<param name="...">attribute._title: Custom popover section header title.
- Automatic
SettingsTab Deduplication & Hidden Backing Fields:BlockSuiteFormMetadataVisitorautomatically removes any duplicate<property name="tag">from non-contentsections (such assettings). Ifsettingsbecomes empty as a result, theSettingstab is automatically omitted.- A hidden (
visibleCondition="false")single_selectbackingFieldMetadatais registered on the form so Sulu's JSON schema validation andBlockSuiteMetadataResolverresolve the default value ("h2") on the backend even before the block is re-saved.
3. Dynamic Content Engine, Mixed Sub-Items & Token Interpolation
Blocks can dynamically pull single items or collections from Sulu CMS sources without custom controllers (including live resolution during Sulu Admin Live Preview):
| Source Key | Mode | Provider Class | Description |
|---|---|---|---|
current_page / parent_page / specific_page |
Single | SuluPageSourceProvider |
Current page, parent page, or a selected Sulu 3 page with content, SEO, and excerpt fields. |
page_selection / page_children / sibling_pages / custom_query |
Multiple | SuluPageSourceProvider |
Published Sulu 3 page collections with parent filtering, sorting, limit, offset, and tag/category filters. |
single_article / sulu_articles |
Single / Multiple | SuluArticleSourceProvider |
Published Sulu 3 articles (SuluArticleBundle) with template data, route URL, excerpt, and media fields. |
single_category / sulu_categories |
Single / Multiple | SuluCategorySourceProvider |
Sulu categories (CategoryManagerInterface) with optional parent category filtering and localized translations. |
single_media / sulu_media_collection |
Single / Multiple | SuluMediaCollectionSourceProvider |
Media assets from Sulu collections (MediaManagerInterface) with resolved image/thumbnail structures. |
search_context / search_results |
Single / Multiple | SuluSearchSourceProvider |
Live search query metadata (query, total) and search result items (title, url, description, image). |
sulu_snippets / sulu_contacts / sulu_accounts / sulu_* |
Single / Multiple | SmartContentSourceProviderAdapter |
Automatic bridge for any Sulu 3 SmartContentProviderInterface (sulu_content.smart_content_provider) with cached ResourceLoaderProvider loading and ReferenceStoreInterface tracking. |
Mixed Static & Dynamic Sub-Items (is_dynamic_template)
When a block defines a <block name="items"> repeater, BlockSuiteFormMetadataVisitor automatically injects a "Use as Dynamic Content Template" (is_dynamic_template) checkbox at the top of each sub-item type (visible whenever props.dynamic_source is active):
- Sub-items with
is_dynamic_template = trueact as visual blueprints and are expanded in place for each fetched dynamic record. - Unchecked sibling sub-items before or after the template item are preserved in their exact position as static items.
- If no sub-item has
is_dynamic_templatechecked, the first sub-item (items[0]) is used as the template whenitemshas at most one entry.
Inline {{field}} Token Interpolation & Sub-Block Mapping
- Token Interpolation: Any string field in a dynamically hydrated block or template sub-item can embed
{{field}}tokens with pipe fallbacks and modifiers:- Fallback chain:
{{excerpt_title|title}} - Length / truncation:
{{description:length=120}}or{{content:plain:truncate=160}} - Header summary tokens (on parent block
title/subtitle/description):%total%,%count%,%showing%,%from%,%to%,%page%,%pages%.
- Fallback chain:
- Canonical Field Types & Sub-Block Targets:
BlockTargetFieldMetadataResolvernormalizes Sulu property types to canonical types (string,text,media,link,icon,form,boolean,select,date,number,collection) and supports mapping multi-value source fields (categories,tags,taxonomies) directly into sub-block repeaters (elements,badges,buttons,specs,features,tags) as well as virtualbuttonsandbadgestargets. - Custom / Computed Outputs: Providers extending
AbstractDynamicContentSourceProvidercan register computed fields via$this->registerCustomOutput($name, $label, $type, $resolver).
4. Two-Way Live Preview Block Focus & Auto-Collapse
Core integrates two-way Preview ↔ Admin block focus and automatic sibling block collapsing out of the box:
- Preview → Admin (
sulu-preview-block-click):- During Sulu Live Preview,
DefaultAttributesTransformerandBlockRendererautomatically attachdata-block-id="<_sulu_block_id>"to rendered blocks and nested sub-blocks (section > rows > columns > blocks > items). - Blocks rendered via
PageLayoutProvider(layout_render()) are marked with_in_page_layout: trueso global header/footer layout blocks do not hijack page block focus. - Blocks expanded by
ReusableBlockTransformerinherit_reusable_parent_block_idso clicking any element inside a reusable block focuses the parentreusableblock in the Admin form. - Clicking a block in the preview iframe automatically switches to the matching ancestor tab (
Content), expands every ancestor Sulu block card along the hierarchy, collapses sibling blocks at each level, scrolls the target block into view, and applies a focus highlight ring.
- During Sulu Live Preview,
- Admin → Preview (
sulu-admin-block-focus):- Clicking or expanding a block card in the Sulu Admin form dispatches
sulu-admin-block-focusto the Live Preview iframe, which smoothly scrolls the matching[data-block-id]element into view and plays a pulse highlight outline.
- Clicking or expanding a block card in the Sulu Admin form dispatches
5. Preset Gallery, Wireframe Registry & Reusable Block Actions
The block_suite_advanced.xml form includes <property name="preset" type="preset_selection">, which renders inside the Advanced tab and provides:
- Active Preset Status Badge & Clear Action: Displays the currently active preset name and source badge (
[CMS]or[YAML]) with a one-click clear button. - Visual Preset Gallery (
Browse Presets): Opens a searchable modal gallery of all YAML and CMS presets for the block type; selecting a preset immediately applies allprops/*defaults across tabs via atomicformStore.changeMultiple(). - Apply Content & Styles: Populates both content fields (
title,content, nested items) andprops/*from the active preset. - Save as Preset: Captures the current block's live form state and saves/publishes a
block_presetsnippet in Sulu CMS. - Save as Reusable Block: Saves and publishes the current block as a shared
reusable_blocksnippet and optionally replaces the current block in-place with a{ type: 'reusable', snippet: '<uuid>' }reference.
6. Scoped .el-element CSS & Sanitized Custom Attributes
In the Advanced tab:
- Custom Attributes (
props.attributes): Enter onekey="value"attribute per line (data-hs-overlay="#modal",aria-label="Hero Banner"). Inline event handlers (onclick,onload, etc.) and dangerous URI schemes are stripped byAttributeSanitizer. - Scoped CSS (
props.css): Write rules using.el-element(or bare declarations).DefaultAttributesTransformerassigns or reuses_id, rewrites.el-elementto#<_id>viaCssScoper, strips any<script>/<style>tags or dangerous CSS expressions, andBlockRendererprepends<style>...</style>to the rendered block output.
7. Reusable Global Blocks, Page Layouts & Fallback Chains (|)
ReusableBlockTransformer: Expandsreusableblocks at priority110(beforeDynamicContentTransformerat100andDefaultAttributesTransformerat90) so expanded blocks undergo dynamic content hydration and attribute normalization uniformly. When areusableblock is expanded inside a nested slot/column, registeredNestedBlockFlattenerInterfaceservices unwrap container blocks into leaf blocks.- HTTP Cache Reference Tracking & Invalidation: When a
reusable_blockorpage_layoutsnippet is resolved or expanded,SingleReusableBlockSelectionPropertyResolver,ReusableBlockTransformer, andPageLayoutProviderregister the snippet UUID in Sulu'sReferenceStoreInterface(snippets).ReusableBlockCacheInvalidationSubscriberlistens to snippet modification, workflow transition, and removal events to invalidate Sulu HTTP cache tags and references. - Pipe-Separated Layout Fallback Chains:
layout_render()andlayout_load()support|-delimited fallback chains (e.g.homepage|default,article|default,search|default,error|default):{{ layout_render(content.layout|default('search|default'), _context) }}
Writing a Framework Package
You can build a standalone UI framework bundle (e.g., Preline UI, Bootstrap, UIkit) or a base + theme inheritance hierarchy on top of sulu-block-suite-bundle without modifying Core.
1. Prepend Block Directory & Prioritized Template Namespace
In your framework bundle's DI Extension::prepend(ContainerBuilder $container):
public function prepend(ContainerBuilder $container): void { if ($container->hasExtension('sulu_admin')) { $container->prependExtensionConfig('sulu_admin', [ 'templates' => [ 'block' => [ 'directories' => [ 'my_uikit_blocks' => \dirname(__DIR__, 2) . '/config/templates/blocks', ], ], ], ]); } if ($container->hasExtension('twig')) { $container->prependExtensionConfig('twig', [ 'paths' => [ \dirname(__DIR__, 2) . '/templates' => 'MyUikitBlocks', ], ]); } if ($container->hasExtension('sulu_block_suite')) { $container->prependExtensionConfig('sulu_block_suite', [ 'rendering' => [ 'template_namespaces' => [ '@MyUikitBlocks/blocks' => 200, ], ], ]); } }
2. Include Framework Styling Tabs via Native xi:include
Core does not inject opinionated styling fields. Instead, your framework bundle owns its styling tab (<section name="general">) and includes it in each block XML via standard xi:include:
<?xml version="1.0"?> <template xmlns="http://schemas.sulu.io/template/template" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xi="http://www.w3.org/2001/XInclude" xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd"> <key>hero_banner</key> <meta> <title lang="en">Hero Banner</title> </meta> <properties> <section name="content"> <!-- Root-level content fields with optional inline <param type="collection"> option groups --> </section> <section name="settings"> <!-- Block-specific settings (automatically namespaced under props/*) --> </section> <xi:include href="../../forms/general/general_justify.xml" xpointer="xmlns(sulu=http://schemas.sulu.io/template/template)xpointer(/sulu:form/sulu:properties/sulu:*)"/> </properties> </template>
Composing Blocks in a Project
Projects control which blocks are available in each template using Sulu-native <types><type ref="..."/></types> lists and standard xi:include fragments:
<!-- config/templates/includes/content_block_types.xml --> <?xml version="1.0"?> <types xmlns="http://schemas.sulu.io/template/template"> <type ref="hero_banner"/> <type ref="text"/> <type ref="image"/> <type ref="card"/> <type ref="grid"/> <type ref="reusable"/> </types>
Include this list in your page templates, article templates, reusable_block.xml, and block_preset.xml:
<!-- config/templates/snippets/reusable_block.xml --> <property name="blocks" type="block" default-type="text"> <types> <type ref="section_break"/> <type ref="row"/> <xi:include href="../includes/content_block_types.xml" xpointer="xmlns(sulu=http://schemas.sulu.io/template/template)xpointer(/sulu:types/sulu:*)"/> </types> </property>
Sulu MCP & AI Integration
SuluBlockSuiteBundle includes zero-dependency AI/MCP services and auto-registers MCP tools when mcp/sdk (sulu/mcp-bundle or symfony/ai-mate) is present:
Totoglu\SuluBlockSuiteBundle\Ai\BlockSuiteSchemaInspector:listBlocks(string $locale = 'en'): Compact catalog of registered global blocks (content_fields,field_option_keys,settings_props,preset_count,presets).inspectBlock(string $blockType, string $locale = 'en', bool $includeAdvanced = false, bool $includeDynamic = false): Progressive-disclosure block schema returningcontent_fields(with inline<params>option groups),settings_props, andpresetswhile omitting the ~26advanced/dynamic_contentfields unless requested.listPresets(string $blockType, string $locale = 'en', ?string $query = null): Lists or searches YAML and Snippet visual presets (id,title,description,props,values).isTranslatableField(string $fieldName): Distinguishes translatable content fields from technicalprops/*(props/id,props/class,props/custom_css,props/attributes, inline enum options).
Totoglu\SuluBlockSuiteBundle\Ai\BlockSuitePayloadComposer:compose(string $blockType, array $content = [], array $props = [], ?string $preset = null, string $locale = 'en'): Applies presetvalues/props, validates inline option group and select values, and synchronizes nestedpropsand flatprops/*keys forsulu_block_add,sulu_block_update, andsulu_page_create.preserveTechnicalProps(array $sourceBlockData, array $translatedBlockData): Restores non-translatable technicalpropsandprops/*keys after full-content AI translation.
Totoglu\SuluBlockSuiteBundle\Mcp\BlockSuiteMcpTools(auto-taggedmcp.toolwhenMcp\Capability\Attribute\McpToolexists):sulu_block_suite_blockssulu_block_suite_presetssulu_block_suite_compose_block
- Symfony AI Mate (
extra.ai-mate): BundlesINSTRUCTIONS.mdandsrc/Mcpscan directory incomposer.json.
Internationalization (i18n)
Complete admin (translations/admin.<locale>.json) and backend (translations/sulu_block_suite.<locale>.yaml) translations (sulu_block_suite.*) are bundled for 6 languages: en, tr, de, ru, es, and fr.
Verification & Quality Assurance
# Run PHPUnit 12 test suite (including Architecture/FrameworkAgnosticTest) vendor/bin/phpunit -c packages/sulu-block-suite-bundle/phpunit.xml.dist # Run PHPStan static analysis at level max vendor/bin/phpstan analyse --configuration=packages/sulu-block-suite-bundle/phpstan.neon.dist # Check PHP-CS-Fixer formatting vendor/bin/php-cs-fixer fix --config=packages/sulu-block-suite-bundle/.php-cs-fixer.dist.php --dry-run --diff
License
This bundle is open-source software licensed under the MIT License.