From 07e9d2b5a26f6f75545b97b05aed07008b8e1712 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:38:36 +0200 Subject: [PATCH 01/23] Headless Page Builder (SaaS) --- code_samples/page/headless/page.html | 785 ++++++++++++++++++ .../img/headless-saas-siteaccess-config.png | Bin 0 -> 66430 bytes .../pages/headless_page_builder.md | 551 ++++++++++++ docs/content_management/pages/pages.md | 1 + mkdocs.yml | 1 + 5 files changed, 1338 insertions(+) create mode 100644 code_samples/page/headless/page.html create mode 100644 docs/content_management/img/headless-saas-siteaccess-config.png create mode 100644 docs/content_management/pages/headless_page_builder.md diff --git a/code_samples/page/headless/page.html b/code_samples/page/headless/page.html new file mode 100644 index 0000000000..ba0bd3b6ed --- /dev/null +++ b/code_samples/page/headless/page.html @@ -0,0 +1,785 @@ + + + + + + Headless Page Builder test + + + + + +
Header
+ +
+ + +
+ + + + + + + + diff --git a/docs/content_management/img/headless-saas-siteaccess-config.png b/docs/content_management/img/headless-saas-siteaccess-config.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed5009f4c1c5bf40b49303aaea74a0c467e7a9f GIT binary patch literal 66430 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 D3;k1O literal 0 HcmV?d00001 diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md new file mode 100644 index 0000000000..4beea448c8 --- /dev/null +++ b/docs/content_management/pages/headless_page_builder.md @@ -0,0 +1,551 @@ +--- +description: Preview in Page Builder using your frontend. +edition: experience +month_change: true +--- + +# Headless frontend preview in Page Builder + +The Page Builder can preview landing pages rendered by your frontend application. + +You provide a single URL for the frontend page and let it communicate with the Page Builder using the JavaScript message API. + +## Configuration + +- Navigate to **Administration** > **SiteAccess Configuration** +- Choose a SiteAccess you want this set up a preview for. None if you want to set up a default preview for SiteAccesses not having a specific preview. +- Then, click **Headless** + +Below the description, enable the **Headless mode**, fill in the **Page Builder preview URL**, and **Save** the configuration. + +![Headless mode enabled with Page Builder preview URL](headless-saas-siteaccess-config.png) + +## Communication protocol + +The frontend resource targeted by `base_url` is loaded by the Page Builder when editing a content having a Landing page field "Edit in the headless Page Builder" enabled. +This resource must follow a protocol to communicate with the Page Builder from the iframe is loaded in. +This protocol is based on the JavaScript message API. + +The Page Builder sends messages to the framed frontend resource. +They can be received by listening the [message event](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/message_event). + +The frontend resource sends back messages to the Page Builder. +They can be sent using the [`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) method. +The target origin of those messages must be the Page Builder's origin. + +```js +const pbOrigin = 'https://my-company.cohesivo.app'; +window.parent.postMessage(message, pbOrigin); +``` + +Those messages are JS objects with the following structure: + +```js +let message = { + type: 'PREFIX:MESSAGE_TYPE', + data: {} +}; +``` + +The `PREFIX` sort message types by their sender. +`PB:` for messages sent by the Page Builder, `APP:` for messages sent by the frontend resource. + +The data depends on the message type. + +Message types are then sorted by capabilities. +So the frontend can declare which capabilities it supports and the Page Builder can refrain from sending or expecting unsupported messages. + +### Message types and capabilities + +The handshake and core messages are mandatory and not related to an optional capability. + +| Capability | Message type | Description | +|--------------------|---------------------------------------------------------------------------------------------|--------------------------------------| +| (handshake) | [`APP:INITIALIZED`](#communication-initialization) | Establish protocol and capabilities. | +| (handshake) | [`PB:INIT_MODE`](#communication-initialization) | Confirm protocol and draft info. | +| (core) | [`PB:UPDATE_FIELD_DATA`](#on-field-update) | Send updated field data. | +| (core) | [`PB:DISPATCH_EVENT`](#re-dispatching-events) | Re-dispatch a frontend event. | +| `blocks.dnd` | [`PB:DRAG_START_PREVIEW`](#drag-and-drop-blocksdnd) | Existing block drag started. | +| `blocks.dnd` | [`PB:DRAG_OVER`](#drag-and-drop-blocksdnd) | Mouse position during drag. | +| `blocks.dnd` | [`PB:DRAG_END_PREVIEW`](#drag-and-drop-blocksdnd) | Existing block drag ended. | +| `blocks.dnd` | [`PB:DROP`](#drag-and-drop-blocksdnd) | Drop notification. | +| `blocks.dnd` | [`APP:DROP_RESPONSE`](#drag-and-drop-blocksdnd) | Report where the block was dropped. | +| `blocks.dnd` | [`PB:SCROLL_BY`](#drag-and-drop-blocksdnd) | Scroll the preview. | +| `blocks.geometry` | [`APP:POSITIONS_UPDATE`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Report block positions. | +| `blocks.geometry` | [`APP:SCROLL_END`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Scroll ended. | +| `blocks.remove` | [`PB:BLOCK_REMOVE`](#block-removal-blocksremove) | Remove a block. | +| `blocks.remove` | [`APP:BLOCK_REMOVE_RESPONSE`](#block-removal-blocksremove) | Confirm removal. | +| `blocks.remove` | [`APP:BLOCK_REMOVE_REQUEST`](#block-removal-blocksremove) | Request block removal. | +| `blocks.reveal` | [`PB:SCROLL_INTO_BLOCK`](#block-reveal-blocksreveal) | Scroll a block into view. | +| `blocks.select` | `APP:BLOCK_CLICKED` | Block clicked. | +| `pointer.tracking` | [`APP:MOUSE_POSITION`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Report mouse position. | + +### Communication initialization + +First, the frontend send an initialization message to the Page Builder, indicating which version of the protocol and which capabilities are supported: + +```js +const initializedMessage = { + type: 'APP:INITIALIZED', + data: { + protocol: { + supported: [1] + }, + capabilities: [ + 'blocks.dnd', + 'blocks.geometry', + 'pointer.tracking', + // … + ], + } +}; +``` + +The Page Builder replies with a confirmation message. +The initialization message might be sent several times until the Page Builder replies to it. + +This confirmation `data` contains: + +- the actual version of the protocol used (`protocol.version`) and the other supported versions (`protocol.supported`) +- a list of all available capabilities (`capabilities`) +- a list of the existing block types, their attributes, and their configuration (`blocksConfig`) - It contains all the block types but mark as not visible the blocks not available for this field. +- information about the actually edited content draft (`intentParameters`) +- the current value of the Landing page field being edited (`fieldValue`) including the layout, zones, and blocks. +- a block-ID-to-name mapping (`blocksIdMap`) +- a list of translations for the frontend to use (`translations`) + +```json +{ + "type": "PB:INIT_MODE", + "data": { + "protocol": { + "version": 1, + "supported": [ + 1 + ] + }, + "capabilities": [ + "blocks.dnd", + "blocks.geometry", + "blocks.remove", + "blocks.reveal", + "blocks.select", + "pointer.tracking", + "preview.params" + ], + "blocksConfig": [ + { + "type": "block_type", + "name": "Block type name", + "category": "Block category", + "thumbnail": "path/to/block/thumbnail.file", + "visible": true, + "views": { + "default": { + "name": "Default" + } + }, + "attributes": [ + { + "id": "attribute_id", + "name": "Attribute name", + "type": "attribute_type", + "value": null, + "constraints": { + "not_blank": { + "message": "Please select…" + } + } + } + ] + } + ], + "intentParameters": { + "locationId": "2", + "contentId": 52, + "versionNo": 6, + "languageCode": "eng-GB" + }, + "fieldValue": { + "layout": "ibexa_fieldtype_page.layouts..identifier", + "zones": [ + { + "id": "123", + "name": "ibexa_fieldtype_page.layouts..zones..name", + "blocks": [ + { + "visible": true, + "id": "456", + "type": "block_type", + "name": "Block name", + "view": "default", + "class": null, + "style": null, + "compiled": "", + "since": null, + "till": null, + "attributes": [ + { + "id": "789", + "name": "attribute_name", + "value": "…" + } + ] + } + ] + } + ] + }, + "blocksIdMap": { + "456": "Block name" + }, + "translations": { + "block.attribute.invalid": "%name% is invalid", + "block.no_availability.content": "You have to delete it to publish", + "block.no_availability.delete": "Delete", + "block.no_availability.title": "This element is not available in this page", + "block.unknown.type": "Unknown block type: %type% (block name: %name%)", + "drag.drop.blocks.here": "Drag and drop blocks here", + "structure.drop.zone": "Drop zone %number%" + } + } +} +``` + +### On field update + +The `PB:UPDATE_FIELD_DATA` message is sent from the Page Builder to the frontend resource when the Landing page field value is updated. + +Its data contains the new value of the field with the following structure: + +- Always, the current value of the Landing page field being edited (`fieldValue`) including the layout, zones, and blocks. +
For example, only `fieldValue` is sent when manipulating the [timeline]([[= user_doc =]]/content_management/schedule_publishing/#timeline)![](page_builder_toolbartimelinetoggler.png){style="display:inline;width:27px;vertical-align:middle;"}. +- Optionally, a list of the existing block types, their attributes, and their configuration (`blocksConfig`) +- Optionally, a block-ID-to-name mapping (`blocksIdMap`) +- Optionally, a list of IDs from the new blocks that have been added (`highlightedBlockIds`) + +```json +{ + "type": "PB:UPDATE_FIELD_DATA", + "data": { + "fieldValue": { + "layout": "…", + "zones": [] + }, + "blocksConfig": [], + "blocksIdMap": {}, + "highlightedBlockIds": [] + } +} +``` + +### Re-dispatching events + +The `PB:DISPATCH_EVENT` message is sent from the Page Builder to the frontend resource for being re-dispatched there as a custom event. +Its data contains the name of the event to dispatch (`eventName`) and the data to pass to the event detail (`eventData`). + +```js +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'PB:DISPATCH_EVENT': + window.dispatchEvent(new CustomEvent(messageEvent.data.data.eventName, { detail: messageEvent.data.data.eventData })); + break; + } +}); +``` + +#### Available events + +- `ibexa-active-block-clicked`: It confirms `APP:BLOCK_CLICKED` have been received. It has no data. +- `ibexa-post-update-blocks-preview`: It's sent when the timeline is used. + - `fieldValue`: The same as in [`PB:UPDATE_FIELD_DATA`](#on-field-update) + - `blockIds`: A list of all the block IDs + - `blocksMaps`: A map of block config per block ID + +```js +window.addEventListener('ibexa-post-update-blocks-preview', (customEvent) => { + setLayout(customEvent.details.fieldValue.layout); + renderZones(customEvent.details.fieldValue.zones); +}); +``` + +### Geometry and pointer tracking (`blocks.geometry` and `pointer.tracking`) + +The pointer tracking and the geometry helps the Page Builder to position the block editing menus above the frontend preview. +Such menu is a `.c-pb-headless-preview-menu` element positioned by the Page Builder from its DOM above the preview `iframe`. + +`APP:MOUSE_POSITION` message is sent from the frontend preview to the Page Builder to declare the actual position of the mouse. + +```js +window.addEventListener('mousemove', (mouseEvent) => { + window.parent.postMessage({ + type: 'APP:MOUSE_POSITION', + data: { + x: mouseEvent.clientX, + y: mouseEvent.clientY, + }, + }, pbOrigin); +}); +``` + +`APP:POSITIONS_UPDATE` message is sent from the frontend preview to the Page Builder to declare the actual position of the blocks. +Its data contains a list of objects with block IDs, their positions, and dimensions in the frontend preview. +This format is close to [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) method. + +```js +const positionsUpdate = () => { + let blocks = []; + for (const blockElement of document.getElementsByClassName('landing-page__block')) { + const blockId = blockElement.dataset.ibexaBlockId; + const blockRect = blockElement.getBoundingClientRect(); + blocks.push({ + id: blockId, + top: blockRect.top, + left: blockRect.left, + right: blockRect.right, + bottom: blockRect.bottom, + width: blockRect.width, + height: blockRect.height, + }); + } + window.parent.postMessage({ + type: 'APP:POSITIONS_UPDATE', + data: { + blocks: blocks, + }, + }, pbOrigin); +}; +``` + +This message should be sent each time the positions of the blocks change. +It should be sent after updating the blocks, like in response to [`PB:UPDATE_FIELD_DATA`](#on-field-update). +It should be sent after scrolling or resizing. + +`APP:SCROLL_END` message is sent from the frontend preview to the Page Builder to notify that a scroll operation has ended. +It has no data. + +```js +window.addEventListener('scrollend', (event) => { + window.parent.postMessage({ + type: 'APP:SCROLL_END', + }, pbOrigin); + positionsUpdate(); +}); +window.addEventListener('resize', (event) => { + positionsUpdate(); +}); +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'PB:UPDATE_FIELD_DATA': + setLayout(messageEvent.data.data.fieldValue.layout); + renderZones(messageEvent.data.data.fieldValue.zones); + positionsUpdate(); + break; + } +}); +``` + +### Drag and drop (`blocks.dnd`) + +`PB:DRAG_OVER` message is sent from the Page Builder to the frontend preview to give the position of the mouse while a (new or existing) block is dragged. + +!!! tip "Mouse tracking" + + - `APP:MOUSE_POSITION` helps the Page Builder to know where the mouse is when moved over the preview. It's associated to `APP:POSITIONS_UPDATE` to know if a preview block is hovered. + - `PB:DRAG_OVER` helps the frontend to know where the mouse is when a block is dragged over the preview. + +`PB:DRAG_START_PREVIEW` and `PB:DRAG_END_PREVIEW` are sent at the beginning and at the end of a drag operation on an existing block in the frontend preview. +Its data contains the ID of the block being dragged (`blockId`). + +```js +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'PB:DRAG_START_PREVIEW': + document.querySelector(`[data-ibexa-block-id="${messageEvent.data.data.blockId}"]`).classList.add('c-pb-block-preview--is-dragging-out'); + break; + case 'PB:DRAG_END_PREVIEW': + document.querySelector(`[data-ibexa-block-id="${messageEvent.data.data.blockId}"]`).classList.remove('c-pb-block-preview--is-dragging-out'); + break; + } +}); +``` + +`PB:DROP` message is sent from the Page Builder to the frontend preview to notify that a block has been dropped. +It has no data. Combined with the last `PB:DRAG_OVER` message, the frontend can determine where the block has been dropped. + +`APP:DROP_RESPONSE` message is sent from the frontend preview to the Page Builder to tell where the block has been dropped. + +Its data contains: + +- the ID of the zone where the block has been dropped (`zoneId`) +- the ID of a block that is now below the dropped block (`nextBlockId`) if the dropped block isn't the last one of the zone. + +In the following example, `targetBlockId` value is the ID of a block the dropped block was dropped on or just before, so the dragged block takes its place and move it below, or `null` when dropped at the bottom of the zone. + +```js +const dropResponseMessage = { + type: 'APP:DROP_RESPONSE', + data: { + zoneId: zoneId, + nextBlockId: targetBlockId, + } +}; +``` + +After a drop `APP:DROP_RESPONSE` from the frontend, the next `PB:UPDATE_FIELD_DATA` message from the Page Builder contains the dropped block ID in its `highlightedBlockIds` array (`messageEvent.data.data.highlightedBlockIds`). + +`PB:SCROLL_BY` message is sent from the Page Builder when a block is dragged near a border of the preview which needs to be scrolled. +Its data contains the `top` or `left` amount to scroll by. + +```js +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'PB:SCROLL_BY': + window.scrollBy(messageEvent.data.data); + break; + } +}); +``` + +See [Geometry (`blocks.geometry`)](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking)'s `APP:SCROLL_END` message to declare the end of the scroll operation +and `APP:POSITIONS_UPDATE` message to update the blocks positions. + +### Block reveal (`blocks.reveal`) + +`PB:SCROLL_INTO_BLOCK` is send by the Page Builder to the frontend preview to request that a block is scrolled into view. +Its data contains the ID of the block to scroll into view (`blockId`). +It can be used with the [`scrollIntoView()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) method. + +```js +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'PB:SCROLL_INTO_BLOCK': + const blockElementToScrollInto = document.querySelector(`[data-ibexa-block-id="${messageEvent.data.data.blockId}"]`);; + blockElementToScrollInto.scrollIntoView({ behavior: 'smooth', block: 'center' }); + break; + } +}); +``` + +### Block removal (`blocks.remove`) + +`PB:BLOCK_REMOVE` message is sent from the Page Builder to the frontend preview to notify that a block should be removed, for example, from the Structure view. +Its data contains the ID of the block to remove (`blockId`). + +`APP:BLOCK_REMOVE_RESPONSE` message is sent from the frontend preview to the Page Builder to confirm that the block has been removed as requested by `PB:BLOCK_REMOVE`. +Its data contains the ID of the removed block (`blockId`). +It can be sent immediately or after removal animation. + +```js +window.addEventListener('message', (messageEvent) => { + switch (messageEvent.data.type) { + case 'PB:BLOCK_REMOVE': + const blockId = messageEvent.data.data.blockId; + const blockElementToRemove = document.querySelector(`[data-ibexa-block-id="${blockId}"]`);; + if (blockElementToRemove) { + blockElementToRemove.addEventListener('animationend', () => { + blockElementToRemove.remove(); + window.parent.postMessage({ + type: 'APP:BLOCK_REMOVE_RESPONSE', + data: { + blockId: blockId, + }, + }); + }); + blockElementToRemove.classList.add('c-pb-block-preview--is-removing'); + } else { + console.error('No block element found for block ID ' + messageEvent.data.data.blockId, 'notification.headless_unresponsive_preview'); + } + break; + } +}); +``` +```css +.c-pb-block-preview--is-removing { + animation-duration: 1s; + animation-name: c-pb-block-preview--is-removing; +} +@keyframes c-pb-block-preview--is-removing { + to { + opacity: 0; + height: 0; + } +} +``` + +`APP:BLOCK_REMOVE_REQUEST` message is sent from the frontend preview to the Page Builder to request the removal of a block. +Its data contains the ID of the block to remove (`blockId`). +The Page Builder responses with a `PB:UPDATE_FIELD_DATA`. + +## Guidelines for frontend implementation + +The protocol documentation is illustrated with Vanilla JS examples. +But you should use a JS framework to implement the frontend. + +Pure frontend and Page Builder preview should share most code as possible. +It could be a same controller called in a different ways to know which content to render on frontend or if it's framed into a Page Builder preview. + +Each block type view should be implemented as a component so you can easily add new block types and new views. + +CSS classes can be named however you wish, but it may be advisable to follow certain conventions to help the reuse of existing style sheets. + +### CSS classes and data attribute conventions + +Some class names are, by convention, only used when the frontend is used in the Page Builder preview, some are always used. + +For example, the convention is that when the frontend is used in the Page Builder preview, the `c-pb-iframe__preview-body` class is added to the document body. + +#### Zones + +The always present `data-ibexa-zone-id` attribute (`zoneElement.dataset.ibexaZoneId`) contains the zone ID. + +| Class name | PB only | Description | +|----------------------------------|---------|-----------------------------------------------------| +| `landing-page__zone` | No | Every zone container | +| `landing-page__zone--${zone.id}` | No | Each zone container with its own ID | +| `m-page-builder__zone` | Yes | Every zone container when previewed in Page Builder | +| `m-page-builder__zone--dragover` | Yes | When a block is dragged over the zone | +| `m-page-builder__zone--empty` | Yes | When the zone has no block | + +### Blocks + +The always present `data-ibexa-block-id` attribute (`blockElement.dataset.ibexaBlockId`) contains the block ID. + +| Class name | PB only | Description | +|---------------------------------------|---------|-------------------------------------------------------------------------------------------------------| +| `landing-page__block` | No | Every block container | +| `c-pb-block-preview` | Yes | Every block container when previewed in Page Builder | +| `c-pb-block-preview--is-dragging-out` | Yes | When a block is being dragged | +| `c-pb-block-preview--is-removing` | Yes | When a block is being removed (see [Block removal (`blocks.remove`)](#block-removal-blocksremove)) | +| `ibexa-mark-invisible` | Yes | When a scheduled block is marked as invisible | +| `c-pb-block-preview--unavailable` | Yes | When a block is unavailable for this field | +| `c-pb-block-preview__inner` | Yes | The inner container of a block | +| `c-pb-block-preview__inner--invalid` | Yes | The inner container of a block with invalid attribute value | +| `droppable-placeholder` | Yes | The placeholder element shown when a block is being dragged over a zone to indicate the drop position | +| `c-pb-block-preview--highlighted` | Yes | When a block is highlighted as when newly dropped | + +## Static example + +The following example is just a demo in vanilla JS provided as-is to illustrate the Page Builder protocol usage. +It can be used to observe the messages exchanged between the Page Builder and a frontend preview in the browser JS console. +It doesn't support all the block types or views. + +`page.html` is a static HTML page with some JS to handle the Page Builder protocol messages and basic CSS to show how conventional classes are for. +It works both as a standalone page and as a Page Builder preview. + +??? note "`page.html`" + + ``` html hl_lines="175 527 778" + [[= include_code('code_samples/page/headless/page.html', indent_level=1) =]] + ``` + +Edit `page.html`: + +- Change the `apiBaseUrl` constant to declare the origin on which the REST API and the conversion controller are called. +- Change the `pbOrigin` constant to declare the origin of the Page Builder. +- Change the `pageContentTypeIds` to list content type IDs that have a Landing Page field. You can set this to `false` to skip the content type test. + +It supports the following block types and views: + +- Code (`tag`) block type with `default` and `source_code` views +- Content List (`contentlist`) block type with `default` view diff --git a/docs/content_management/pages/pages.md b/docs/content_management/pages/pages.md index 0c62b090e1..2694ab542d 100644 --- a/docs/content_management/pages/pages.md +++ b/docs/content_management/pages/pages.md @@ -10,4 +10,5 @@ Pages are block-based special types of content that editors can create and modif [[= cards([ "content_management/pages/page_builder_guide", "content_management/pages/page_blocks", + "content_management/pages/headless_page_builder", ], columns=3) =]] diff --git a/mkdocs.yml b/mkdocs.yml index 02989a04d4..4b2f0ec568 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Pages: content_management/pages/pages.md - Page Builder guide: content_management/pages/page_builder_guide.md - Page blocks: content_management/pages/page_blocks.md + - Headless Page Builder: content_management/pages/headless_page_builder.md - Forms: - Forms: content_management/forms/forms.md - Form Builder guide: content_management/forms/form_builder_guide.md From a2014cfe5e4b500191b171d69def065c8761334e Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:40:48 +0200 Subject: [PATCH 02/23] page.html: Remove layout-2-columns --- code_samples/page/headless/page.html | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/code_samples/page/headless/page.html b/code_samples/page/headless/page.html index ba0bd3b6ed..c384c80508 100644 --- a/code_samples/page/headless/page.html +++ b/code_samples/page/headless/page.html @@ -24,12 +24,6 @@ margin: 10px 0; } - #layout-2-columns { - display: grid; - grid-auto-flow: column; - gap: 3px; - } - .landing-page__zone { background: oklch(86.53% 0.0233 241.92deg); border: 1px solid oklch(47.52% 0.0999 241.4deg); @@ -161,10 +155,6 @@ -
Footer
From da74bbc5e386881101e5b0bad84b1c76ef8e5022 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:56:37 +0200 Subject: [PATCH 03/23] headless_page_builder.md: Fix Markdown Error: docs/content_management/pages/headless_page_builder.md:222:145 MD045/no-alt-text Images should have alternate text (alt text) https://github.com/DavidAnson/markdownlint/blob/v0.41.1/doc/md045.md Error: docs/content_management/pages/headless_page_builder.md:462 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.41.1/doc/md031.md Error: docs/content_management/pages/headless_page_builder.md:463 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```css"] https://github.com/DavidAnson/markdownlint/blob/v0.41.1/doc/md031.md --- docs/content_management/pages/headless_page_builder.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 4beea448c8..8c860c0630 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -219,7 +219,7 @@ The `PB:UPDATE_FIELD_DATA` message is sent from the Page Builder to the frontend Its data contains the new value of the field with the following structure: - Always, the current value of the Landing page field being edited (`fieldValue`) including the layout, zones, and blocks. -
For example, only `fieldValue` is sent when manipulating the [timeline]([[= user_doc =]]/content_management/schedule_publishing/#timeline)![](page_builder_toolbartimelinetoggler.png){style="display:inline;width:27px;vertical-align:middle;"}. +
For example, only `fieldValue` is sent when manipulating the [timeline]([[= user_doc =]]/content_management/schedule_publishing/#timeline)![Timeline toggler icon](page_builder_toolbartimelinetoggler.png){style="display:inline;width:27px;vertical-align:middle;"}. - Optionally, a list of the existing block types, their attributes, and their configuration (`blocksConfig`) - Optionally, a block-ID-to-name mapping (`blocksIdMap`) - Optionally, a list of IDs from the new blocks that have been added (`highlightedBlockIds`) @@ -460,6 +460,7 @@ window.addEventListener('message', (messageEvent) => { } }); ``` + ```css .c-pb-block-preview--is-removing { animation-duration: 1s; From 235ac92690993800102065f1e0c11eee78a362f7 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:14:07 +0200 Subject: [PATCH 04/23] headless_page_builder.md: Reword meta --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 8c860c0630..29efa0f9ad 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -1,5 +1,5 @@ --- -description: Preview in Page Builder using your frontend. +description: Preview and edit in Page Builder using your frontend. edition: experience month_change: true --- From 18bdaf2c15f787e2e39e982a0b5f99e801fb5213 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:22:32 +0200 Subject: [PATCH 05/23] headless_page_builder.md: Reword --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 29efa0f9ad..7a57917fab 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -531,7 +531,7 @@ The following example is just a demo in vanilla JS provided as-is to illustrate It can be used to observe the messages exchanged between the Page Builder and a frontend preview in the browser JS console. It doesn't support all the block types or views. -`page.html` is a static HTML page with some JS to handle the Page Builder protocol messages and basic CSS to show how conventional classes are for. +`page.html` is a static HTML page with some JS to handle the Page Builder protocol messages and basic CSS to show what conventional CSS classes are designed for. It works both as a standalone page and as a Page Builder preview. ??? note "`page.html`" From ebb07f437171f5678d46b9b28487d0b7b0fce667 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:23:29 +0200 Subject: [PATCH 06/23] headless_page_builder.md: Reword --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 7a57917fab..97ef2690fb 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -531,7 +531,7 @@ The following example is just a demo in vanilla JS provided as-is to illustrate It can be used to observe the messages exchanged between the Page Builder and a frontend preview in the browser JS console. It doesn't support all the block types or views. -`page.html` is a static HTML page with some JS to handle the Page Builder protocol messages and basic CSS to show what conventional CSS classes are designed for. +`page.html` is a static HTML page with some JS to handle the Page Builder protocol messages and to show what conventional CSS classes are designed for. It works both as a standalone page and as a Page Builder preview. ??? note "`page.html`" From 07a06814388c1f9aee667232c741f736642b7f59 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:47:35 +0200 Subject: [PATCH 07/23] headless_page_builder.md: Remove remaining "Edit in the headless Page Builder" --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 97ef2690fb..554a4cb7bb 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -22,7 +22,7 @@ Below the description, enable the **Headless mode**, fill in the **Page Builder ## Communication protocol -The frontend resource targeted by `base_url` is loaded by the Page Builder when editing a content having a Landing page field "Edit in the headless Page Builder" enabled. +The specified frontend resource is loaded by the Page Builder when you edit content with a Landing page field. This resource must follow a protocol to communicate with the Page Builder from the iframe is loaded in. This protocol is based on the JavaScript message API. From 72255f4c01137e7c05ba8cee7048e182188cad28 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:52:16 +0200 Subject: [PATCH 08/23] headless_page_builder.md: Rewording --- .../pages/headless_page_builder.md | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 554a4cb7bb..941ed88de3 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -13,7 +13,7 @@ You provide a single URL for the frontend page and let it communicate with the P ## Configuration - Navigate to **Administration** > **SiteAccess Configuration** -- Choose a SiteAccess you want this set up a preview for. None if you want to set up a default preview for SiteAccesses not having a specific preview. +- Choose the SiteAccess for which you want to set up a preview. Choose none to set up a default preview for SiteAccesses that do not have a specific preview. - Then, click **Headless** Below the description, enable the **Headless mode**, fill in the **Page Builder preview URL**, and **Save** the configuration. @@ -23,11 +23,11 @@ Below the description, enable the **Headless mode**, fill in the **Page Builder ## Communication protocol The specified frontend resource is loaded by the Page Builder when you edit content with a Landing page field. -This resource must follow a protocol to communicate with the Page Builder from the iframe is loaded in. +This resource must follow a protocol to communicate with the Page Builder from within the iframe in which it's loaded. This protocol is based on the JavaScript message API. -The Page Builder sends messages to the framed frontend resource. -They can be received by listening the [message event](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/message_event). +The Page Builder sends messages to the frontend resource in the iframe. +You can receive them by listening for the [message event](https://developer.mozilla.org/en-US/docs/Web/API/Window/message_event). The frontend resource sends back messages to the Page Builder. They can be sent using the [`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) method. @@ -38,7 +38,7 @@ const pbOrigin = 'https://my-company.cohesivo.app'; window.parent.postMessage(message, pbOrigin); ``` -Those messages are JS objects with the following structure: +These messages are JavaScript objects with the following structure: ```js let message = { @@ -47,17 +47,19 @@ let message = { }; ``` -The `PREFIX` sort message types by their sender. -`PB:` for messages sent by the Page Builder, `APP:` for messages sent by the frontend resource. +The `PREFIX` identifies the sender of each message type: + +- `PB:` for messages sent by the Page Builder +- `APP:` for messages sent by the frontend resource The data depends on the message type. -Message types are then sorted by capabilities. -So the frontend can declare which capabilities it supports and the Page Builder can refrain from sending or expecting unsupported messages. +Message types are also grouped by capability. +The frontend can declare which capabilities it supports, so the Page Builder can avoid sending or expecting unsupported messages. ### Message types and capabilities -The handshake and core messages are mandatory and not related to an optional capability. +Handshake and core messages are mandatory and don't depend on optional capabilities. | Capability | Message type | Description | |--------------------|---------------------------------------------------------------------------------------------|--------------------------------------| @@ -82,7 +84,7 @@ The handshake and core messages are mandatory and not related to an optional cap ### Communication initialization -First, the frontend send an initialization message to the Page Builder, indicating which version of the protocol and which capabilities are supported: +First, the frontend sends an initialization message to the Page Builder, indicating which protocol versions and capabilities it supports: ```js const initializedMessage = { @@ -102,15 +104,15 @@ const initializedMessage = { ``` The Page Builder replies with a confirmation message. -The initialization message might be sent several times until the Page Builder replies to it. +The frontend may send the initialization message several times until the Page Builder replies. This confirmation `data` contains: -- the actual version of the protocol used (`protocol.version`) and the other supported versions (`protocol.supported`) +- the protocol version in use (`protocol.version`) and the other supported versions (`protocol.supported`) - a list of all available capabilities (`capabilities`) -- a list of the existing block types, their attributes, and their configuration (`blocksConfig`) - It contains all the block types but mark as not visible the blocks not available for this field. -- information about the actually edited content draft (`intentParameters`) -- the current value of the Landing page field being edited (`fieldValue`) including the layout, zones, and blocks. +- all block types, their attributes, and their configuration (`blocksConfig`) - types unavailable for this field are marked as not visible +- information about the content draft currently being edited (`intentParameters`) +- the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks - a block-ID-to-name mapping (`blocksIdMap`) - a list of translations for the frontend to use (`translations`) From 1ae98bb669628a659423ac39e9e4c0c24d2fbce7 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:12:57 +0200 Subject: [PATCH 09/23] headless_page_builder.md: Rewording --- .../pages/headless_page_builder.md | 114 +++++++++--------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 941ed88de3..a18c3dfc48 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -218,10 +218,10 @@ This confirmation `data` contains: The `PB:UPDATE_FIELD_DATA` message is sent from the Page Builder to the frontend resource when the Landing page field value is updated. -Its data contains the new value of the field with the following structure: +Its data contains the field's new value, with the following structure: -- Always, the current value of the Landing page field being edited (`fieldValue`) including the layout, zones, and blocks. -
For example, only `fieldValue` is sent when manipulating the [timeline]([[= user_doc =]]/content_management/schedule_publishing/#timeline)![Timeline toggler icon](page_builder_toolbartimelinetoggler.png){style="display:inline;width:27px;vertical-align:middle;"}. +- Always included: the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks. +
For example, only `fieldValue` is sent when you use the [timeline]([[= user_doc =]]/content_management/schedule_publishing/#timeline)![Timeline toggler icon](page_builder_toolbartimelinetoggler.png){style="display:inline;width:27px;vertical-align:middle;"}. - Optionally, a list of the existing block types, their attributes, and their configuration (`blocksConfig`) - Optionally, a block-ID-to-name mapping (`blocksIdMap`) - Optionally, a list of IDs from the new blocks that have been added (`highlightedBlockIds`) @@ -243,8 +243,8 @@ Its data contains the new value of the field with the following structure: ### Re-dispatching events -The `PB:DISPATCH_EVENT` message is sent from the Page Builder to the frontend resource for being re-dispatched there as a custom event. -Its data contains the name of the event to dispatch (`eventName`) and the data to pass to the event detail (`eventData`). +The Page Builder sends the `PB:DISPATCH_EVENT` message to the frontend resource to be re-dispatched there as a custom event. +Its data contains the name of the event to dispatch (`eventName`) and the data to pass in the event's `detail` property (`eventData`). ```js window.addEventListener('message', (messageEvent) => { @@ -258,8 +258,8 @@ window.addEventListener('message', (messageEvent) => { #### Available events -- `ibexa-active-block-clicked`: It confirms `APP:BLOCK_CLICKED` have been received. It has no data. -- `ibexa-post-update-blocks-preview`: It's sent when the timeline is used. +- `ibexa-active-block-clicked`: Confirms that `APP:BLOCK_CLICKED` was received. It has no data. +- `ibexa-post-update-blocks-preview`: Sent when the timeline is used. - `fieldValue`: The same as in [`PB:UPDATE_FIELD_DATA`](#on-field-update) - `blockIds`: A list of all the block IDs - `blocksMaps`: A map of block config per block ID @@ -273,10 +273,10 @@ window.addEventListener('ibexa-post-update-blocks-preview', (customEvent) => { ### Geometry and pointer tracking (`blocks.geometry` and `pointer.tracking`) -The pointer tracking and the geometry helps the Page Builder to position the block editing menus above the frontend preview. -Such menu is a `.c-pb-headless-preview-menu` element positioned by the Page Builder from its DOM above the preview `iframe`. +Pointer tracking and geometry help the Page Builder position block-editing menus over the frontend preview. +These menus are `.c-pb-headless-preview-menu` elements positioned by the Page Builder in its DOM above the preview `iframe`. -`APP:MOUSE_POSITION` message is sent from the frontend preview to the Page Builder to declare the actual position of the mouse. +The frontend preview sends the `APP:MOUSE_POSITION` message to report the mouse's current position to the Page Builder. ```js window.addEventListener('mousemove', (mouseEvent) => { @@ -290,9 +290,9 @@ window.addEventListener('mousemove', (mouseEvent) => { }); ``` -`APP:POSITIONS_UPDATE` message is sent from the frontend preview to the Page Builder to declare the actual position of the blocks. -Its data contains a list of objects with block IDs, their positions, and dimensions in the frontend preview. -This format is close to [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) method. +The frontend preview sends the `APP:POSITIONS_UPDATE` message to report the blocks' current positions to the Page Builder. +Its data contains a list of objects with block IDs, positions, and dimensions in the frontend preview. +This format is similar to the object returned by the [`getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect) method. ```js const positionsUpdate = () => { @@ -319,11 +319,9 @@ const positionsUpdate = () => { }; ``` -This message should be sent each time the positions of the blocks change. -It should be sent after updating the blocks, like in response to [`PB:UPDATE_FIELD_DATA`](#on-field-update). -It should be sent after scrolling or resizing. +Send this message whenever the positions of the blocks change, for example, after updating the blocks in response to [`PB:UPDATE_FIELD_DATA`](#on-field-update), scrolling, or resizing. -`APP:SCROLL_END` message is sent from the frontend preview to the Page Builder to notify that a scroll operation has ended. +The frontend preview sends the `APP:SCROLL_END` message to notify the Page Builder that a scroll operation has ended. It has no data. ```js @@ -349,15 +347,15 @@ window.addEventListener('message', (messageEvent) => { ### Drag and drop (`blocks.dnd`) -`PB:DRAG_OVER` message is sent from the Page Builder to the frontend preview to give the position of the mouse while a (new or existing) block is dragged. +The Page Builder sends the `PB:DRAG_OVER` message to the frontend preview to report the mouse position while a new or existing block is being dragged. !!! tip "Mouse tracking" - - `APP:MOUSE_POSITION` helps the Page Builder to know where the mouse is when moved over the preview. It's associated to `APP:POSITIONS_UPDATE` to know if a preview block is hovered. - - `PB:DRAG_OVER` helps the frontend to know where the mouse is when a block is dragged over the preview. + - `APP:MOUSE_POSITION` helps the Page Builder track the mouse as it moves over the preview. Together with `APP:POSITIONS_UPDATE`, it lets the Page Builder determine whether the pointer is over a preview block. + - `PB:DRAG_OVER` helps the frontend track the mouse as a block is dragged over the preview. -`PB:DRAG_START_PREVIEW` and `PB:DRAG_END_PREVIEW` are sent at the beginning and at the end of a drag operation on an existing block in the frontend preview. -Its data contains the ID of the block being dragged (`blockId`). +The Page Builder sends `PB:DRAG_START_PREVIEW` and `PB:DRAG_END_PREVIEW` at the beginning and end of a drag operation on an existing block in the frontend preview. +Their data contains the ID of the block being dragged (`blockId`). ```js window.addEventListener('message', (messageEvent) => { @@ -372,17 +370,19 @@ window.addEventListener('message', (messageEvent) => { }); ``` -`PB:DROP` message is sent from the Page Builder to the frontend preview to notify that a block has been dropped. -It has no data. Combined with the last `PB:DRAG_OVER` message, the frontend can determine where the block has been dropped. +The Page Builder sends the `PB:DROP` message to the frontend preview to notify it that a block has been dropped. +The message has no data. Combined with the most recent `PB:DRAG_OVER` message, it lets the frontend determine where the block was dropped. -`APP:DROP_RESPONSE` message is sent from the frontend preview to the Page Builder to tell where the block has been dropped. +The frontend preview sends the `APP:DROP_RESPONSE` message to the Page Builder to report where the block was dropped. Its data contains: - the ID of the zone where the block has been dropped (`zoneId`) -- the ID of a block that is now below the dropped block (`nextBlockId`) if the dropped block isn't the last one of the zone. +- the ID of the block that will follow the dropped block (`nextBlockId`), if the dropped block is not the last block in the zone -In the following example, `targetBlockId` value is the ID of a block the dropped block was dropped on or just before, so the dragged block takes its place and move it below, or `null` when dropped at the bottom of the zone. +In the following example, `targetBlockId` is the ID of the block that the dragged block was dropped on or just before. +The dragged block is inserted in that block's place, moving the existing block down one position. +If the dragged block is dropped at the bottom of the zone, `targetBlockId` is `null`. ```js const dropResponseMessage = { @@ -394,10 +394,10 @@ const dropResponseMessage = { }; ``` -After a drop `APP:DROP_RESPONSE` from the frontend, the next `PB:UPDATE_FIELD_DATA` message from the Page Builder contains the dropped block ID in its `highlightedBlockIds` array (`messageEvent.data.data.highlightedBlockIds`). +After the frontend sends `APP:DROP_RESPONSE`, the next `PB:UPDATE_FIELD_DATA` message from the Page Builder contains the dropped block's ID in its `highlightedBlockIds` array (`messageEvent.data.data.highlightedBlockIds`). -`PB:SCROLL_BY` message is sent from the Page Builder when a block is dragged near a border of the preview which needs to be scrolled. -Its data contains the `top` or `left` amount to scroll by. +The Page Builder sends the `PB:SCROLL_BY` message when a block is dragged near an edge of the preview, indicating that the preview needs to be scrolled. +Its data contains the amount to scroll vertically (`top`) or horizontally (`left`). ```js window.addEventListener('message', (messageEvent) => { @@ -409,12 +409,11 @@ window.addEventListener('message', (messageEvent) => { }); ``` -See [Geometry (`blocks.geometry`)](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking)'s `APP:SCROLL_END` message to declare the end of the scroll operation -and `APP:POSITIONS_UPDATE` message to update the blocks positions. +For information about reporting the end of a scroll operation and updating block positions, see `APP:SCROLL_END` and `APP:POSITIONS_UPDATE` in [Geometry (`blocks.geometry`)](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking). ### Block reveal (`blocks.reveal`) -`PB:SCROLL_INTO_BLOCK` is send by the Page Builder to the frontend preview to request that a block is scrolled into view. +The Page Builder sends the `PB:SCROLL_INTO_BLOCK` message to the frontend preview to request that a block be scrolled into view. Its data contains the ID of the block to scroll into view (`blockId`). It can be used with the [`scrollIntoView()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) method. @@ -422,7 +421,7 @@ It can be used with the [`scrollIntoView()`](https://developer.mozilla.org/en-US window.addEventListener('message', (messageEvent) => { switch (messageEvent.data.type) { case 'PB:SCROLL_INTO_BLOCK': - const blockElementToScrollInto = document.querySelector(`[data-ibexa-block-id="${messageEvent.data.data.blockId}"]`);; + const blockElementToScrollInto = document.querySelector(`[data-ibexa-block-id="${messageEvent.data.data.blockId}"]`); blockElementToScrollInto.scrollIntoView({ behavior: 'smooth', block: 'center' }); break; } @@ -431,19 +430,19 @@ window.addEventListener('message', (messageEvent) => { ### Block removal (`blocks.remove`) -`PB:BLOCK_REMOVE` message is sent from the Page Builder to the frontend preview to notify that a block should be removed, for example, from the Structure view. +The Page Builder sends the `PB:BLOCK_REMOVE` message to the frontend preview to notify it that a block should be removed, for example, from the Structure view. Its data contains the ID of the block to remove (`blockId`). -`APP:BLOCK_REMOVE_RESPONSE` message is sent from the frontend preview to the Page Builder to confirm that the block has been removed as requested by `PB:BLOCK_REMOVE`. +The frontend preview sends the `APP:BLOCK_REMOVE_RESPONSE` message to the Page Builder to confirm that the block has been removed as requested by `PB:BLOCK_REMOVE`. Its data contains the ID of the removed block (`blockId`). -It can be sent immediately or after removal animation. +You can send it immediately or after the removal animation. ```js window.addEventListener('message', (messageEvent) => { switch (messageEvent.data.type) { case 'PB:BLOCK_REMOVE': const blockId = messageEvent.data.data.blockId; - const blockElementToRemove = document.querySelector(`[data-ibexa-block-id="${blockId}"]`);; + const blockElementToRemove = document.querySelector(`[data-ibexa-block-id="${blockId}"]`); if (blockElementToRemove) { blockElementToRemove.addEventListener('animationend', () => { blockElementToRemove.remove(); @@ -476,27 +475,26 @@ window.addEventListener('message', (messageEvent) => { } ``` -`APP:BLOCK_REMOVE_REQUEST` message is sent from the frontend preview to the Page Builder to request the removal of a block. +The frontend preview sends the `APP:BLOCK_REMOVE_REQUEST` message to the Page Builder to request that a block be removed. Its data contains the ID of the block to remove (`blockId`). -The Page Builder responses with a `PB:UPDATE_FIELD_DATA`. +The Page Builder responds with a `PB:UPDATE_FIELD_DATA` message. ## Guidelines for frontend implementation -The protocol documentation is illustrated with Vanilla JS examples. -But you should use a JS framework to implement the frontend. +The protocol documentation uses vanilla JavaScript examples, but you should use a JavaScript framework to implement the frontend. -Pure frontend and Page Builder preview should share most code as possible. -It could be a same controller called in a different ways to know which content to render on frontend or if it's framed into a Page Builder preview. +The frontend and Page Builder preview should share as much code as possible. +For example, you could use the same controller, called in different ways, to determine whether to render the regular frontend or the Page Builder preview. Each block type view should be implemented as a component so you can easily add new block types and new views. -CSS classes can be named however you wish, but it may be advisable to follow certain conventions to help the reuse of existing style sheets. +You can name CSS classes as you wish, but following these conventions can make existing stylesheets easier to reuse. ### CSS classes and data attribute conventions -Some class names are, by convention, only used when the frontend is used in the Page Builder preview, some are always used. +By convention, some class names are used only in the Page Builder preview, while others are always used. -For example, the convention is that when the frontend is used in the Page Builder preview, the `c-pb-iframe__preview-body` class is added to the document body. +For example, when the frontend is used in the Page Builder preview, the `c-pb-iframe__preview-body` class is added to the document body. #### Zones @@ -506,7 +504,7 @@ The always present `data-ibexa-zone-id` attribute (`zoneElement.dataset.ibexaZon |----------------------------------|---------|-----------------------------------------------------| | `landing-page__zone` | No | Every zone container | | `landing-page__zone--${zone.id}` | No | Each zone container with its own ID | -| `m-page-builder__zone` | Yes | Every zone container when previewed in Page Builder | +| `m-page-builder__zone` | Yes | Every zone container in the Page Builder preview | | `m-page-builder__zone--dragover` | Yes | When a block is dragged over the zone | | `m-page-builder__zone--empty` | Yes | When the zone has no block | @@ -517,7 +515,7 @@ The always present `data-ibexa-block-id` attribute (`blockElement.dataset.ibexaB | Class name | PB only | Description | |---------------------------------------|---------|-------------------------------------------------------------------------------------------------------| | `landing-page__block` | No | Every block container | -| `c-pb-block-preview` | Yes | Every block container when previewed in Page Builder | +| `c-pb-block-preview` | Yes | Every block container in the Page Builder preview | | `c-pb-block-preview--is-dragging-out` | Yes | When a block is being dragged | | `c-pb-block-preview--is-removing` | Yes | When a block is being removed (see [Block removal (`blocks.remove`)](#block-removal-blocksremove)) | | `ibexa-mark-invisible` | Yes | When a scheduled block is marked as invisible | @@ -525,15 +523,15 @@ The always present `data-ibexa-block-id` attribute (`blockElement.dataset.ibexaB | `c-pb-block-preview__inner` | Yes | The inner container of a block | | `c-pb-block-preview__inner--invalid` | Yes | The inner container of a block with invalid attribute value | | `droppable-placeholder` | Yes | The placeholder element shown when a block is being dragged over a zone to indicate the drop position | -| `c-pb-block-preview--highlighted` | Yes | When a block is highlighted as when newly dropped | +| `c-pb-block-preview--highlighted` | Yes | When a block is highlighted, for example, when newly dropped | ## Static example -The following example is just a demo in vanilla JS provided as-is to illustrate the Page Builder protocol usage. -It can be used to observe the messages exchanged between the Page Builder and a frontend preview in the browser JS console. -It doesn't support all the block types or views. +The following vanilla JavaScript demo is provided as-is to illustrate how to use the Page Builder protocol. +You can use it to observe the messages exchanged between the Page Builder and a frontend preview in the browser's JavaScript console. +It does not support all block types or views. -`page.html` is a static HTML page with some JS to handle the Page Builder protocol messages and to show what conventional CSS classes are designed for. +`page.html` is a static HTML page with JavaScript that handles Page Builder protocol messages and demonstrates the intended uses of the conventional CSS classes. It works both as a standalone page and as a Page Builder preview. ??? note "`page.html`" @@ -544,11 +542,11 @@ It works both as a standalone page and as a Page Builder preview. Edit `page.html`: -- Change the `apiBaseUrl` constant to declare the origin on which the REST API and the conversion controller are called. -- Change the `pbOrigin` constant to declare the origin of the Page Builder. -- Change the `pageContentTypeIds` to list content type IDs that have a Landing Page field. You can set this to `false` to skip the content type test. +- Change the `apiBaseUrl` constant to set the origin to which requests are sent for the REST API and conversion controller. +- Change the `pbOrigin` constant to set the Page Builder's origin. +- Change `pageContentTypeIds` to list the content type IDs that have a Landing Page field. You can set it to `false` to skip the content type check. -It supports the following block types and views: +The demo supports the following block types and views: - Code (`tag`) block type with `default` and `source_code` views - Content List (`contentlist`) block type with `default` view From ec116aeb00902809e409ea07323af6f136e90335 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:25:36 +0200 Subject: [PATCH 10/23] eventData -> eventDetail; rm preview.params --- code_samples/page/headless/page.html | 15 +++++++-------- .../pages/headless_page_builder.md | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/code_samples/page/headless/page.html b/code_samples/page/headless/page.html index c384c80508..a42bcdd486 100644 --- a/code_samples/page/headless/page.html +++ b/code_samples/page/headless/page.html @@ -484,13 +484,12 @@ if (window.parent !== window) { // Inside the Page Builder iframe const pbOrigin = 'https://my-company.cohesivo.app'; const capabilities = [ - 'blocks.dnd', // 100% - 'blocks.geometry', // 100% - 'blocks.remove', // 100% - 'blocks.reveal', // 100% - 'blocks.select', // 100% - 'pointer.tracking', // 100% - 'preview.params', // 0% + 'blocks.dnd', + 'blocks.geometry', + 'blocks.remove', + 'blocks.reveal', + 'blocks.select', + 'pointer.tracking', ]; const initInterval = 1500; // ms const initRetries = -1; // -1 = infinite, 0 = no retry, 1 = 1 retry, etc. @@ -640,7 +639,7 @@ positionsUpdate(); break; case 'PB:DISPATCH_EVENT': - window.dispatchEvent(new CustomEvent(messageEvent.data.data.eventName, {detail: messageEvent.data.data.eventData})); + window.dispatchEvent(new CustomEvent(messageEvent.data.data.eventName, {detail: messageEvent.data.data.eventDetail})); break; case 'PB:BLOCK_REMOVE': const blockId = messageEvent.data.data.blockId; diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index a18c3dfc48..2e362e01ab 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -244,13 +244,13 @@ Its data contains the field's new value, with the following structure: ### Re-dispatching events The Page Builder sends the `PB:DISPATCH_EVENT` message to the frontend resource to be re-dispatched there as a custom event. -Its data contains the name of the event to dispatch (`eventName`) and the data to pass in the event's `detail` property (`eventData`). +Its data contains the name of the event to dispatch (`eventName`) and the data to pass in the event's `detail` property (`eventDetail`). ```js window.addEventListener('message', (messageEvent) => { switch (messageEvent.data.type) { case 'PB:DISPATCH_EVENT': - window.dispatchEvent(new CustomEvent(messageEvent.data.data.eventName, { detail: messageEvent.data.data.eventData })); + window.dispatchEvent(new CustomEvent(messageEvent.data.data.eventName, { detail: messageEvent.data.data.eventDetail })); break; } }); From cb625a164221592f795798fc4dc47533d16ef446 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:30:09 +0200 Subject: [PATCH 11/23] ibexa-post-update-blocks-preview customEvent.detail without s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Grabowski --- docs/content_management/pages/headless_page_builder.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 2e362e01ab..9e209ff5c2 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -266,8 +266,8 @@ window.addEventListener('message', (messageEvent) => { ```js window.addEventListener('ibexa-post-update-blocks-preview', (customEvent) => { - setLayout(customEvent.details.fieldValue.layout); - renderZones(customEvent.details.fieldValue.zones); + setLayout(customEvent.detail.fieldValue.layout); + renderZones(customEvent.detail.fieldValue.zones); }); ``` From eeebd0ab28cdec113257096591bb2d4a0d49a4cb Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:31:20 +0200 Subject: [PATCH 12/23] PB:SCROLL_BY only top --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 9e209ff5c2..ca6b7aa564 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -397,7 +397,7 @@ const dropResponseMessage = { After the frontend sends `APP:DROP_RESPONSE`, the next `PB:UPDATE_FIELD_DATA` message from the Page Builder contains the dropped block's ID in its `highlightedBlockIds` array (`messageEvent.data.data.highlightedBlockIds`). The Page Builder sends the `PB:SCROLL_BY` message when a block is dragged near an edge of the preview, indicating that the preview needs to be scrolled. -Its data contains the amount to scroll vertically (`top`) or horizontally (`left`). +Its data contains the amount to scroll vertically (`top`). ```js window.addEventListener('message', (messageEvent) => { From 5c3a4c2b141fdd81d8480cf50876a70425026050 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:33:41 +0200 Subject: [PATCH 13/23] pbOrigin to send APP:BLOCK_REMOVE_RESPONSE (from @GrabowskiM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Grabowski --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index ca6b7aa564..a40d4a2dd5 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -451,7 +451,7 @@ window.addEventListener('message', (messageEvent) => { data: { blockId: blockId, }, - }); + }, pbOrigin); }); blockElementToRemove.classList.add('c-pb-block-preview--is-removing'); } else { From cb823fb334b2ae2928ea5e8493e628c81e60c1aa Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:38:58 +0200 Subject: [PATCH 14/23] PB:UPDATE_FIELD_DATA now always send all properties --- docs/content_management/pages/headless_page_builder.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index a40d4a2dd5..f1aab1916b 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -220,11 +220,10 @@ The `PB:UPDATE_FIELD_DATA` message is sent from the Page Builder to the frontend Its data contains the field's new value, with the following structure: -- Always included: the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks. -
For example, only `fieldValue` is sent when you use the [timeline]([[= user_doc =]]/content_management/schedule_publishing/#timeline)![Timeline toggler icon](page_builder_toolbartimelinetoggler.png){style="display:inline;width:27px;vertical-align:middle;"}. -- Optionally, a list of the existing block types, their attributes, and their configuration (`blocksConfig`) -- Optionally, a block-ID-to-name mapping (`blocksIdMap`) -- Optionally, a list of IDs from the new blocks that have been added (`highlightedBlockIds`) +- the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks. +- the list of the existing block types, their attributes, and their configuration (`blocksConfig`) +- the block-ID-to-name mapping (`blocksIdMap`) +- the list of IDs from the new blocks that have been added (`highlightedBlockIds`) ```json { From bb51ffaab26b4c046ec0da08dcbfb3193f387757 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:21:03 +0200 Subject: [PATCH 15/23] Apply batched suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- .../pages/headless_page_builder.md | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index f1aab1916b..cf24bcc780 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -12,25 +12,24 @@ You provide a single URL for the frontend page and let it communicate with the P ## Configuration -- Navigate to **Administration** > **SiteAccess Configuration** -- Choose the SiteAccess for which you want to set up a preview. Choose none to set up a default preview for SiteAccesses that do not have a specific preview. -- Then, click **Headless** - -Below the description, enable the **Headless mode**, fill in the **Page Builder preview URL**, and **Save** the configuration. +1. In the left panel, go to **Administration** -> **SiteAccess Configuration** +1. Choose the SiteAccess that you want to set up a preview for. Do not choose a SiteAccess if you want to set up a default preview for the SiteAccesses that don't have a specific preview. +1. In **Available configurations** list, click **Headless**. +1. In **Edit configuration** section, expand the **Headless mode** drop-down list and select **Enabled**. +1. Populate the **Page Builder preview URL** field and click **Save**. ![Headless mode enabled with Page Builder preview URL](headless-saas-siteaccess-config.png) ## Communication protocol -The specified frontend resource is loaded by the Page Builder when you edit content with a Landing page field. -This resource must follow a protocol to communicate with the Page Builder from within the iframe in which it's loaded. -This protocol is based on the JavaScript message API. +The Page Builder loads the specified frontend resource when you edit a content item with the **Landing page** field. +The resource must use the JavaScript message API-based protocol to communicate with the Page Builder from within the iframe in which the Page Builder loads it. The Page Builder sends messages to the frontend resource in the iframe. You can receive them by listening for the [message event](https://developer.mozilla.org/en-US/docs/Web/API/Window/message_event). -The frontend resource sends back messages to the Page Builder. -They can be sent using the [`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) method. +The frontend resource sends messages back to the Page Builder. +You can send them by using the [`postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) method. The target origin of those messages must be the Page Builder's origin. ```js @@ -42,20 +41,20 @@ These messages are JavaScript objects with the following structure: ```js let message = { - type: 'PREFIX:MESSAGE_TYPE', + type: ':', data: {} }; ``` The `PREFIX` identifies the sender of each message type: -- `PB:` for messages sent by the Page Builder -- `APP:` for messages sent by the frontend resource +- `PB:` - for messages sent by the Page Builder +- `APP:` - for messages sent by the frontend resource The data depends on the message type. -Message types are also grouped by capability. -The frontend can declare which capabilities it supports, so the Page Builder can avoid sending or expecting unsupported messages. +Message types are grouped by capability. +The frontend can declare which capabilities it supports, so that the Page Builder can avoid sending or expecting unsupported messages. ### Message types and capabilities @@ -103,14 +102,14 @@ const initializedMessage = { }; ``` -The Page Builder replies with a confirmation message. The frontend may send the initialization message several times until the Page Builder replies. +The Page Builder replies with a confirmation message. -This confirmation `data` contains: +The confirmation `data` property object contains: -- the protocol version in use (`protocol.version`) and the other supported versions (`protocol.supported`) +- the protocol version in use (`protocol.version`) and other supported versions (`protocol.supported`) - a list of all available capabilities (`capabilities`) -- all block types, their attributes, and their configuration (`blocksConfig`) - types unavailable for this field are marked as not visible +- all block types, their attributes, and their configuration (`blocksConfig`) - types unavailable for this field are marked as not `visible` - information about the content draft currently being edited (`intentParameters`) - the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks - a block-ID-to-name mapping (`blocksIdMap`) @@ -243,7 +242,7 @@ Its data contains the field's new value, with the following structure: ### Re-dispatching events The Page Builder sends the `PB:DISPATCH_EVENT` message to the frontend resource to be re-dispatched there as a custom event. -Its data contains the name of the event to dispatch (`eventName`) and the data to pass in the event's `detail` property (`eventDetail`). +The message's data contains the name of the event to dispatch (`eventName`) and the data to pass in the event's `detail` property (`eventDetail`). ```js window.addEventListener('message', (messageEvent) => { @@ -257,11 +256,11 @@ window.addEventListener('message', (messageEvent) => { #### Available events -- `ibexa-active-block-clicked`: Confirms that `APP:BLOCK_CLICKED` was received. It has no data. -- `ibexa-post-update-blocks-preview`: Sent when the timeline is used. - - `fieldValue`: The same as in [`PB:UPDATE_FIELD_DATA`](#on-field-update) - - `blockIds`: A list of all the block IDs - - `blocksMaps`: A map of block config per block ID +- `ibexa-active-block-clicked` - Confirms that `APP:BLOCK_CLICKED` was received. It has no data +- `ibexa-post-update-blocks-preview` - Sent when the timeline is used + - `fieldValue` - The same as in [`PB:UPDATE_FIELD_DATA`](#on-field-update) + - `blockIds` - A list of all the block IDs + - `blocksMaps` - A map of block config per block ID ```js window.addEventListener('ibexa-post-update-blocks-preview', (customEvent) => { @@ -344,7 +343,7 @@ window.addEventListener('message', (messageEvent) => { }); ``` -### Drag and drop (`blocks.dnd`) +### Dragging and dropping (`blocks.dnd`) The Page Builder sends the `PB:DRAG_OVER` message to the frontend preview to report the mouse position while a new or existing block is being dragged. @@ -353,8 +352,8 @@ The Page Builder sends the `PB:DRAG_OVER` message to the frontend preview to rep - `APP:MOUSE_POSITION` helps the Page Builder track the mouse as it moves over the preview. Together with `APP:POSITIONS_UPDATE`, it lets the Page Builder determine whether the pointer is over a preview block. - `PB:DRAG_OVER` helps the frontend track the mouse as a block is dragged over the preview. -The Page Builder sends `PB:DRAG_START_PREVIEW` and `PB:DRAG_END_PREVIEW` at the beginning and end of a drag operation on an existing block in the frontend preview. -Their data contains the ID of the block being dragged (`blockId`). +The Page Builder sends `PB:DRAG_START_PREVIEW` and `PB:DRAG_END_PREVIEW` messages at the beginning and end of a drag operation on an existing block in the frontend preview. +Message data contains the ID of the block being dragged (`blockId`). ```js window.addEventListener('message', (messageEvent) => { @@ -370,7 +369,8 @@ window.addEventListener('message', (messageEvent) => { ``` The Page Builder sends the `PB:DROP` message to the frontend preview to notify it that a block has been dropped. -The message has no data. Combined with the most recent `PB:DRAG_OVER` message, it lets the frontend determine where the block was dropped. +The message has no data. +Combined with the most recent `PB:DRAG_OVER` message, it lets the frontend determine where the block was dropped. The frontend preview sends the `APP:DROP_RESPONSE` message to the Page Builder to report where the block was dropped. @@ -380,7 +380,7 @@ Its data contains: - the ID of the block that will follow the dropped block (`nextBlockId`), if the dropped block is not the last block in the zone In the following example, `targetBlockId` is the ID of the block that the dragged block was dropped on or just before. -The dragged block is inserted in that block's place, moving the existing block down one position. +The dragged block is inserted in that block's place, moving the existing block down by one position. If the dragged block is dropped at the bottom of the zone, `targetBlockId` is `null`. ```js @@ -430,10 +430,10 @@ window.addEventListener('message', (messageEvent) => { ### Block removal (`blocks.remove`) The Page Builder sends the `PB:BLOCK_REMOVE` message to the frontend preview to notify it that a block should be removed, for example, from the Structure view. -Its data contains the ID of the block to remove (`blockId`). +Message data contains the ID of the block to remove (`blockId`). The frontend preview sends the `APP:BLOCK_REMOVE_RESPONSE` message to the Page Builder to confirm that the block has been removed as requested by `PB:BLOCK_REMOVE`. -Its data contains the ID of the removed block (`blockId`). +Message data contains the ID of the removed block (`blockId`). You can send it immediately or after the removal animation. ```js @@ -475,7 +475,7 @@ window.addEventListener('message', (messageEvent) => { ``` The frontend preview sends the `APP:BLOCK_REMOVE_REQUEST` message to the Page Builder to request that a block be removed. -Its data contains the ID of the block to remove (`blockId`). +Message data contains the ID of the block to remove (`blockId`). The Page Builder responds with a `PB:UPDATE_FIELD_DATA` message. ## Guidelines for frontend implementation @@ -528,7 +528,7 @@ The always present `data-ibexa-block-id` attribute (`blockElement.dataset.ibexaB The following vanilla JavaScript demo is provided as-is to illustrate how to use the Page Builder protocol. You can use it to observe the messages exchanged between the Page Builder and a frontend preview in the browser's JavaScript console. -It does not support all block types or views. +It doesn't support all block types or views. `page.html` is a static HTML page with JavaScript that handles Page Builder protocol messages and demonstrates the intended uses of the conventional CSS classes. It works both as a standalone page and as a Page Builder preview. From 4ff8a5ae9a075bfc68f485e516742e33f9bdbde6 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:24:09 +0200 Subject: [PATCH 16/23] Apply suggestion from @dabrt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index cf24bcc780..85c8c9eb01 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -215,7 +215,7 @@ The confirmation `data` property object contains: ### On field update -The `PB:UPDATE_FIELD_DATA` message is sent from the Page Builder to the frontend resource when the Landing page field value is updated. +When the **Landing page* field value is updated, the Page Builder sends the `PB:UPDATE_FIELD_DATA` message to the frontend resource. Its data contains the field's new value, with the following structure: From 2173ec79a8f7d566f7ed86119b66f2b1cf33d592 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:25:50 +0200 Subject: [PATCH 17/23] Apply suggestion from @dabrt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 85c8c9eb01..ccf54389da 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -217,7 +217,7 @@ The confirmation `data` property object contains: When the **Landing page* field value is updated, the Page Builder sends the `PB:UPDATE_FIELD_DATA` message to the frontend resource. -Its data contains the field's new value, with the following structure: +The message's data contains the field's new value, with the following structure: - the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks. - the list of the existing block types, their attributes, and their configuration (`blocksConfig`) From 6ca5f96831d0b086bcac8211b59d266350328fc0 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:26:08 +0200 Subject: [PATCH 18/23] Apply suggestion from @dabrt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index ccf54389da..e94395d513 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -219,7 +219,7 @@ When the **Landing page* field value is updated, the Page Builder sends the `PB: The message's data contains the field's new value, with the following structure: -- the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks. +- the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks - the list of the existing block types, their attributes, and their configuration (`blocksConfig`) - the block-ID-to-name mapping (`blocksIdMap`) - the list of IDs from the new blocks that have been added (`highlightedBlockIds`) From 756a1353bcc655ad15cc8c24b68d9ee17df081be Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:26:30 +0200 Subject: [PATCH 19/23] Apply suggestion from @dabrt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> --- docs/content_management/pages/headless_page_builder.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index e94395d513..2c71b5b953 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -220,9 +220,9 @@ When the **Landing page* field value is updated, the Page Builder sends the `PB: The message's data contains the field's new value, with the following structure: - the current value of the Landing page field being edited (`fieldValue`), including the layout, zones, and blocks -- the list of the existing block types, their attributes, and their configuration (`blocksConfig`) +- a list of the existing block types, their attributes, and their configuration (`blocksConfig`) - the block-ID-to-name mapping (`blocksIdMap`) -- the list of IDs from the new blocks that have been added (`highlightedBlockIds`) +- a list of IDs from the new blocks that have been added (`highlightedBlockIds`) ```json { From 0090d6d6a0b543a1e7529e94460af0a707e78c22 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:26:53 +0200 Subject: [PATCH 20/23] Apply suggestion from @dabrt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 2c71b5b953..bc4ca9dc44 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -272,7 +272,7 @@ window.addEventListener('ibexa-post-update-blocks-preview', (customEvent) => { ### Geometry and pointer tracking (`blocks.geometry` and `pointer.tracking`) Pointer tracking and geometry help the Page Builder position block-editing menus over the frontend preview. -These menus are `.c-pb-headless-preview-menu` elements positioned by the Page Builder in its DOM above the preview `iframe`. +The menus are the `.c-pb-headless-preview-menu` elements that the Page Builder positions in its DOM above the preview `iframe`. The frontend preview sends the `APP:MOUSE_POSITION` message to report the mouse's current position to the Page Builder. From 48eebd5984d2b34ce4f83cc40e3cffc9adbafad4 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:28:48 +0200 Subject: [PATCH 21/23] Apply suggestion from @dabrt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomasz DÄ…browski <64841871+dabrt@users.noreply.github.com> --- .../pages/headless_page_builder.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index bc4ca9dc44..9d25484491 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -62,24 +62,24 @@ Handshake and core messages are mandatory and don't depend on optional capabilit | Capability | Message type | Description | |--------------------|---------------------------------------------------------------------------------------------|--------------------------------------| -| (handshake) | [`APP:INITIALIZED`](#communication-initialization) | Establish protocol and capabilities. | -| (handshake) | [`PB:INIT_MODE`](#communication-initialization) | Confirm protocol and draft info. | -| (core) | [`PB:UPDATE_FIELD_DATA`](#on-field-update) | Send updated field data. | -| (core) | [`PB:DISPATCH_EVENT`](#re-dispatching-events) | Re-dispatch a frontend event. | -| `blocks.dnd` | [`PB:DRAG_START_PREVIEW`](#drag-and-drop-blocksdnd) | Existing block drag started. | -| `blocks.dnd` | [`PB:DRAG_OVER`](#drag-and-drop-blocksdnd) | Mouse position during drag. | -| `blocks.dnd` | [`PB:DRAG_END_PREVIEW`](#drag-and-drop-blocksdnd) | Existing block drag ended. | +| (handshake) | [`APP:INITIALIZED`](#communication-initialization) | Establishes protocol and capabilities. | +| (handshake) | [`PB:INIT_MODE`](#communication-initialization) | Confirms protocol and draft info. | +| (core) | [`PB:UPDATE_FIELD_DATA`](#on-field-update) | Sends updated field data. | +| (core) | [`PB:DISPATCH_EVENT`](#re-dispatching-events) | Re-dispatches a frontend event. | +| `blocks.dnd` | [`PB:DRAG_START_PREVIEW`](#drag-and-drop-blocksdnd) | Informs that existing block dragging has started. | +| `blocks.dnd` | [`PB:DRAG_OVER`](#drag-and-drop-blocksdnd) | Passes mouse position during dragging. | +| `blocks.dnd` | [`PB:DRAG_END_PREVIEW`](#drag-and-drop-blocksdnd) | Informs that existing block dragging has ended. | | `blocks.dnd` | [`PB:DROP`](#drag-and-drop-blocksdnd) | Drop notification. | -| `blocks.dnd` | [`APP:DROP_RESPONSE`](#drag-and-drop-blocksdnd) | Report where the block was dropped. | -| `blocks.dnd` | [`PB:SCROLL_BY`](#drag-and-drop-blocksdnd) | Scroll the preview. | -| `blocks.geometry` | [`APP:POSITIONS_UPDATE`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Report block positions. | -| `blocks.geometry` | [`APP:SCROLL_END`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Scroll ended. | -| `blocks.remove` | [`PB:BLOCK_REMOVE`](#block-removal-blocksremove) | Remove a block. | -| `blocks.remove` | [`APP:BLOCK_REMOVE_RESPONSE`](#block-removal-blocksremove) | Confirm removal. | -| `blocks.remove` | [`APP:BLOCK_REMOVE_REQUEST`](#block-removal-blocksremove) | Request block removal. | -| `blocks.reveal` | [`PB:SCROLL_INTO_BLOCK`](#block-reveal-blocksreveal) | Scroll a block into view. | -| `blocks.select` | `APP:BLOCK_CLICKED` | Block clicked. | -| `pointer.tracking` | [`APP:MOUSE_POSITION`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Report mouse position. | +| `blocks.dnd` | [`APP:DROP_RESPONSE`](#drag-and-drop-blocksdnd) | Reports where the block was dropped. | +| `blocks.dnd` | [`PB:SCROLL_BY`](#drag-and-drop-blocksdnd) | Scrolls the preview. | +| `blocks.geometry` | [`APP:POSITIONS_UPDATE`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Reports block positions. | +| `blocks.geometry` | [`APP:SCROLL_END`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Informs that scrolling has ended. | +| `blocks.remove` | [`PB:BLOCK_REMOVE`](#block-removal-blocksremove) | Removes a block. | +| `blocks.remove` | [`APP:BLOCK_REMOVE_RESPONSE`](#block-removal-blocksremove) | Confirms removal. | +| `blocks.remove` | [`APP:BLOCK_REMOVE_REQUEST`](#block-removal-blocksremove) | Requests block removal. | +| `blocks.reveal` | [`PB:SCROLL_INTO_BLOCK`](#block-reveal-blocksreveal) | Scrolls a block into view. | +| `blocks.select` | `APP:BLOCK_CLICKED` | Informs that block is clicked. | +| `pointer.tracking` | [`APP:MOUSE_POSITION`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Reports mouse position. | ### Communication initialization From e3ada604c9d53a7199cab5664e30be5ce35bdf10 Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:31:25 +0200 Subject: [PATCH 22/23] headless_page_builder.md: Update anchors after section renaming --- .../pages/headless_page_builder.md | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 9d25484491..06cbd078c8 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -60,26 +60,26 @@ The frontend can declare which capabilities it supports, so that the Page Builde Handshake and core messages are mandatory and don't depend on optional capabilities. -| Capability | Message type | Description | -|--------------------|---------------------------------------------------------------------------------------------|--------------------------------------| -| (handshake) | [`APP:INITIALIZED`](#communication-initialization) | Establishes protocol and capabilities. | -| (handshake) | [`PB:INIT_MODE`](#communication-initialization) | Confirms protocol and draft info. | -| (core) | [`PB:UPDATE_FIELD_DATA`](#on-field-update) | Sends updated field data. | -| (core) | [`PB:DISPATCH_EVENT`](#re-dispatching-events) | Re-dispatches a frontend event. | -| `blocks.dnd` | [`PB:DRAG_START_PREVIEW`](#drag-and-drop-blocksdnd) | Informs that existing block dragging has started. | -| `blocks.dnd` | [`PB:DRAG_OVER`](#drag-and-drop-blocksdnd) | Passes mouse position during dragging. | -| `blocks.dnd` | [`PB:DRAG_END_PREVIEW`](#drag-and-drop-blocksdnd) | Informs that existing block dragging has ended. | -| `blocks.dnd` | [`PB:DROP`](#drag-and-drop-blocksdnd) | Drop notification. | -| `blocks.dnd` | [`APP:DROP_RESPONSE`](#drag-and-drop-blocksdnd) | Reports where the block was dropped. | -| `blocks.dnd` | [`PB:SCROLL_BY`](#drag-and-drop-blocksdnd) | Scrolls the preview. | -| `blocks.geometry` | [`APP:POSITIONS_UPDATE`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Reports block positions. | -| `blocks.geometry` | [`APP:SCROLL_END`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Informs that scrolling has ended. | -| `blocks.remove` | [`PB:BLOCK_REMOVE`](#block-removal-blocksremove) | Removes a block. | -| `blocks.remove` | [`APP:BLOCK_REMOVE_RESPONSE`](#block-removal-blocksremove) | Confirms removal. | -| `blocks.remove` | [`APP:BLOCK_REMOVE_REQUEST`](#block-removal-blocksremove) | Requests block removal. | -| `blocks.reveal` | [`PB:SCROLL_INTO_BLOCK`](#block-reveal-blocksreveal) | Scrolls a block into view. | -| `blocks.select` | `APP:BLOCK_CLICKED` | Informs that block is clicked. | -| `pointer.tracking` | [`APP:MOUSE_POSITION`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Reports mouse position. | +| Capability | Message type | Description | +|--------------------|---------------------------------------------------------------------------------------------|---------------------------------------------------| +| (handshake) | [`APP:INITIALIZED`](#communication-initialization) | Establishes protocol and capabilities. | +| (handshake) | [`PB:INIT_MODE`](#communication-initialization) | Confirms protocol and draft info. | +| (core) | [`PB:UPDATE_FIELD_DATA`](#on-field-update) | Sends updated field data. | +| (core) | [`PB:DISPATCH_EVENT`](#re-dispatching-events) | Re-dispatches a frontend event. | +| `blocks.dnd` | [`PB:DRAG_START_PREVIEW`](#dragging-and-dropping-blocksdnd) | Informs that existing block dragging has started. | +| `blocks.dnd` | [`PB:DRAG_OVER`](#dragging-and-dropping-blocksdnd) | Passes mouse position during dragging. | +| `blocks.dnd` | [`PB:DRAG_END_PREVIEW`](#dragging-and-dropping-blocksdnd) | Informs that existing block dragging has ended. | +| `blocks.dnd` | [`PB:DROP`](#dragging-and-dropping-blocksdnd) | Drop notification. | +| `blocks.dnd` | [`APP:DROP_RESPONSE`](#dragging-and-dropping-blocksdnd) | Reports where the block was dropped. | +| `blocks.dnd` | [`PB:SCROLL_BY`](#dragging-and-dropping-blocksdnd) | Scrolls the preview. | +| `blocks.geometry` | [`APP:POSITIONS_UPDATE`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Reports block positions. | +| `blocks.geometry` | [`APP:SCROLL_END`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Informs that scrolling has ended. | +| `blocks.remove` | [`PB:BLOCK_REMOVE`](#block-removal-blocksremove) | Removes a block. | +| `blocks.remove` | [`APP:BLOCK_REMOVE_RESPONSE`](#block-removal-blocksremove) | Confirms removal. | +| `blocks.remove` | [`APP:BLOCK_REMOVE_REQUEST`](#block-removal-blocksremove) | Requests block removal. | +| `blocks.reveal` | [`PB:SCROLL_INTO_BLOCK`](#block-reveal-blocksreveal) | Scrolls a block into view. | +| `blocks.select` | `APP:BLOCK_CLICKED` | Informs that block is clicked. | +| `pointer.tracking` | [`APP:MOUSE_POSITION`](#geometry-and-pointer-tracking-blocksgeometry-and-pointertracking) | Reports mouse position. | ### Communication initialization From 1d5b32cba8c63a01e52c152881ee8620f5ae8a8f Mon Sep 17 00:00:00 2001 From: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:38:24 +0200 Subject: [PATCH 23/23] Update docs/content_management/pages/headless_page_builder.md --- docs/content_management/pages/headless_page_builder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content_management/pages/headless_page_builder.md b/docs/content_management/pages/headless_page_builder.md index 06cbd078c8..39218c316c 100644 --- a/docs/content_management/pages/headless_page_builder.md +++ b/docs/content_management/pages/headless_page_builder.md @@ -491,7 +491,7 @@ You can name CSS classes as you wish, but following these conventions can make e ### CSS classes and data attribute conventions -By convention, some class names are used only in the Page Builder preview, while others are always used. +By convention, some class names are used only in the Page Builder preview, while others are used in both final frontend and Page Builder frontend preview. For example, when the frontend is used in the Page Builder preview, the `c-pb-iframe__preview-body` class is added to the document body.