How to Leverage Magento 2's Page Builder for Maximum Marketer Independence

Hey — if you’re a marketer or working alongside one, this is the practical, no-fluff walkthrough you’ve been waiting for. We’re going to show comment use Magento 2’s Page Builder to give marketing teams real control over product visibility and stock status without pinging engineering every time. Expect code snippets, étape-by-étape config, and small automation patterns so marketers can independently tweak stock-related rules from Page Builder blocks and templates.

Why Page Builder for marketer independence?

Page Builder is already a marketer-first tool for building contenu and landing pages. What many shops miss is treating Page Builder not just as a visual composer, but as an operational interface for marketing-controlled product behavior: stock visibility toggles, forced stock attributes, automated syncs and dynamic show/hide rules. With a few sensible extensions and a little integration code, you can:

  • Expose product-level flags like Force Stock Status inside Page Builder blocks.
  • Allow marketers to edit those flags via friendly UI blocks and save them to products.
  • Keep inventaire data consistent by syncing the forced flags with real stock items via automated flux de travails.
  • Use Page Builder’s dynamic rendering to automatically show or hide products basé sur stock flags.

Aperçu of the approche

Here’s the high-level recipe we’ll follow. Vous pouvez pick and choose parts depending on how much autonomy you want to give marketing:

  1. Create a custom attribut produit: force_stock_status (e.g., "default", "force_in_stock", "force_out_of_stock").
  2. Create a Page Builder contenu type or block that renders products (product card / grid) and reads force_stock_status.
  3. Expose an editable control within Page Builder (a toggle or dropdown) which triggers an AJAX save to update the attribut produit.
  4. Implement an automated sync: when force_stock_status changes, update cataloginventory_stock_item to reflect marketing intent (observateur or queued job).
  5. Render logic in the Page Builder template to show/hide items or change labels basé sur the attribute valeur.

Step 1 — Add the attribut produit Force Stock Status

We add a attribut produit that marketing will edit. It’s a simple dropdown (source model) with valeurs: use default, force in stock, force out of stock. Add it in a script de setup or via InstallData / UpgradeData in your module.

Example InstallData (module: Vendor/ForceStock)

// app/code/Vendor/ForceStock/Setup/InstallData.php
namespace Vendor\ForceStock\Setup;

use Magento\Catalog\Model\Product;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

class InstallData implements InstallDataInterface
{
    private $eavSetupFactory;

    public function __construct(EavSetupFactory $eavSetupFactory)
    {
        $this->eavSetupFactory = $eavSetupFactory;
    }

    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);

        $eavSetup->addAttribute(
            Product::ENTITY,
            'force_stock_status',
            [
                'type' => 'int',
                'label' => 'Force Stock Status',
                'input' => 'select',
                'source' => 'Vendor\ForceStock\Model\Attribute\Source\ForceStockStatus',
                'required' => false,
                'default' => 0,
                'global' => ScopedAttributeInterface::SCOPE_WEBSITE,
                'visible' => true,
                'used_in_product_listing' => true,
                'user_defined' => true,
            ]
        );
    }
}

Source model

// app/code/Vendor/ForceStock/Model/Attribute/Source/ForceStockStatus.php
namespace Vendor\ForceStock\Model\Attribute\Source;

use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;

class ForceStockStatus extends AbstractSource
{
    public function getAllOptions()
    {
        if ($this->_options === null) {
            $this->_options = [
                ['label' => __('Use Product Default'), 'value' => 0],
                ['label' => __('Force In Stock'), 'value' => 1],
                ['label' => __('Force Out of Stock'), 'value' => 2],
            ];
        }
        return $this->_options;
    }
}

Après installing the module and running bin/magento setup:mise à jour, the attribute sera available in the product edit UI. But our goal is to let marketers change it inside Page Builder blocks. So next we’ll make Page Builder contenu types that can read and update it.

Step 2 — Create a tiny Page Builder contenu type that displays a product card

Page Builder supports custom contenu types. For simplicity, we’ll add a custom block that takes a product SKU (or product id) and renders a card with status and a UI for marketers to change the Force Stock Status valeur. The block will expose a champ in Page Builder config so a marketer can pick a product while editing a page.

Module skeleton

