Appearance
Page Builder (Admin)
Flow ID: AD-29 Module(s): builder Complexity: High Last Updated: 2026-04-04
Business Context
The Page Builder is a WYSIWYG block editor powered by GrapesJS that allows admin users to create reusable HTML/CSS/JS content blocks. These blocks can be attached to various entity types (products, categories, vendors, blog articles, CMS pages, promo pages) on a per-language basis via builder_block_id foreign keys in their respective _mui tables. The feature is gated by the BUILDER.ENABLED registry flag, which controls both the admin menu visibility and frontend rendering.
Architecture
The builder follows a two-part architecture:
- Editor (GrapesJS) -- A full WYSIWYG page builder that runs on the storefront frontend layout (
Front_c), not the admin layout. This is deliberate: the editor canvas loads the storefront CSS/JS so blocks render exactly as they will appear to customers. - Listing (Admin Panel) -- A standard admin CRUD listing (
Admin_c) for managing existing blocks (clone, rename, delete, filter by position).
HMVC Module Structure
ecommercen/builder/
controllers/
AdvBlockBuilderController.php -- Editor (extends Front_c, uses ApiEndpointTrait)
AdvBlockBuilderListing.php -- Listing (extends Admin_c)
libraries/
AdvBlockBuilder.php -- Core service: enabled check, frontend/admin rendering
AdvBuilderConfig.php -- Abstract singleton: position config, container HTML
AdvBuilderBlockViewData.php -- Value object for frontend block rendering
models/
AdvBlockBuilderModel.php -- DB access: CRUD, admin listing with position counts, cascade deleteDatabase Schema
builder_blocks table:
| Column | Type | Description |
|---|---|---|
id | int(11) PK AUTO_INCREMENT | Block identifier |
title | varchar(255) | Admin-facing block title |
content | longtext | GrapesJS project data (JSON) |
html | longtext | Compiled HTML output |
css | longtext | Compiled CSS output |
js | longtext | Compiled JavaScript output |
The content column stores the GrapesJS editor state (component tree, styles, etc.) for re-editing. The html, css, and js columns store the compiled output rendered on the frontend.
Foreign Key Relationships
Blocks are referenced via builder_block_id columns in 6 MUI tables:
| MUI Table | Position Key | Entity |
|---|---|---|
blog_mui | blogArticle | Blog articles |
categories_mui | cmsPage | CMS pages |
shop_product_category_mui | categoryDescription | Product categories |
shop_product_mui | productDescription | Products |
shop_promo_mui | promoPage | Promo/landing pages |
shop_vendor_mui | vendorDescription | Vendors (description) |
shop_vendor_mui | vendorExclusive | Vendors (exclusive page, uses exclusive_builder_block_id) |
All references are nullable. When a block is deleted, all FK references are nullified in a transaction (not cascaded via DB constraints -- handled in application code).
Key Features
GrapesJS Editor
The editor initializes in a #block-builder container on the storefront layout. Key configuration:
- Canvas: Loads storefront stylesheets (
main.css,homepage.css) and scripts (vendor.js,vendors.js) plus Slick Carousel, ensuring WYSIWYG fidelity. - Storage: Remote storage via
/block-builder/saveBlock(POST) and/block-builder/loadBlock/{id}(GET). Autosave is disabled; users must explicitly publish. - Asset Manager: Upload to
/block-builder/saveAssets, files stored infiles/builder/via Flysystem. Existing assets loaded from storage listing on editor init. - JS in HTML: Disabled (
jsInHtml: false). JavaScript is stored/rendered separately.
Editor Plugins
8 GrapesJS plugins loaded:
| Plugin | Purpose |
|---|---|
grapesjs-preset-webpage | Standard webpage editing preset |
grapesjs-blocks-basic | Basic layout blocks (flex grid) |
grapesjs-touch | Touch device support |
grapesjs-style-bg | Background style controls |
grapesjs-style-gradient | CSS gradient editor |
grapesjs-style-filter | CSS filter editor |
grapesjs-custom-code | Raw HTML/CSS/JS injection |
test-blocks (custom) | 34 custom block templates |
Custom Block Templates (34 files)
Organized in assets/admin/js/builder/blocks/:
- Promo pages: 6 landing page layouts (
promo-page-1throughpromo-page-6) - Hero sections: 4 templates (cover variations, animated text+image, text+CTA)
- Navigation: 3 horizontal nav lists (basic, with background, animated)
- Banners: Grid, border, round images, text, card, image banner blocks
- Cards/Lists: Horizontal card containers, 4-item and 5-item containers, list with title
- Headers: 3 header component variations
- Grid: Block grid container
- Slider: Custom slider component with prev/next navigation
Custom RTE (Rich Text Editor) Extensions
The editor extends the default GrapesJS RTE with:
- Font size control -- Increment/decrement buttons with numeric input
- Font family dropdown -- 13 web-safe font families
- Text color picker -- Color input with link-aware application
- Text alignment -- Cycle through left/center/right
- List style toggle -- Normal text to UL to OL and back
- Text spacing -- Sliders for letter-spacing, line-height, vertical alignment (flex)
- Copy style -- Paint-roller tool to copy styles between components
- Link customization -- Modal for href, title, target attributes
Custom Toolbar Buttons
Per-component toolbar adds 5 commands:
- Background color -- Color picker with opacity slider (RGBA)
- Padding settings -- Horizontal/vertical sliders with advanced per-side controls
- Margin settings -- Same as padding but for margins
- Wrap with link -- Convert any element to
<a>with link modal - Copy style -- Clone styles to another component
Panel Buttons
- Back to panel -- Returns to admin dashboard (with unsaved changes detection)
- Set title -- Modal to rename the block
- Publish -- Saves block to server (store-data command)
- Exit -- Navigate away with unsaved changes prompt
Internationalization
GrapesJS i18n configured with Greek (el) as default locale, English (en) as fallback. Custom locale files at assets/admin/js/builder/editor/locales/ (el, en, es, it, ru, zh).
Admin Controllers
AdvBlockBuilderController (extends Front_c)
The editor controller. Extends Front_c (not Admin_c) because the editor needs the storefront layout for canvas fidelity.
| Method | Route | Description |
|---|---|---|
build($blockId) | block-builder / block-builder/{id} | Editor page. New block if no ID, edit existing if ID provided. |
view($blockId) | block-builder/view/{id} | Preview: renders compiled HTML in storefront layout |
saveBlock() | block-builder/saveBlock | JSON API: create or update block (id, title, html, css, js, data) |
loadBlock($blockId) | block-builder/loadBlock/{id} | JSON API: load GrapesJS project data for editor |
saveAssets() | block-builder/saveAssets | File upload to files/builder/ via advuploader |
getBlocks() | block-builder/getBlocks | JSON API: dropdown list of all blocks (id + title) |
Auth: isLoggedIn() check on all controller methods. No role-based restriction at controller level (menu visibility handles RBAC).
HTML sanitization on save: strips <body> tags and GrapesJS default CSS reset (* { box-sizing: border-box; } body {margin: 0;}).
Auto-generated title for new blocks: 'Titlos Block ' + random_string() (Greek).
AdvBlockBuilderListing (extends Admin_c)
Standard admin listing with position filtering and pagination.
| Method | Route | Description |
|---|---|---|
index($position, $offset) | block-builder/list/{position?}/{offset?} | Paginated listing (50/page). Position filter dropdown. |
cloneBlock($blockId) | block-builder/clone/{id} | Duplicate block with new random title |
deleteBlock($blockId) | block-builder/delete/{id} | Delete with cascade nullification of all FK references |
renameBlock($blockId) | block-builder/rename/{id} | Update title via POST newTitle |
Model Layer
AdvBlockBuilderModel extends Adv_base_model with GenericReadModel + GenericWriteModel traits.
Admin Listing Query
The adminList() method runs a complex query with 7 LEFT JOINs to count how many entities reference each block across all position types:
sql
SELECT t1.id, t1.title,
COALESCE(t2.blogArticle, 0) AS blogArticle,
COALESCE(t3.cmsPage, 0) AS cmsPage,
COALESCE(t4.categoryDescription, 0) AS categoryDescription,
COALESCE(t5.productDescription, 0) AS productDescription,
COALESCE(t6.promoPage, 0) AS promoPage,
COALESCE(t7.vendorDescription, 0) AS vendorDescription,
COALESCE(t8.vendorExclusive, 0) AS vendorExclusive
FROM builder_blocks t1
LEFT JOIN (SELECT builder_block_id, COUNT(*) ... FROM blog_mui ...) t2 ...
LEFT JOIN (SELECT builder_block_id, COUNT(*) ... FROM categories_mui ...) t3 ...
LEFT JOIN (SELECT builder_block_id, COUNT(*) ... FROM shop_product_category_mui ...) t4 ...
LEFT JOIN (SELECT builder_block_id, COUNT(*) ... FROM shop_product_mui ...) t5 ...
LEFT JOIN (SELECT builder_block_id, COUNT(*) ... FROM shop_promo_mui ...) t6 ...
LEFT JOIN (SELECT builder_block_id, COUNT(*) ... FROM shop_vendor_mui ...) t7 ... -- builder_block_id
LEFT JOIN (SELECT exclusive_builder_block_id, COUNT(*) ... FROM shop_vendor_mui ...) t8 ... -- exclusive_builder_block_idPosition filtering uses WHERE tN.positionKey > 0 clauses via filterPosition() match expression.
Cascade Delete
deleteWithRelations($id) runs a transaction that:
- Nullifies
builder_block_idin all 6 MUI tables - Nullifies
exclusive_builder_block_idinshop_vendor_mui - Deletes the block from
builder_blocks
Position System
The AdvBuilderConfig singleton (abstract, extended per-client in application/libraries/) reads application/config/builder.php to define 7 rendering positions with per-position HTML container wrappers:
| Position | Open Container | Close Container |
|---|---|---|
promoPage | <div class="d-block custom-builder-pg"> | </div> |
productDescription | (default) | (default) |
categoryDescription | <div class="container"> | </div> |
vendorDescription | <div class="container-fluid"> | </div> |
vendorExclusive | (default) | (default) |
blogArticle | (default) | (default) |
cmsPage | <div class="d-block custom-builder-pg"> | </div> |
Default container is empty string (no wrapper). null means use the default.
Frontend Rendering
Blocks are rendered on the storefront through the AdvBlockBuilder::frontRender() pipeline:
- Entity controller calls
$this->blockBuilder->frontRender($render, $muiRecord->builder_block_id, 'positionKey') frontRender()checksenabledflag and block ID, fetches block data, wraps inBuilderBlockViewDatawith position-specific containers- Block is added to
$render['blockData'][$position] - View template reads
$blockData['positionKey']and renders via thebuilder/viewcomponent - Footer JS iterates
$blockDataand outputs each block's JS in a<script>tag
The BuilderBlockViewData value object carries: key, title, css, js, html, openContainer, closeContainer.
Frontend controllers using builder blocks:
| Controller | Position | Call Site |
|---|---|---|
Adv_products.php | productDescription | Product detail page |
Adv_product_categories.php | categoryDescription | Category listing page |
Adv_vendors.php | vendorDescription | Vendor products listing |
Adv_vendors.php | vendorExclusive | Vendor exclusive/details page |
Adv_blog.php | blogArticle | Blog article page |
Adv_category.php | cmsPage | CMS page |
Adv_promo.php | promoPage | Promo/landing page |
For CMS and promo pages, the presence of a builder block switches the view template entirely (e.g., promoBuilder or cmsBuilder layout instead of the standard view).
Admin Integration Points
The AdvBlockBuilder::adminRender() method provides a block dropdown (builderBlocks) and enabled flag (builderEnabled) to entity admin forms. This is used in 6 admin controllers:
Adv_products_admin.php-- Product create/edit formsAdv_product_categories_admin.php-- Product category create/editAdv_vendors_admin.php-- Vendor create/edit (bothbuilder_block_idandexclusive_builder_block_id)Adv_blog_admin.php-- Blog article create/editAdv_category_admin.php-- CMS page create/editAdv_promo_admin.php-- Promo page create/edit
Each form renders a per-language dropdown (builder_block_id_{lang}) using the builderBlocks array from adminRender().
Asset Management
- Upload endpoint:
saveAssets()accepts multipart file uploads, stores infiles/builder/viaadvuploader->uploadFilesSameFolder() - Asset listing:
getAssets()uses Flysystem (storage()->listContents()) to enumeratefiles/builder/, filtering outindex.html,.gitignore,.gitkeep - Assets are passed to the GrapesJS asset manager on editor init via
window.builderEditor.assetManagerAssets
Routes
All routes defined in application/config/routes.php (lines 745-761), with locale-prefixed variants:
block-builder -> AdvBlockBuilderController::build()
block-builder/list -> AdvBlockBuilderListing::index('all')
block-builder/list/{position} -> AdvBlockBuilderListing::index($position)
block-builder/list/{position}/{offset} -> AdvBlockBuilderListing::index($position, $offset)
block-builder/clone/{id} -> AdvBlockBuilderListing::cloneBlock($id)
block-builder/delete/{id} -> AdvBlockBuilderListing::deleteBlock($id)
block-builder/rename/{id} -> AdvBlockBuilderListing::renameBlock($id)
block-builder/{id} -> AdvBlockBuilderController::build($id)
block-builder/{action} -> AdvBlockBuilderController::{action}()Admin Menu
Listed under the CONTENT group with roles: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA. Menu item hidden when BUILDER.ENABLED registry flag is false (filtered by AdminMenu::parseBuilderMenu()).
The builder editor controller is also whitelisted in siteModeAllowedControllers (accessible even when the site is in maintenance mode).
Build Pipeline
The builder JavaScript is compiled via Laravel Mix:
js
// webpack.mix.admin.js
.js('assets/admin/js/builder/index.js', 'public/ui/admin/dist/builder.js')Output: public/ui/admin/dist/builder.js. Loaded conditionally in footer_js.php when $isBuilderEditor is true, along with Toasted.js (toast notifications) and Font Awesome 6.
Business Rules
| Rule | Description |
|---|---|
| Feature gated | BUILDER.ENABLED registry flag controls menu visibility and frontRender() |
| Per-language blocks | Each MUI record can reference a different block per language |
| Cascade delete safe | Deleting a block nullifies all references, never orphans |
| No autosave | Editor requires explicit publish action |
| WYSIWYG fidelity | Editor canvas loads storefront CSS/JS for accurate preview |
| Storefront auth | Editor uses isLoggedIn() (any admin), not role-based checks |
| CMS/Promo view switching | Builder block presence overrides the default view template |
| Vendor dual blocks | Vendors support two builder blocks: description and exclusive page |
Migrations
| Migration | Description |
|---|---|
| Initial schema | builder_blocks table (id, title, content, css, html) |
20241008101631 | Add js LONGTEXT column to builder_blocks |
20241031133710 | Add builder_block_id to categories_mui |
20241031135126 | Add builder_block_id to blog_mui |
20241031154616 | Add builder_block_id + exclusive_builder_block_id to shop_vendor_mui |
20241031161307 | Add builder_block_id to shop_product_category_mui |
20241031164345 | Add builder_block_id to shop_product_mui |
Related Flows
- CF-35 Page Builder (Frontend) -- Storefront rendering of builder blocks
- AD-02 Product Management -- Product admin with builder block assignment
- AD-05 Category Management -- Category/CMS admin with builder block assignment
- AD-12 Blog Admin -- Blog admin with builder block assignment
- AD-19 Vendor Management -- Vendor admin with builder block assignment (description + exclusive)
- AD-48 CMS Static Pages -- CMS pages with builder block assignment via
builder_block_id - AD-49 Subcontent Blocks -- simpler text-only embeddable blocks (alternative to builder)
- SY-23 MUI Translation Pattern -- builder blocks attach to entities via
builder_block_idFK stored in per-language_muitables (products, categories, vendors, blog, CMS pages, promos) - SY-25 File Upload & Storage --
saveAssets()endpoint usesadvuploader->uploadFilesSameFolder()to store asset files infiles/builder/
Wiki Guide: Builder Guide -- developer reference for the page builder system