Appearance
Page Builder / Block System
Flow ID: CF-35 | Module(s): builder | Complexity: High
Business Overview
The Page Builder is a modular content block system that enables administrators to create reusable, visually designed HTML/CSS/JS blocks using a GrapesJS-powered WYSIWYG editor. These blocks can be assigned to various storefront entity types on a per-language basis, replacing or augmenting the default description/content area for that entity.
Key capabilities:
- Full drag-and-drop visual editor with 30+ pre-built block templates (hero sections, banner layouts, promo pages, navigation lists, card containers, headers)
- Custom CSS, HTML, and JavaScript per block -- all three are stored and rendered independently
- Per-language block assignment via MUI table foreign keys, enabling different visual content per locale
- Configurable container wrapping per position (e.g.,
container,container-fluid, or no wrapper) - Asset management with file upload to
files/builder/via the editor's built-in asset manager - Block reuse across multiple entities: a single block can be linked to many products, categories, etc.
- Clone, rename, and cascade-delete operations in the admin listing
Supported entity types (7 positions):
| Position Key | Entity | MUI Table | FK Column | Container Wrap |
|---|---|---|---|---|
productDescription | Product | shop_product_mui | builder_block_id | None |
categoryDescription | Product Category | shop_product_category_mui | builder_block_id | <div class="container"> |
vendorDescription | Vendor (listing) | shop_vendor_mui | builder_block_id | <div class="container-fluid"> |
vendorExclusive | Vendor (details) | shop_vendor_mui | exclusive_builder_block_id | None |
blogArticle | Blog Post | blog_mui | builder_block_id | None |
cmsPage | CMS Page | categories_mui | builder_block_id | <div class="d-block custom-builder-pg"> |
promoPage | Promo Page | shop_promo_mui | builder_block_id | <div class="d-block custom-builder-pg"> |
Feature gate: Controlled by the BUILDER.ENABLED registry flag. When disabled, neither the admin menu entry nor storefront block rendering is active.
Admin Routes (Internal API)
There is no REST API for the builder. All operations are handled via legacy HMVC admin controllers.
Listing Routes (Admin Panel -- Admin_c)
| Route | Method | Controller | Purpose |
|---|---|---|---|
GET /block-builder/list | index | AdvBlockBuilderListing | Paginated block list (all positions) |
GET /block-builder/list/{position} | index | AdvBlockBuilderListing | Filter list by position key |
GET /block-builder/list/{position}/{offset} | index | AdvBlockBuilderListing | Paginated, filtered list |
GET /block-builder/clone/{id} | cloneBlock | AdvBlockBuilderListing | Duplicate a block (new title, same content) |
GET /block-builder/delete/{id} | deleteBlock | AdvBlockBuilderListing | Delete with cascade across 7 MUI tables |
POST /block-builder/rename/{id} | renameBlock | AdvBlockBuilderListing | Update block title only |
Editor Routes (Storefront Layout -- Front_c)
| Route | Method | Controller | Purpose |
|---|---|---|---|
GET /block-builder | build | AdvBlockBuilderController | Create new block (GrapesJS editor) |
GET /block-builder/{id} | build | AdvBlockBuilderController | Edit existing block (GrapesJS editor) |
GET /block-builder/view/{id} | view | AdvBlockBuilderController | Preview block in storefront context |
POST /block-builder/saveBlock | saveBlock | AdvBlockBuilderController | Save/update block data (GrapesJS remote storage) |
GET /block-builder/loadBlock/{id} | loadBlock | AdvBlockBuilderController | Load block project data (GrapesJS remote storage) |
POST /block-builder/saveAssets | saveAssets | AdvBlockBuilderController | Upload files to files/builder/ |
GET /block-builder/getBlocks | getBlocks | AdvBlockBuilderController | JSON dropdown list for admin entity forms |
All routes are language-prefixed (/{lang}/block-builder/...) and require admin authentication (isLoggedIn()).
The editor runs on the storefront layout (Front_c), not the admin layout. This is deliberate -- the GrapesJS canvas loads storefront CSS/JS (main.css, homepage.css, vendor.js) so blocks render exactly as they will appear to customers.
Code Flow: Storefront Rendering
When a customer visits a storefront page (product, category, vendor, blog, CMS, or promo), the rendering follows this path:
1. Controller Fetches Entity Data
Each entity controller queries its main table joined with the _mui table, which includes the builder_block_id column. For example, in Adv_products.php:
php
// The SELECT includes: shop_product_mui.builder_block_id
$this->render = $this->blockBuilder->frontRender(
$this->render, $query->builder_block_id, 'productDescription'
);2. AdvBlockBuilder::frontRender() Checks Feature Gate and Block ID
php
public function frontRender(array $render, ?int $blockId, ?string $position, ...): array
{
if (!$this->enabled || !$blockId) {
return $render; // No-op if builder disabled or no block assigned
}
$block = $this->getData($blockId, $position);
if ($block) {
$render['blockData'][$block->key] = $block; // Key = position name
}
return $render;
}3. getData() Loads Block Content and Wraps with Container
php
public function getData(int $blockId, ?string $position = null, ...): ?BuilderBlockViewData
{
$data = $this->model->get($blockId); // SELECT * FROM builder_blocks WHERE id = ?
if (!$data) return null;
return new BuilderBlockViewData(
key: $position,
title: $data->title,
css: $data->css,
js: $data->js,
html: $data->html,
openContainer: $this->config->openContainer($position),
closeContainer: $this->config->closeContainer($position),
);
}4. CSS Injected in <head> (main.php)
php
<?php if (!empty($blockData) && is_array($blockData)) { ?>
<style>
<?php foreach ($blockData as $blockItem) { echo $blockItem->css; } ?>
</style>
<?php } ?>5. HTML Rendered in Entity View Template
Each entity view extracts the block for its position and either replaces or augments the default content:
Product (product.php): Replaces the description tab content entirely.
php
$blockViewData = $blockData['productDescription'] ?? null;
if ($blockViewData) :
echo $this->load->view($template->componentView('builder'), ['blockViewData' => $blockViewData]);
else :
echo $productData->description;
endif;Category (category_products_list.php): Replaces the header area (name, description, slider).
php
$blockViewData = $blockData['categoryDescription'] ?? null;
<?php if ($blockViewData) : ?>
<?= $this->load->view($template->componentView('builder'), ['blockViewData' => $blockViewData], true) ?>
<?php endif; ?>
<?php if (!$blockViewData) : ?>
<!-- Standard category header with name, slider, description -->
<?php endif; ?>Vendor Details (vendor_details.php): Replaces the entire banner/details section with exclusive block.
php
$blockViewData = $blockData['vendorExclusive'] ?? null;
if (!$blockViewData) :
<!-- Standard vendor banner layout -->
<?php else : ?>
<?= $this->load->view($template->componentView('builder'), ['blockViewData' => $blockViewData], true); ?>
<?php endif; ?>Vendor Listing (vendor_products_list.php): Replaces the listing header area.
Blog (blog_post.php): Replaces the entire article content.
CMS Page: If block exists, view switches to cmsBuilder layout; otherwise renders the standard page view.
Promo Page: If block exists, view switches to promoBuilder layout; otherwise uses the promo's view_type.
6. Shared Builder Component View
All positions render through the same component (components/builder/view.php):
php
echo $blockViewData->openContainer; // Position-specific wrapper (or empty)
echo $blockViewData->html; // Raw HTML from GrapesJS
echo $blockViewData->closeContainer; // Closing wrapper tag7. JavaScript Injected in Footer
Block JS is executed after page load in footer_js.php:
php
<?php if (!empty($blockData) && is_array($blockData)) { ?>
<script>
<?php foreach ($blockData as $blockItem) { echo $blockItem->js; } ?>
</script>
<?php } ?>Per-Entity Rendering Summary
| Entity | Behavior When Block Assigned | Storefront View File |
|---|---|---|
| Product | Replaces description tab content | layouts/product_page/product.php |
| Product Category | Replaces header area (name/description/slider) | layouts/product_category/category_products_list.php |
| Vendor Details | Replaces entire banner/details section | layouts/product_vendor/vendor_details.php |
| Vendor Listing | Replaces listing header | layouts/product_vendor/vendor_products_list.php |
| Blog Article | Replaces entire article body | layouts/blog/blog_post.php |
| CMS Page | Switches to cmsBuilder layout | layouts/builder/cms.php |
| Promo Page | Switches to promoBuilder layout | layouts/builder/promo.php |
Architecture
Module Layer Structure
The builder uses the two-tier HMVC pattern: core classes in ecommercen/builder/, application-level thin subclasses in application/modules/builder/.
| Layer | Component | Path | Description |
|---|---|---|---|
| Core Library | AdvBlockBuilder | ecommercen/builder/libraries/AdvBlockBuilder.php | Main business logic: enabled check, frontRender, getData, adminRender |
| Core Config | AdvBuilderConfig | ecommercen/builder/libraries/AdvBuilderConfig.php | Singleton; loads application/config/builder.php, resolves container wrapping per position |
| Core ViewData | AdvBuilderBlockViewData | ecommercen/builder/libraries/AdvBuilderBlockViewData.php | DTO: title, css, js, html, openContainer, closeContainer, key |
| Core Model | AdvBlockBuilderModel | ecommercen/builder/models/AdvBlockBuilderModel.php | DB layer: CRUD via GenericReadModel/GenericWriteModel traits, admin listing with position usage counts, cascade delete |
| Core Editor | AdvBlockBuilderController | ecommercen/builder/controllers/AdvBlockBuilderController.php | GrapesJS editor, save/load endpoints, asset management. Extends Front_c |
| Core Listing | AdvBlockBuilderListing | ecommercen/builder/controllers/AdvBlockBuilderListing.php | Admin listing with position filter, clone, rename, delete. Extends Admin_c |
| App Library | BlockBuilder | application/modules/builder/libraries/BlockBuilder.php | Thin subclass of AdvBlockBuilder |
| App Config | BuilderConfig | application/modules/builder/libraries/BuilderConfig.php | Thin subclass of AdvBuilderConfig (singleton) |
| App Model | Builder_model | application/modules/builder/models/Builder_model.php | Thin subclass of AdvBlockBuilderModel |
Frontend (GrapesJS Editor)
The GrapesJS editor is a standalone JS application compiled by Laravel Mix:
Entry point: assets/admin/js/builder/index.js -> public/ui/admin/dist/builder.js
Build config: webpack.mix.admin.js line 100
| File | Purpose |
|---|---|
editor/editor.js | Initializes GrapesJS with plugins, canvas styles, remote storage config, asset manager |
editor/plugins.js | Plugin list: preset-webpage, blocks-basic, touch, style-bg, style-gradient, style-filter, custom-code |
editor/commands.js | Custom commands: store-data, modal-title, open-url, change-bg-color, padding-settings, margin-settings, wrap-with-link, copy-style |
editor/buttons.js | Panel buttons: back-to-panel (ecommercen logo), set-title, publish, exit |
editor/toolbarButtons.js | Per-component toolbar: background color, padding, margin, wrap-with-link, copy-style |
editor/rte.js | Rich text enhancements: font size, font family, text color, text alignment, list style, text spacing (letter-spacing, line-height, vertical align), copy style, custom link modal |
editor/assets.js | Asset manager config: upload to /block-builder/saveAssets, loads from window.builderEditor.assetManagerAssets |
editor/i18n.js | i18n: Greek (default), English, Spanish, Italian, Russian, Chinese |
editor/t.js | Translation helper wrapping editor.I18n.t() |
blocks/test-blocks.js | Block registry: imports and registers all 30+ custom block templates |
blocks/grapes-custom-slider.js | Custom GrapesJS component type for image sliders |
GrapesJS Plugins:
| Plugin | npm Package | Purpose |
|---|---|---|
| Preset Webpage | grapesjs-preset-webpage | Standard webpage editing features |
| Blocks Basic | grapesjs-blocks-basic | Basic layout blocks (flex grid enabled) |
| Touch | grapesjs-touch | Touch device support |
| Style Background | grapesjs-style-bg | Background style panel |
| Style Gradient | grapesjs-style-gradient | Gradient editor |
| Style Filter | grapesjs-style-filter | CSS filter editor |
| Custom Code | grapesjs-custom-code | Custom HTML/CSS/JS component |
Canvas Configuration: The editor canvas loads the storefront CSS and JS to provide WYSIWYG fidelity:
/ui/main/css/main.css,/ui/main/css/homepage.css- Slick Carousel CSS from CDN
/ui/main/js/vendor.js,/ui/main/js/vendors.js- Slick Carousel JS from CDN
Pre-built Block Templates (30+ blocks in blocks/ directory):
| Category | Templates |
|---|---|
| Promo Pages | promo-page-1 through promo-page-6 |
| Hero Sections | hero-cover-1, hero-cover-2, hero-cover-3, hero-animated-text-image, hero-section-w-text-cta |
| Item Containers | block-four-items-container, block-five-items-container, block-grid-container |
| Card Layouts | horizontal-cards-container, horizontal-cards-container-2, card-banners-block, cardItem-1, cardItem-2 |
| Banner Layouts | grid-banners-container, border-banners-container, image-banners-block, round-images-block, text-banners-block |
| Navigation | horizontal-nav-list, horizontal-nav-list-w-background, horizontal-nav-list-animated |
| Headers | header-1, header-2, header-3 |
| Lists | list-w-title |
| Custom Components | grapes-custom-slider (slider with prev/next navigation) |
Storage Architecture
Remote Storage (GrapesJS storageManager):
- Save:
POST /block-builder/saveBlock-- sendsid,css,html,js,title,data(GrapesJS project JSON) - Load:
GET /block-builder/loadBlock/{id}-- returns{ id, data }wheredatais the GrapesJS project JSON - Autosave is disabled; saving is manual via the "Publish" button
- On first save (no
blockId), a new row is inserted and the new ID returned to the editor viawindow.builderEditor.builderProjectId
Asset Upload:
POST /block-builder/saveAssets-- multipart file upload- Files stored to
files/builder/viaAdvUploader::uploadFilesSameFolder() - File listing retrieved from Flysystem (
storage()->listContents('files/builder/')) - URLs returned as
assetUrl("files/builder/{filename}")for the GrapesJS asset manager
Admin Integration (Entity Forms)
Every entity admin controller (products, categories, vendors, blog, promo, CMS) integrates the builder via a consistent pattern:
- Controller instantiates
BlockBuilderand callsadminRender()to injectbuilderBlocks(dropdown) andbuilderEnabled(flag) into the render array - Create/Update views show a
<select class="js-builder-select">dropdown per language tab with action buttons:- Add (
+): Opens a new builder editor tab - Edit (pencil): Opens the selected block in editor, stores return URL in
localStorage - Refresh (reload): AJAX fetch of updated block list from
/block-builder/getBlocks - Clear (x): Clears the selection
- Add (
- Admin JS (
footer_js.php) toggles visibility of standard description fields (js-no-builder-data) when a builder block is selected - Save handler reads
builder_block_id_{lang}from POST and stores it in the MUI row
Data Model
builder_blocks Table
The central table storing all block content:
| Column | Type | Description |
|---|---|---|
id | INT (PK, AI) | Block identifier |
title | VARCHAR | Admin-facing block title |
content | LONGTEXT | GrapesJS project JSON (component tree, styles, etc.) |
html | LONGTEXT | Compiled HTML output from GrapesJS |
css | LONGTEXT | Compiled CSS output from GrapesJS |
js | LONGTEXT | Compiled JS output from GrapesJS |
Foreign Key References (7 MUI columns across 6 tables)
| MUI Table | FK Column | Index | Migration |
|---|---|---|---|
shop_product_mui | builder_block_id | Yes | 20241031164345_add_product_block_id.php |
shop_product_category_mui | builder_block_id | Yes | 20241031161307_add_product_category_block_id.php |
shop_vendor_mui | builder_block_id | Yes | 20241031154616_add_vendor_block_ids.php |
shop_vendor_mui | exclusive_builder_block_id | Yes | 20241031154616_add_vendor_block_ids.php |
blog_mui | builder_block_id | Yes | 20241031135126_add_blog_article_block_id.php |
categories_mui | builder_block_id | Yes | 20241031133710_add_cms_block_id.php |
shop_promo_mui | builder_block_id | Yes | (pre-existing in table schema) |
All FK columns are INT(11) DEFAULT NULL with non-unique indexes. There are no database-level foreign key constraints -- referential integrity is enforced by the application-level cascade delete in deleteWithRelations().
Admin Listing Query
The listing query joins builder_blocks with all 7 MUI tables via LEFT JOINs using subquery counts to show usage per position:
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 ...
LEFT JOIN (SELECT exclusive_builder_block_id, COUNT(*) ... FROM shop_vendor_mui ...) t8 ...Position filtering uses WHERE t{N}.{positionKey} > 0 to show only blocks used in a specific position.
Configuration
Builder Config (application/config/builder.php)
Defines all valid positions and their container wrapping behavior:
php
$config['builder'] = [
'positions' => [
'promoPage' => ['openContainer' => '<div class="d-block custom-builder-pg">', 'closeContainer' => '</div>'],
'productDescription' => ['openContainer' => null, 'closeContainer' => null],
'categoryDescription' => ['openContainer' => '<div class="container">', 'closeContainer' => '</div>'],
'vendorDescription' => ['openContainer' => '<div class="container-fluid">', 'closeContainer' => '</div>'],
'vendorExclusive' => ['openContainer' => null, 'closeContainer' => null],
'blogArticle' => ['openContainer' => null, 'closeContainer' => null],
'cmsPage' => ['openContainer' => '<div class="d-block custom-builder-pg">', 'closeContainer' => '</div>'],
],
'defaultOpenContainer' => '',
'defaultCloseContainer' => '',
];Container wrapping rules:
null= usedefaultOpenContainer/defaultCloseContainer(empty string = no wrapper)- Empty string
''= explicitly no container - Any string = use that HTML as the container wrapper
Registry Flag
| Group | Key | Values | Effect |
|---|---|---|---|
BUILDER | ENABLED | 1 / 0 | Controls admin menu visibility, admin form dropdown visibility (builderEnabled), and storefront rendering (frontRender no-ops when disabled) |
Admin Menu Entry
php
[
'route' => 'block-builder/list',
'icon' => t('admin.menu.builder.icon'),
'label' => t('admin.menu.builder.label'),
'roles' => [AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA],
'group' => 'CONTENT'
]The builder is accessible to Advisable, Admin, Marketing, and Media roles.
Template Config (application/config/mainTemplate.json / template.json)
Registers the shared builder component view:
json
"builder": {
"view": "components/builder/view",
"scss": ""
}Resolved by $template->componentView('builder') at render time.
Client Extension Points
Config Override (application/config/builder.php)
Client repos can modify position definitions, add new positions, or change container wrapping by overriding this file. Adding a new position also requires:
- A
builder_block_idcolumn in the target MUI table (migration) - A
frontRender()call in the storefront controller - A view template conditional to render the block
- Updating
deleteWithRelations()to nullify the new FK on cascade delete - Updating the admin listing SQL subqueries and
filterPosition()match cases
BlockBuilder / BuilderConfig Subclasses
Client repos override behavior via thin subclasses in application/modules/builder/libraries/:
BlockBuilder extends AdvBlockBuilder-- overridefrontRender(),getData(), oradminRender()for custom behaviorBuilderConfig extends AdvBuilderConfig-- override container resolution logic
View Templates
Client repos can customize how blocks appear per entity type by overriding the storefront view files in application/views/main/layouts/. The shared component view (components/builder/view.php) can also be overridden.
Asset Storage
Uploaded assets go to files/builder/. Clients can customize storage (local, S3, SFTP) via the Flysystem configuration since asset listing and URLs use the storage() helper and assetUrl().
Admin Form Integration
The builder dropdown appears in admin entity forms when builderEnabled is true. Client repos can customize the admin views to change placement or add additional builder-related controls.
GrapesJS Block Templates
Custom block templates are compiled into builder.js. Clients wanting additional templates would need to add block JS files and rebuild via npm run admin-production. The block template system uses GrapesJS's editor.Blocks.add() API with inline HTML/CSS content.
Business Rules
Feature gating: When
BUILDER.ENABLEDis0,frontRender()returns the render array unchanged, and admin forms hide the block dropdown. The admin menu entry is still registered but the dropdown/editor is not functional.Block content precedence: When a block is assigned to an entity, it replaces (not supplements) the default content area for that entity. For products, the block replaces the description tab. For categories and vendors, it replaces the header/description section. For CMS and promo pages, it switches the entire layout to a builder-specific layout file.
Per-language assignment: Blocks are assigned per language via the MUI table's
builder_block_idcolumn. Different languages can have different blocks (or no block) for the same entity. However, block content itself is not language-aware -- the same HTML/CSS/JS is rendered regardless of language.One block per position per entity: Each MUI row can reference at most one
builder_block_id. To display multiple blocks, the block itself must contain all desired content.Block reuse: A single block can be referenced by multiple entities across different positions. The admin listing shows usage counts per position.
Cascade delete:
deleteWithRelations()nullifies all FK references across all 7 MUI columns before deleting the block row, all within a database transaction. This does not delete the entities themselves -- it simply removes the block association, causing those entities to fall back to their default content rendering.Clone behavior: Cloning creates a new block with a random title (
"Titlos Block {random}") and copieshtml,css,js, andcontent(GrapesJS project data). The clone has no entity associations -- it must be manually linked to entities.Editor authentication: The GrapesJS editor runs on
Front_c(storefront layout) but requiresisLoggedIn()(admin session). This allows the canvas to use storefront CSS/JS for WYSIWYG accuracy.Unsaved changes protection: The editor tracks undo history via GrapesJS's
UndoManager. When exiting with unsaved changes, a modal prompts to save or discard. The return URL is stored inlocalStorageaslinkBeforeBuilder.HTML sanitization: The
saveBlockendpoint strips<body>tags and the default GrapesJS box-sizing reset from the CSS. No further sanitization is applied -- admin users are trusted with raw HTML/CSS/JS.Admin JS field toggling: When a builder block is selected in an entity admin form, standard description fields (marked with
js-no-builder-dataclass) are hidden to avoid confusion about which content will be displayed.
Related Flows
- CF-01 Product Browsing -- category description blocks (
categoryDescriptionposition) - CF-02 Product Detail -- product description blocks (
productDescriptionposition) - CF-12 Promotions -- promo page blocks (
promoPageposition) - CF-19 Vendors -- vendor description and exclusive blocks (
vendorDescription,vendorExclusivepositions) - CF-20 Blog -- blog article blocks (
blogArticleposition) - AD-29 Builder Admin -- admin-side documentation of the builder module
- AD-48 CMS Static Pages -- CMS page management (builder blocks assigned via
builder_block_id) - SY-25 File Upload & Storage — Builder asset uploads via Advuploader
- SY-28 Storage Abstraction — Asset listing via storage() Flysystem
Note: CMS page blocks (cmsPage position) replace category-based CMS page content. This operates through the categories module, not the documents module.
See also the Builder guide for setup instructions and usage reference.