Your module needs a registration, module.xml and the Page Builder contenu type registration. Magento Page Builder contenu types are declared via di and config fichiers and require a renderer template.

page_builder contenu type exemple: view/adminhtml/ui_composant/page_builder_contenu_types.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/config.xsd">
    <!-- This is a simplified example. Real PB content types can be more involved -->
</config>

Rather than documenting the full Page Builder plugin surface (which is extensive), here’s a pragmatic path: create a frontend block and template, register it as a bloc CMS type available inside Page Builder using the existing "HTML" or "Static Block" contenu types, or create a custom widget that marketers can place. Widgets are a lightweight way to get editable paramètres in Page Builder.

Using a custom widget as the marketer-editable interface

Widgets peut être added into Page Builder. A widget vous donne a simple admin-facing form (product SKU) and a front-end renderer (product card that reads Force Stock Status). Marketers can insert this widget in Page Builder pages and change the product SKU. Nous allons extend it to let them toggle Force Stock Status inline.

widget.xml

<?xml version="1.0"?>
<widgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Widget:etc/widget.xsd">
    <widget id="force_stock_product_card" class="Vendor\ForceStock\Block\Widget\ProductCard" is_enabled="true" placeholder_image="<placeholder>">
        <label>Product Card (Force Stock)</label>
        <description>Card showing product with Force Stock toggle</description>
        <parameters>
            <parameter name="product_sku" xsi:type="text" validate="required-entry"
                       sort_order="10" visible="true">
                <label>Product SKU</label>
            </parameter>
        </parameters>
    </widget>
</widgets>

Block class and template

// app/code/Vendor/ForceStock/Block/Widget/ProductCard.php
namespace Vendor\ForceStock\Block\Widget;

use Magento\Catalog\Model\ProductRepository;
use Magento\Widget\Block\BlockInterface;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;

class ProductCard extends \Magento\Framework\View\Element\Template implements BlockInterface
{
    protected $_template = 'widget/product_card.phtml';
    private $productRepository;

    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        ProductRepository $productRepository,
        array $data = []
    ) {
        parent::__construct($context, $data);
        $this->productRepository = $productRepository;
    }

    public function getProduct()
    {
        $sku = $this->getData('product_sku');
        try {
            return $this->productRepository->get($sku);
        } catch (\Exception $e) {
            return null;
        }
    }
}

product_card.phtml

<?php
/** @var $block Vendor\ForceStock\Block\Widget\ProductCard */
$product = $block->getProduct();
if (!$product) : ?>
    <div class="vf-product-card--missing">Product not found: <?= htmlspecialchars($block->escapeHtml($block->getData('product_sku'))) ?></div>
<?php else: ?>
    <div class="vf-product-card" data-sku="<?= $block->escapeHtml($product->getSku()) ?>">
        <h3><?= $block->escapeHtml($product->getName()) ?></h3>
        <p>Price: <?= $product->getPrice() ?></p>

        <div class="vf-force-stock-control">
            <label>Force Stock Status:</label>
            <select class="vf-force-stock-select" data-sku="<?= $block->escapeHtml($product->getSku()) ?>">
                <?php
                $value = $product->getData('force_stock_status');
                $options = [0 => 'Use Product Default', 1 => 'Force In Stock', 2 => 'Force Out of Stock'];
                foreach ($options as $val => $label) : ?>
                    <option value="<?= $val ?>" <?php if ($value == $val) echo 'selected'; ?>><?= $label ?></option>
                <?php endforeach; ?>
            </select>
            <button class="vf-force-stock-save" data-sku="<?= $block->escapeHtml($product->getSku()) ?>">Save</button>
        </div>

        <div class="vf-product-stock-label"><?= $product->getData('is_in_stock') ? 'In stock' : 'Out of stock' ?></div>
    </div>
<?php endif; ?>

So far: the widget renders in Page Builder pages. Marketers can pick a product SKU when adding the widget. Next we wire the save button to an AJAX contrôleur so marketers can change force_stock_status from the page edit interface or even from the vitrine while pavising an admin-edited page.

Step 3 — Add an AJAX endpoint to update the Force Stock Status

