Extending Hyvä CMS for Other Content Types
Advanced topic
Extending Hyvä CMS for other content types should only be attempted by experienced developers. Be prepared to debug cases specific to your content type integration.
Susceptible to breaking changes
This integration area may be particularly susceptible to breaking changes in future releases. When implementing custom content type integrations, be prepared to update your code when upgrading to new versions of Hyvä CMS.
This page is a general reference for extending Hyvä CMS for other content types, such as blog posts or landing pages from third-party modules. A lot of custom logic will be specific to your own implementation, but the steps below give you a good starting point. See the Hyva_CmsMagento module for a concrete example.
Step 1: Understanding Core Concepts
The Hyvä CMS Liveview editor provides an alternative content creation and editing experience to Magento's Page Builder. Hyvä CMS can be enabled:
- Per individual content item.
- For all content of a specific type (e.g., all CMS pages).
- For all content types.
The editor identifies content through two key parameters:
type: Identifies the type of content being edited (e.g.,cms_page,cms_block).id: The unique identifier of the specific content item.
When creating new content, id is set to 0, signalling that a new record for the specified entity type should be created.
Hyvä CMS stores both a draft and a published version of the content.
Step 2: Name and Create Your Module
Choose a unique entity_type identifier for your content type. Following convention, this identifier should match the name of the database table you're extending Liveview for, for example cms_page or cms_block.
Create a dedicated module to implement your Liveview content type integration. To follow naming conventions, it should be named Vendor_HyvaCms[ContentTypeName] (e.g., Namespace_HyvaCmsBlog).
Step 3: Setup Database Tables, Models, and Interfaces
Add the necessary db_schema.xml, db_schema_whitelist.json, Model, and API Interface classes to your module. See etc/db_schema.xml in the Hyva_CmsMagento module for an example.
A table is required to store the main Hyvä CMS content and its properties, along with another for version history (e.g., hyva_commerce_cms_block and hyva_commerce_cms_page_version_history).
Tables should follow the column naming conventions used in Hyva_CmsMagento, especially for version history tables.
Step 4: Admin Settings and UI
4.1 System Config
Add a system configuration setting to enable or disable Hyvä CMS Liveview for your content type, using the config path hyva_commerce_cms/[your_content_type]/enabled. This setting controls the global availability of Liveview for the content type.
4.2 Content Listing Page
On your content listing page, when Liveview is enabled:
- Add a Yes/No column for the is_liveview_enabled status.
- Optionally add an action to open new content in the Liveview editor.
4.3 Content Form
On your content form, when Liveview is enabled:
- Add a toggle for
is_liveview_enabled. - Add a button/link to open the Liveview editor.
- (Optional) Add a preview URL field if implementing custom previews.
4.4 Automatic CSP Frame Policies
To enable Automatic CSP Frame Policies for multi-domain setups without manual configuration, add new routes that display the preview (e.g. the content form from section 4.3) to your module's etc/adminhtml/di.xml file:
<type name="Hyva\CmsLiveviewEditor\Model\Security\IsValidAdminPreviewRequest">
<arguments>
<argument name="allowedRoutes" xsi:type="array">
<item name="blog/page/edit" xsi:type="string">blog/page/edit</item>
</argument>
</arguments>
</type>
An example of this can be found in:
Note
Registering these routes is required to support Automatic CSP Frame Policies for multi-domain setups without manual configuration. Otherwise, Magento instances running their admin area in CSP strict mode (as opposed to report-only mode) won't be able to display the preview.
4.5 Save Logic
Add logic to update the data for your Liveview entity once the form is saved. See Hyva_CmsMagento::Observer/CmsPageSaveAfter.php for an example.
Note
After completing this step, you will see the following error on the content form page and when you try to open the Liveview editor. The next step fixes it.
No content provider found for type: [name of your content type]
Step 5: Create a Liveview Provider
Create a provider class that implements Hyva\CmsLiveviewEditor\Api\ProviderInterface. This class handles the content type-specific logic for Hyvä CMS. Register your provider in etc/di.xml to make it available to the provider pool:
<type name="Hyva\CmsLiveviewEditor\Model\ProviderPool">
<arguments>
<argument name="contentProviders" xsi:type="array">
<item name="cms_page" xsi:type="object">Hyva\CmsMagento\Model\LiveviewCmsPageProvider</item>
</argument>
</arguments>
</type>
It is worth reviewing how the ProviderInterface is implemented in Hyva_CmsMagento, as much of the logic may be adapted for your use:
Hyva\CmsMagento\Model\Provider\CmsBlockProviderHyva\CmsMagento\Model\Provider\CmsPageProvider
Tailwind CSS Provider Methods
ProviderInterface includes methods that let your content type participate in Tailwind CSS generation. The most important is getScopeSelector(), which returns the CSS scope selector applied to your entity's rendered content:
Return a unique, dot-prefixed selector following the convention .hcms-{your-type}-{entityId}. Hyvä CMS wraps your rendered content in this selector so generated styles stay scoped to the entity.
getScopeSelector() is required (breaking change)
getScopeSelector() is a required ProviderInterface method. Custom content-type providers written for older Hyvä CMS versions must add it before they will compile. The previous getTailwindClassPrefix() method is deprecated and no longer used for style isolation, but it remains in the interface, so keep it implemented (a simple stub is fine).
Implement the remaining Tailwind methods (isTailwindJitEnabled(), getTailwindTableName(), and the column-name getters) to point at a per-entity, per-theme, per-edition CSS table. A provider that does not use Tailwind JIT can return false from isTailwindJitEnabled() and stub the others.
Step 6: Add Settings to the Liveview Editor
Core settings handle non-content-related properties and enable the creation of new entities directly within the Hyvä CMS Liveview editor.
In liveview_editor.xml, add a core-settings.[your content entity type] block to the core-settings block. It will require a ViewModel/Block to get form data, as well as an AJAX controller and model to handle saving the form data. Include both frontend and backend form validation.
Use liveview_editor.xml instead of liveview_editor_index.xml to ensure compatibility with all the Liveview editor functionality, e.g. scheduling.
Target a single entity type with its own layout handle
The editor applies a {entity_type}_liveview_editor layout handle in addition to the shared liveview_editor handle. Use it for layout updates that should only apply when editing your content type. For an entity type of blog_post, the handle is blog_post_liveview_editor.
For example:
<referenceBlock name="core-settings">
<block name="core-settings.cms_page" template="Hyva_CmsMagento::core-settings/page-form.phtml"/>
</referenceBlock>
Your block's template should dispatch two JavaScript events:
-
On initialization, set the content label:
-
After saving the entity, notify listeners:
window.dispatchEvent(new CustomEvent('after-content-entity-saved', { detail: { entity_id: this.entityId, entity_type: this.entityType, is_active: this.formData.is_active ? 1 : 0, is_tailwindcss_jit_enabled: this.formData.is_tailwindcss_jit_enabled ? 1 : 0, disabled_notice_message: this.disabledNoticeMessage } }));
Step 7: Handle Liveview Display Logic
Your Liveview provider handles most of the Hyvä CMS specific logic. In your own module, however, you need to add logic to determine when to show Hyvä CMS content instead of Page Builder content, based on system configuration and content-specific settings.
In Hyva_CmsMagento this is handled with plugin classes:
<type name="Magento\Cms\Api\Data\PageInterface">
<plugin name="Hyva_CmsMagento::change_cms_page_content" type="Hyva\CmsMagento\Plugin\Model\Page" />
</type>
<type name="Magento\Cms\Api\Data\BlockInterface">
<plugin name="Hyva_CmsMagento::change_cms_block_content" type="Hyva\CmsMagento\Plugin\Model\Block" />
</type>
Step 8: Add Navigator Support
The navigator slideout provides quick access to browse and create content items. To add support for your content type:
8.1 Create List Controller
Create an admin controller that returns a JSON list of your entities. The controller should:
- Accept
pageSizeandcurrentPageparameters - Support search filtering by identifier/title
- Use
StoreDataProviderto format store data - Generate
liveview_urlwith format:liveview/editor/index/type/{entity_type}/id/{id}/ - Return JSON:
{ total_count: int, items: array }where each item contains:id,value,name,liveview_url,stores
Reference implementation: local-src/module-cms/src/liveview-editor/Controller/Adminhtml/Link/CmsBlocks.php
8.2 Create Listings Template
Create a template at view/adminhtml/templates/listings/{entity_type}.phtml that:
- Uses Alpine.js to fetch and display entities grouped by store
- Listens for the
@listings-slideout-tab.windowevent withevent.detail.tab === '{entity_type}'to trigger data loading - Implements search functionality
- Includes a "Create New" link pointing to
liveview/editor/index/type/{entity_type}/id/0/
Reference implementation: local-src/module-cms/src/magento-cms/view/adminhtml/templates/listings/block.phtml
8.3 Register in Layout
Register your listings template in view/adminhtml/layout/liveview_editor.xml.
Reference implementation: local-src/module-cms/src/magento-cms/view/adminhtml/layout/liveview_editor.xml
Step 9: (Optional) Add Scheduling Support
If you want your custom content type to support scheduling functionality, you'll need to implement additional interfaces and handle scheduled content rendering:
Required Changes for Scheduling
-
Provider Interface Updates: Include the
scheduledItemIdparameter in yourHyva\CmsLiveviewEditor\Api\ProviderInterface::getStoreContentData()method implementation to support scheduling. -
Implement ScheduleProviderInterface: Your content type provider must implement
Hyva\CmsScheduling\Api\ScheduleProviderInterfaceto enable scheduling capabilities. -
Handle Scheduled Content URL Parameter: Update your provider to handle the
scheduled_itemURL parameter when rendering scheduled content. SeeHyva\CmsMagento\Plugin\Model\Pagefor implementation reference.
Step 10: (Optional) Extend with Custom Components
To add custom components for your content type, see Creating Components.
Restricting which components can be added as roots
Components can declare context_flags in their JSON declaration. When your entity emits the init-content-properties event, pass allowed_root_component_context_flags to allow only components carrying a matching flag as top-level (root) components for your content type. Feature modules can also push flags onto Alpine.store('global').disallowedRootComponentContextFlags to hide flagged components from the picker. This is how contexts such as menus or email templates limit their root component choices.
Step 11: Testing and Validation
As this is an advanced integration, thorough testing is critical:
-
Test your implementation thoroughly, including:
- Content creation and editing
- Enabled vs disabled content
- Multi store view support
- Draft and published workflow
- Version history
- Form validation
- Content display logic
-
Be prepared to debug cases specific to your content type
Step 12: (Optional) Register with Import/Export
The Import/Export feature is handler-driven, so any module can make its own content type importable and exportable. There is no need to touch the Import/Export module itself, and a registered handler appears automatically in the export picker, JSON tools, and ZIP packages.
How the Content Type Handler System Works
Each content type is backed by a class implementing the @api interface Hyva\CmsImportExport\Model\ContentType\ContentTypeHandlerInterface. Handlers are registered in a single shared pool, Hyva\CmsImportExport\Model\ContentType\HandlerPool, keyed by a type string such as cms_page or template. The import/export module ships handlers for cms_page, cms_block, instance_component, product_attribute, and category_attribute; the Hyva_CmsTemplate module contributes template and snippet the same way an integrator would.
Most handlers extend the convenience base class Hyva\CmsImportExport\Model\ContentType\AbstractContentTypeHandler, which provides sensible defaults (for example isExportable() returns true) so you only implement what differs.
The Content Type Handler Interface Methods
ContentTypeHandlerInterface declares:
| Method | Purpose |
|---|---|
getLabel(): string |
Human-readable label shown in the Import/Export UI. |
getEntityList(string $search = '', int $limit = 50): array |
Selectable entities. Each item is ['id', 'label', 'identifier'], plus an optional 'stores' array for store-scoped types. |
getEntityData(int $entityId): array |
Portable entity metadata written to the package (use portable identifiers, not auto-increment IDs). |
getProviderType(): ?string |
Liveview provider key (e.g. cms_page), or null for types that do not use the liveview provider. |
getExportFilename(array $entityData): string |
Filename-safe identifier used for ZIP naming. |
isExportable(int $entityId): bool |
Whether the entity may be exported. Default true; override to hide entities without portable content. |
resolveEntityId(array $entityData): ?int |
Resolve a previously exported entity back to a local ID, or null if it does not exist here. |
createEntity(array $entityData): int |
Create a copy for Import as new and return its ID. |
saveContent(int $entityId, array $draftContent, array $publishedContent): void |
Persist liveview content into an existing entity. |
updateEntity(int $entityId, array $entityData): void |
Persist entity-level metadata after content is saved. Default: no-op. |
normalizeContentForExport(array $content): array |
Translate local IDs to portable keys before export. Default: no-op. |
normalizeContentForImport(array $content): array |
Translate portable keys back to local IDs on import. Default: no-op. |
getImportWarnings(): array |
Warnings collected by the last normalizeContentForImport() call (e.g. unresolved references). |
Controlling Which Entities Are Exportable
The isExportable() method gates both the UI and the server. The CMS page and block handlers override it to return true only when the entity carries Hyvä CMS content, which is why non-Hyvä pages and blocks never appear in the picker and are rejected if requested directly. Override it whenever an entity can exist without portable content.
Registering a Content Type Handler
Add your handler to the shared HandlerPool in your module's etc/di.xml. Magento merges the array, so you only declare your own entries:
<type name="Hyva\CmsImportExport\Model\ContentType\HandlerPool">
<arguments>
<argument name="handlers" xsi:type="array">
<item name="my_type" xsi:type="object">My\Module\Model\ImportExport\MyHandler</item>
</argument>
</arguments>
</type>
Declare a module sequence on Hyva_CmsImportExport in your etc/module.xml so the pool is available, then implement the handler. Extending AbstractContentTypeHandler keeps it small:
namespace My\Module\Model\ImportExport;
use Hyva\CmsImportExport\Model\ContentType\AbstractContentTypeHandler;
class MyHandler extends AbstractContentTypeHandler
{
public function getLabel(): string
{
return (string) __('My Content Type');
}
public function getEntityList(string $search = '', int $limit = 50): array
{
// return [['id' => 1, 'label' => '...', 'identifier' => 'portable-key'], ...]
}
public function getEntityData(int $entityId): array
{
// portable metadata, keyed by a stable identifier rather than the numeric ID
}
public function resolveEntityId(array $entityData): ?int
{
// look up the local entity by its portable identifier, or null
}
public function createEntity(array $entityData): int
{
// create a copy for "Import as new" and return its ID
}
}
The template and snippet handlers in Hyva_CmsTemplate (Hyva\CmsTemplate\Model\ImportExport\TemplateHandler and SnippetHandler) are concise, real-world references: each keys its portable identifier off the template or snippet name and persists a single content blob through the template provider.
The Import/Export is driven by adminhtml controllers rather than a public REST endpoint, so a handler is reached through the editor UI. Registering it in the pool is all that is required for it to participate in both ZIP and JSON transfers.