Page Builder Module
The Page Builder module provides admin tooling for composing, saving, and publishing pages with an embedded Webstudio client.
Responsibilities
- Page CRUD in the admin interface
- Embedded editor shell for page composition
- Persisted page-builder UI state per page
- Media asset upload and deletion from the editor
- Publish and draft state management per page
- Registry, contract, and compatibility endpoints required by Webstudio
Current integration model
The current integration no longer uses an external iframe host as the primary editor runtime.
- Laravel serves a module-owned SPA shell at
/{admin_prefix}/page-builder/editor-spa/{path?}. - The shell view is
modules/page-builder/resources/views/editor-spa.blade.php. - The shell loads the built Webstudio client through Laravel Vite:
@vite('resources/js/webstudio-vite-entry.js', 'build/page-builder')
- The Webstudio client is built inside the module and copied to Laravel public assets.
- Admin
Pagesopens this SPA route, and Webstudio bootstraps fromwindow.__pagifyWebstudioBootstrap.
Key admin routes
/{admin_prefix}/page-builder/pages/{admin_prefix}/page-builder/pages/{page}/preview/{admin_prefix}/page-builder/editor-spa/{path?}
Key API groups
Primary admin APIs:
api/v1/{admin_prefix}/page-builder/editor/access-tokenapi/v1/{admin_prefix}/page-builder/pagesapi/v1/{admin_prefix}/page-builder/pages/{page}api/v1/{admin_prefix}/page-builder/pages/{page}/publishapi/v1/{admin_prefix}/page-builder/foldersapi/v1/{admin_prefix}/page-builder/folders/{folderId}api/v1/{admin_prefix}/page-builder/folders/move
Webstudio compatibility APIs:
GET api/v1/{admin_prefix}/page-builder/data/{projectId}POST api/v1/{admin_prefix}/page-builder/patchPOST api/v1/{admin_prefix}/page-builder/resources-loaderGET|POST api/v1/{admin_prefix}/page-builder/assetsPOST api/v1/{admin_prefix}/page-builder/assets/{name}DELETE api/v1/{admin_prefix}/page-builder/assets/{assetId}GET|POST api/v1/{admin_prefix}/page-builder/trpc/{path?}POST api/v1/{admin_prefix}/page-builder/dashboard-logout
Webstudio asset proxy endpoints:
GET /cgi/image/{path?}GET /cgi/asset/{path?}
Data model notes
projectId in the compatibility layer maps to the current Pagify page id.
- Each page has its own
PageBuilderStaterow. PATCHpersistence is page-scoped, not global project-scoped.data_jsonstores only persisted UI state required to rebuild the editor surface.- Dynamic data such as page tree metadata and current publish status should come from live server data, not from the persisted snapshot.
- Folder tree is persisted in a site-scoped folder domain, and page-folder assignment is stored per page.
GET /data/{projectId} behavior
The compatibility data endpoint is the source of truth for bootstrapping Webstudio for a specific page.
- Returns
id,version, andprojectIdfor the selected page state. - Returns persisted design state such as
instances,props,styles,styleSources,styleSourceSelections,resources, anddataSources. - Returns page tree metadata from the current database state so page title, slug, and publish state stay fresh when switching pages.
- Returns folder tree from server-authoritative folder domain (including nested folders and ordered children).
- Returns media assets mapped from the Pagify media system.
Folder APIs behavior
Page folders are managed through dedicated APIs instead of local-only editor state.
GET /foldersreturns folder tree payload for editor synchronization.POST /folderscreates a folder in current site scope.PUT /folders/{folderId}updates folder metadata or parent.POST /folders/movesupports reorder/reparent for both folders and pages.DELETE /folders/{folderId}removes folder and reparents page children to root.
POST /patch behavior
The compatibility patch endpoint persists changes for the currently selected page.
- Client sends transaction batch plus a reduced
statesnapshot. - Snapshot should include only persisted UI state required for editor reconstruction.
- Frequently changing dynamic data should not be embedded into the snapshot if it already exists in server APIs.
- Versioning is optimistic and checked per page state record.
Assets behavior
The editor asset manager is backed by the Pagify media system.
- Uploading an asset stores it in the media library and returns Webstudio-compatible asset payloads.
- Deleting an asset from Webstudio also deletes the corresponding system asset.
- Asset
nameis returned as a relative path so Webstudio image URLs resolve cleanly through/cgi/image/.... /cgi/image/*and/cgi/asset/*exist as compatibility proxies for Webstudio loaders.
Publish behavior
Publish state is controlled per page.
- The Publish switch must reflect the currently selected page.
- Switching pages should reload publish state for that page from live server data.
- Persisted builder snapshots must not override live publish status from the database.
Lifecycle
- Open
Pagesin admin. - Laravel serves the Webstudio SPA shell.
- Webstudio boots with page-scoped compatibility data from
/data/{projectId}. - User edits the selected page.
- Webstudio mutates page/folder tree through folder/page APIs.
- Webstudio saves page-scoped UI state through
/patch. - User toggles draft or publish state through page CRUD APIs.
Admin fullscreen
Admin editor shell provides a fullscreen toggle for easier editing.
- Uses browser Fullscreen API when available.
- Falls back to opening editor URL in a new full window/tab when Fullscreen API is unavailable.
Operational notes
- Rebuild and publish the Webstudio client whenever the editor frontend changes.
- Keep the Vite output namespace consistent with
build/page-builder. - If the editor loads but stays at loading state, verify iframe embedding, canvas sync, and the compatibility
/dataresponse. - If thumbnails fail, inspect
/cgi/image/*responses and returned assetnamevalues. - If page edits save to the wrong record, verify the selected page id is also the
projectIdsent to/patch.
Testing coverage highlights
- page builder lifecycle
- embedded Webstudio shell bootstrap
- page-scoped compatibility data
- asset upload and delete compatibility
- publish state hydration from live database state
Register Component for Webstudio
Page Builder supports component registration from both module and plugin by using class-based component definitions.
Direct quickstart guide:
Discovery flow
- Page Builder subscribes hook
page-builder.webstudio.componentson Event Bus. - Discovery service scans enabled modules/plugins for class references in the main config.
- Definitions are normalized and validated by
ComponentDefinitionDiscoveryService+ComponentDefinitionValidatorwith owner metadata. - Registry endpoint returns owner-aware blocks.
- Compatibility
GET /data/{projectId}returnsregisteredComponentsfor editor bootstrap. - Webstudio Components tab groups registered items by owner (module/plugin).
Module main config
Add webstudio_components in modules/{module-slug}/config/module.php:
<?php
use Vendor\YourModule\Webstudio\Components\HeroBannerComponent;
return [
// ...existing module config
'webstudio_components' => [
HeroBannerComponent::class,
],
];
Plugin main config
Create plugins/{plugin-slug}/config/plugin.php and declare webstudio_components:
<?php
use Plugins\DemoWebstudioRegister\Webstudio\Components\CtaStripComponent;
return [
'webstudio_components' => [
CtaStripComponent::class,
],
];
Each component class must implement Pagify\PageBuilder\Webstudio\Contracts\CustomComponent and return a definition array.
Dynamic fallback via arbitrary definition() objects is not supported.
To reduce verbosity and avoid missing fields, you can use Pagify\PageBuilder\Webstudio\Support\ComponentDefinitionBuilder:
<?php
use Pagify\PageBuilder\Webstudio\Support\ComponentDefinitionBuilder;
return ComponentDefinitionBuilder::make('hero-banner', 'Hero Banner')
->description('Starter hero block')
->element('section')
->classes(['hero', 'hero--primary'])
->styles([
'padding' => '24px',
'border-radius' => '12px',
])
->attribute('data-variant', 'hero')
->text('Hero Banner')
->toArray();
Supported fields
key(required): unique component key in owner scopelabel(optional): display name in Components tabdescription(optional): tooltip/description texticon(optional): icon stringcategory(optional): legacy category metadataowner(optional): module/plugin slug for groupingowner_type(optional):moduleorpluginhtml_template(optional): fallback snippet for legacy block renderingelementortag(optional): HTML element name used whenhtml_templateis omittedclass(optional): string or array of css classesstyle(optional): inline style string or key/value arrayattributes(optional): HTML attributes map (data-*,role, etc.)textorinner_html(optional): inner content for generated templatechildren(optional): nested child nodes/components for template compositiondynamic_data(optional): dynamic context payload for server-side pre-render in editor APIsprops_schema(optional): custom props metadata
children supports:
- string: references another registered component key (for example
hero-bannerordemo-webstudio-register:hero-banner) - object node: supports
key/componentreference, or inlineelement/tag+class+style+attributes+text+inner_html+ recursivechildren
Example:
return ComponentDefinitionBuilder::make('cta-strip', 'CTA Strip')
->tag('div')
->classes('cta-strip')
->children([
'hero-banner',
[
'element' => 'p',
'text' => 'Nested note',
],
])
->toArray();
Dynamic data pre-render before editor
Custom component definitions support server-side placeholder rendering before payload is sent to Webstudio editor.
- Placeholder format:
{{ page.title }},{{ page.slug }},{{ dynamic.summary }},{{ now }} - Rendering is applied in both:
GET /api/v1/{admin_prefix}/page-builder/data/{projectId}
- Target fields are rendered before editor bootstrap:
label,description,html_template,text,inner_html,class,style, attribute values, nestedchildren, anddynamic_data.
Supported placeholder namespaces (via context helper):
page.*project.*runtime.*dynamic.*now
Fail-fast validation:
- Invalid placeholder root (for example
{{ foo.bar }}) makes the component definition invalid and it will be skipped. - Invalid/malformed placeholder syntax (for example missing
}}) makes the component definition invalid and it will be skipped. dynamic.*must reference an existing key indynamic_data; unknown paths are rejected.
Example:
return ComponentDefinitionBuilder::make('hero-banner', 'Hero Banner')
->dynamicData([
'summary' => 'Page: {{ page.title }}',
])
->description('Dynamic summary: {{ dynamic.summary }}')
->attribute('data-page', '{{ page.slug }}')
->text('Welcome to {{ page.title }}')
->toArray();
Normalization rules
- If
owneris missing, owner defaults to module/plugin slug. - If
keyhas no namespace, key is prefixed as{owner}:{key}. - If
html_templateis missing andelement/tagis provided, template is generated from element + class/style/attributes. - If neither
html_templatenorelement/tagis provided, a default section template is generated. - Invalid component definitions (missing/invalid
key) are skipped before registry/data payload. - Missing optional fields are auto-normalized (
label,icon,category,owner_type,attributes,props_schema).
End-to-end verification checklist
- Add or update component class and register it in module/plugin main config.
- Ensure module/plugin is enabled.
- Open
GET /api/v1/{admin_prefix}/page-builder/data/{projectId}and verifyregisteredComponentsincludeowner,owner_type,source, and normalizedkey. - Open
GET /api/v1/{admin_prefix}/page-builder/data/{projectId}and verifyregisteredComponentspayload. - Open editor and confirm Components tab shows a group named by owner with registered items.
CI validation command
Run command below in CI to fail early when webstudio_components has invalid classes or invalid definitions:
php artisan cms:page-builder:validate-webstudio-components
Optional machine-readable output:
php artisan cms:page-builder:validate-webstudio-components --json