Create an admin contrôleur (or a REST endpoint secured with admin token) that updates the attribut produit. Parce que this endpoint will modify products, keep it restricted to admin utilisateurs. If marketers do not have admin accounts, you can provide a role with limited rights strictly to the widget editing area.

Controller: SaveForceStatus

// app/code/Vendor/ForceStock/Controller/Adminhtml/Save/ForceStatus.php
namespace Vendor\ForceStock\Controller\Adminhtml\Save;

use Magento\Backend\App\Action;
use Magento\Catalog\Model\ProductRepository;
use Magento\Framework\Controller\Result\JsonFactory;

class ForceStatus extends Action
{
    protected $productRepository;
    protected $resultJsonFactory;

    public function __construct(
        Action\Context $context,
        ProductRepository $productRepository,
        JsonFactory $resultJsonFactory
    ) {
        parent::__construct($context);
        $this->productRepository = $productRepository;
        $this->resultJsonFactory = $resultJsonFactory;
    }

    public function execute()
    {
        $result = $this->resultJsonFactory->create();
        $sku = $this->getRequest()->getParam('sku');
        $value = intval($this->getRequest()->getParam('value'));
        try {
            $product = $this->productRepository->get($sku);
            $product->setData('force_stock_status', $value);
            $this->productRepository->save($product);
            return $result->setData(['success' => true]);
        } catch (\Exception $e) {
            return $result->setData(['success' => false, 'message' => $e->getMessage()]);
        }
    }
}

Client-side JS to call the endpoint

// Add this to a small JS file loaded by the widget template
require(['jquery'], function($) {
    $(document).on('click', '.vf-force-stock-save', function () {
        var sku = $(this).data('sku');
        var select = $(this).closest('.vf-force-stock-control').find('.vf-force-stock-select');
        var value = select.val();
        var url = '/admin/force_stock/save/forcestatus'; // map to your route
        $.ajax({
            url: url,
            method: 'POST',
            data: { sku: sku, value: value },
            success: function (resp) {
                if (resp.success) {
                    alert('Saved');
                } else {
                    alert('Error: ' + resp.message);
                }
            },
            error: function () { alert('Request failed'); }
        });
    });
});

Note: Si vous require the widget to be editable from the vitrine by marketing utilisateurs who aren’t logged in to admin, use a secure REST endpoint and restrict access via tokens and roles. The above admin contrôleur is simplest when Page Builder is edited inside the admin interface.

Step 4 — Sync the Force Stock Status with real inventaire (automation)

Setting a attribut produit alone doesn’t change the cataloginventaire stock. To make the behavior actionable (so the vitrine actually hides or shows the product to clients or affects buyability), sync attribute changes to the stock item. Il y a two patterns:

  • Immediate sync on attribute save via observateur.
  • Queued sync via fichier de messages or tâche cron (safer for mass changes).

Observer exemple — quick and direct

// etc/events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="catalog_product_save_after">
        <observer name="force_stock_sync" instance="Vendor\ForceStock\Observer\SyncStockItem" />
    </event>
</config>
// Observer: SyncStockItem.php
namespace Vendor\ForceStock\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\CatalogInventory\Api\StockRegistryInterface;

class SyncStockItem implements ObserverInterface
{
    private $stockRegistry;

    public function __construct(StockRegistryInterface $stockRegistry)
    {
        $this->stockRegistry = $stockRegistry;
    }

    public function execute(Observer $observer)
    {
        $product = $observer->getEvent()->getProduct();
        $force = $product->getData('force_stock_status');
        try {
            $stockItem = $this->stockRegistry->getStockItemBySku($product->getSku());
            if ($force === null) return;
            if ($force == 1) {
                // Force in stock
                $stockItem->setIsInStock(true);
                // Optionally set qty to some safe number
                $stockItem->setQty(max($stockItem->getQty(), 1));
            } elseif ($force == 2) {
                // Force out of stock
                $stockItem->setIsInStock(false);
            }
            $this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem);
        } catch (\Exception $e) {
            // log the exception
        }
    }
}

This immediate observateur works well for single updates (e.g., marketers toggling a widget for a product). If marketing performs bulk changes or you worry about performance, implement a queue: when attribute changes, push the SKU and desired action to a fichier de messages and process it via consommateur or cron.

Step 5 — Use dynamic Page Builder rendering to show/hide products

