Skip to content

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:

  1. 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.
  2. 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 delete

Database Schema

builder_blocks table:

ColumnTypeDescription
idint(11) PK AUTO_INCREMENTBlock identifier
titlevarchar(255)Admin-facing block title
contentlongtextGrapesJS project data (JSON)
htmllongtextCompiled HTML output
csslongtextCompiled CSS output
jslongtextCompiled 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 TablePosition KeyEntity
blog_muiblogArticleBlog articles
categories_muicmsPageCMS pages
shop_product_category_muicategoryDescriptionProduct categories
shop_product_muiproductDescriptionProducts
shop_promo_muipromoPagePromo/landing pages
shop_vendor_muivendorDescriptionVendors (description)
shop_vendor_muivendorExclusiveVendors (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 in files/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:

PluginPurpose
grapesjs-preset-webpageStandard webpage editing preset
grapesjs-blocks-basicBasic layout blocks (flex grid)
grapesjs-touchTouch device support
grapesjs-style-bgBackground style controls
grapesjs-style-gradientCSS gradient editor
grapesjs-style-filterCSS filter editor
grapesjs-custom-codeRaw 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-1 through promo-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.

MethodRouteDescription
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/saveBlockJSON 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/saveAssetsFile upload to files/builder/ via advuploader
getBlocks()block-builder/getBlocksJSON 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.

MethodRouteDescription
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_id

Position filtering uses WHERE tN.positionKey > 0 clauses via filterPosition() match expression.

Cascade Delete

deleteWithRelations($id) runs a transaction that:

  1. Nullifies builder_block_id in all 6 MUI tables
  2. Nullifies exclusive_builder_block_id in shop_vendor_mui
  3. 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:

PositionOpen ContainerClose 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:

  1. Entity controller calls $this->blockBuilder->frontRender($render, $muiRecord->builder_block_id, 'positionKey')
  2. frontRender() checks enabled flag and block ID, fetches block data, wraps in BuilderBlockViewData with position-specific containers
  3. Block is added to $render['blockData'][$position]
  4. View template reads $blockData['positionKey'] and renders via the builder/view component
  5. Footer JS iterates $blockData and 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:

ControllerPositionCall Site
Adv_products.phpproductDescriptionProduct detail page
Adv_product_categories.phpcategoryDescriptionCategory listing page
Adv_vendors.phpvendorDescriptionVendor products listing
Adv_vendors.phpvendorExclusiveVendor exclusive/details page
Adv_blog.phpblogArticleBlog article page
Adv_category.phpcmsPageCMS page
Adv_promo.phppromoPagePromo/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 forms
  • Adv_product_categories_admin.php -- Product category create/edit
  • Adv_vendors_admin.php -- Vendor create/edit (both builder_block_id and exclusive_builder_block_id)
  • Adv_blog_admin.php -- Blog article create/edit
  • Adv_category_admin.php -- CMS page create/edit
  • Adv_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 in files/builder/ via advuploader->uploadFilesSameFolder()
  • Asset listing: getAssets() uses Flysystem (storage()->listContents()) to enumerate files/builder/, filtering out index.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

RuleDescription
Feature gatedBUILDER.ENABLED registry flag controls menu visibility and frontRender()
Per-language blocksEach MUI record can reference a different block per language
Cascade delete safeDeleting a block nullifies all references, never orphans
No autosaveEditor requires explicit publish action
WYSIWYG fidelityEditor canvas loads storefront CSS/JS for accurate preview
Storefront authEditor uses isLoggedIn() (any admin), not role-based checks
CMS/Promo view switchingBuilder block presence overrides the default view template
Vendor dual blocksVendors support two builder blocks: description and exclusive page

Migrations

MigrationDescription
Initial schemabuilder_blocks table (id, title, content, css, html)
20241008101631Add js LONGTEXT column to builder_blocks
20241031133710Add builder_block_id to categories_mui
20241031135126Add builder_block_id to blog_mui
20241031154616Add builder_block_id + exclusive_builder_block_id to shop_vendor_mui
20241031161307Add builder_block_id to shop_product_category_mui
20241031164345Add builder_block_id to shop_product_mui

Wiki Guide: Builder Guide -- developer reference for the page builder system