Now that Force Stock Status influences the real stock item (or at least is stored reliably), you can use Page Builder templates or widgets to automatically hide or show products. Il y a two main approchees:

  1. Server-side: the renderer checks the attribute/stock item and outputs nothing if the product devrait être hidden.
  2. Client-side: load all cards and hide them via JS basé sur attribute valeur / API call. Use client-side only when you need instant pavis toggles without page reload.

Server-side conditional render (recommended)

// In product_card.phtml or your Page Builder template
$force = $product->getData('force_stock_status');
$isInStock = (bool) $product->getData('is_in_stock');
$shouldHide = false;
if ($force == 1) {
    // Force show
    $shouldHide = false;
} elseif ($force == 2) {
    // Force hide / mark out of stock
    $shouldHide = true;
} else {
    // Use product's real stock
    $shouldHide = !$isInStock;
}
if ($shouldHide) {
    // For a product grid, returning empty means the product is not shown at all.
    return;
}
// otherwise render the card normally

With that server-side check, any Page Builder landing page or product gallery will automatically obey the Force Stock Status setting. Marketers can toggle Force Stock Status via the widget and then refresh the page to verify the effect immediately.

Making flux de travails: automation and safety

Marketer independence is great, but you still want guardrails and clear flux de travails. Voici recommended bonnes pratiques:

  • Create an admin role for marketing with limited access (only to Product Attributes and Page Builder operations). Don’t make them full admins.
  • Log every change: write a tiny audit table or use the existing admin activity logs. This helps revert mistakes.
  • Use a staging flux de travail: let marketers make changes on a staging site or in a draft Page Builder page. Only push to production after avis.
  • Use queued jobs for bulk updates to avoid spikes on inventaire tables and to give you a retry mechanism.
  • Expose an easy revert: Page Builder contenu blocks can store previous setting as JSON or audit record to restore default quickly.

Example: queued sync (pattern sketch)

Instead of syncing in an observateur directly, push a message to Magento Message Queue with the SKU and desired status. A consommateur processes messages and updates StockRegistry. If a consommateur fails, messages stay in queue and peut être retried.

// etc/queue_consumer.xml - sketch
<?xml version="1.0"?>
<queue_consumer name="force_stock_sync_consumer" queue="force_stock_sync.queue" handler="Vendor\ForceStock\Model\Consumer\ForceStockConsumer" />

Consumers are a more robust pattern for production sites where marketers may modify many items at once (holiday campaigns, flash sales, etc.).

Practical exemples: real scenarios

Scenario 1: Flash-sale marketer wants to show a product as "in stock" despite 0 qty

  1. Marketer inserts Product Card widget into a Page Builder landing page and selects product SKU.
  2. Marketer picks "Force In Stock" from the dropdown and clicks Save.
  3. The admin contrôleur saves force_stock_status = 1 for that product.
  4. Observer or consommateur sets is_in_stock = true and sets qty >=1 via StockRegistry.
  5. Landing page now shows the product as buyable; CTA goes live without dev involvement.

Scenario 2: Marketing wants to temporarily hide an item from a promotional block without altering catalog categories

  1. Marketer uses Page Builder to edit the promotional block (product grid) where the product appears.
  2. They open the Product Card widget for the SKU and select "Force Out of Stock".
  3. Server-side rendering will exclude the product from the grid because the template checks the attribute.
  4. When the campaign ends, toggling back to "Use Product Default" will reinstate product visibility.

Scenario 3: Complex rule — show specific products only if they are forced in stock or have more than X qty

In your Page Builder rendering logic you can combine conditions:

$force = $product->getData('force_stock_status');
$qty = (float) $product->getStockItem()->getQty();
$minShowQty = 5; // marketing rule
if ($force == 1 || ($force == 0 && $qty >= $minShowQty)) {
    // show product
} else {
    // hide product
}

Tips for making Page Builder UI friendly for marketers

  • Use descriptive labels in the widget: "Force stock status (Use default will keep catalog stock behavior)".
  • Add quick help text right in the widget settings so marketers understand the consequence (e.g., "Force In Stock will make the product purchasable regardless of qty").
  • When possible, show a pavis badge on the product card in Page Builder: "Pavis: Forced In Stock" so marketers know the live effect before publishing.
  • Provide an undo link near the widget to revert to product default (set valeur to 0).

Security and permissions

Parce que this approche allows non-développeurs to change product-level behavior, handle permissions carefully:

  • Limit access to the Page Builder edit interface to trusted marketing utilisateurs.
  • Use an admin ACL for the AJAX contrôleur so only utilisateurs with the right role can call it.
  • Log all saves and changes to an audit log with utilisateur id, timestamp, old valeur and new valeur.

Performance considerations

Changing product stock often triggers cache invalidation and indexeurs. Keep these points in mind:

  • Batch updates via queue to reduce index churn.
  • Reindex only the necessary indexeurs after a batch job.
  • Use Full Page Cache and Varnish invalidation rules appropriately. If showing/hiding products on page de catégories, ensure cache is purged for affected pages.
  • Set widget rendering to be as lightweight as possible: avoid loading heavy collections inside loops; prefer product repository calls by SKU when rendering single product cards.

Search Engine Optimization (SEO) and UX

From an SEO perspective, hiding products from promotional landing pages is usually fine. But if you remove product from page de catégories, think about recherche index consequences:

  • Prefer server-side rendering that returns 200 responses. Don’t return 404 for hidden items unless they’re truly removed from the catalog.
  • Use rel=canonical and other SEO bonnes pratiques if you present alternate contenu on campaign pages.
  • When forcing items in stock (for promos), ensure the buying process respects fulfillment promises; don’t oversell if you can't fulfill. Consider using backcommande strategies or clear "limited supply" messaging.

How this fits into Magefine hosting & extensions world

At Magefine, we focus on practical Magento 2 extensions and hosting that make teams independent and stores resilient. The patterns above are built on Magento core APIs and lightweight modules so you can keep déploiement simple and compatible with managed hosting. Si vous use any tiers inventaire or PIM system, the same pattern works: push Force Stock Status to that system via API or import/export automation.

Résumé checklist for implémentation

  1. Add attribut produit force_stock_status with valeurs (default, force in, force out).
  2. Create a Page Builder friendly widget or contenu type to render product cards and expose the attribute in-line.
  3. Create secure contrôleur endpoint or API REST to allow safe updates by marketing utilisateurs.
  4. Hook an observateur or fichier de messages consommateur to sync attribute changes to StockRegistry.
  5. Use server-side conditional rendering in templates to show/hide products basé sur attribute and stock.
  6. Set up audit logging, permission roles, and staging flux de travail to protect production catalog integrity.

Appendix: quick reference code snippets

Add attribute CLI (quick php snippet)

// Quick script to add attribute if you prefer CLI vs InstallData
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$eavSetupFactory = $objectManager->create(\Magento\Eav\Setup\EavSetupFactory::class);
$eavSetup = $eavSetupFactory->create(['setup' => $objectManager->get(\Magento\Framework\Setup\ModuleDataSetupInterface::class)]);
$eavSetup->addAttribute(
    \Magento\Catalog\Model\Product::ENTITY,
    'force_stock_status',
    [
        'type' => 'int',
        'label' => 'Force Stock Status',
        'input' => 'select',
        'source' => 'Vendor\\ForceStock\\Model\\Attribute\\Source\\ForceStockStatus',
        'default' => 0
    ]
);

Simple SQL for audit table

CREATE TABLE vendor_force_stock_audit (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(64),
  old_value TINYINT,
  new_value TINYINT,
  changed_by INT,
  changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Final thoughts — give marketers power, safely

Page Builder becomes far more than a page composer when you let it surface operational controls. With the Force Stock Status attribute, a marketer can choose to promote or pause products, control visibility and buying behavior, and do it within the Page Builder editing flow they already know. Pair this with a secure save endpoint, a sync mechanism that updates real stock items, and server-side rendering that respects the attribute — and you’ll remove a lot of friction between marketing ideas and live pages.

Si vous want, I can:

  • Write a complete module skeleton (fichiers + di configs) you can drop into a dev instance.
  • Sketch a fichier de messages consommateur/producer implémentation for bulk updates.
  • Help craft an admin role and ACL XML for safe marketing permissions.

Tell me which of the three you want next and I’ll generate the exact fichiers you can install in a dev environment.