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 how to 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, step-by-step 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 content 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 inventory data consistent by syncing the forced flags with real stock items via automated workflows.
  • Use Page Builder’s dynamic rendering to automatically show or hide products based on stock flags.

Overview of the approach

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

  1. Create a custom product attribute: force_stock_status (e.g., "default", "force_in_stock", "force_out_of_stock").
  2. Create a Page Builder content 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 product attribute.
  4. Implement an automated sync: when force_stock_status changes, update cataloginventory_stock_item to reflect marketing intent (observer or queued job).
  5. Render logic in the Page Builder template to show/hide items or change labels based on the attribute value.

Step 1 — Add the product attribute Force Stock Status

We add a product attribute that marketing will edit. It’s a simple dropdown (source model) with values: use default, force in stock, force out of stock. Add it in a setup script 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;
    }
}

After installing the module and running bin/magento setup:upgrade, the attribute will be 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 content types that can read and update it.

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

Page Builder supports custom content 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 value. The block will expose a field 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 content type registration. Magento Page Builder content types are declared via di and config files and require a renderer template.

page_builder content type example: view/adminhtml/ui_component/page_builder_content_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 CMS block type available inside Page Builder using the existing "HTML" or "Static Block" content types, or create a custom widget that marketers can place. Widgets are a lightweight way to get editable parameters in Page Builder.

Using a custom widget as the marketer-editable interface

Widgets can be added into Page Builder. A widget gives you 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. We'll 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 controller so marketers can change force_stock_status from the page edit interface or even from the storefront while previewing an admin-edited page.

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

Create an admin controller (or a REST endpoint secured with admin token) that updates the product attribute. Because this endpoint will modify products, keep it restricted to admin users. 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: If you require the widget to be editable from the storefront by marketing users who aren’t logged in to admin, use a secure REST endpoint and restrict access via tokens and roles. The above admin controller is simplest when Page Builder is edited inside the admin interface.

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

Setting a product attribute alone doesn’t change the cataloginventory stock. To make the behavior actionable (so the storefront actually hides or shows the product to customers or affects buyability), sync attribute changes to the stock item. There are two patterns:

  • Immediate sync on attribute save via observer.
  • Queued sync via message queue or cron job (safer for mass changes).

Observer example — 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 observer 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 message queue and process it via consumer 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. There are two main approaches:

  1. Server-side: the renderer checks the attribute/stock item and outputs nothing if the product should be hidden.
  2. Client-side: load all cards and hide them via JS based on attribute value / API call. Use client-side only when you need instant preview 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 workflows: automation and safety

Marketer independence is great, but you still want guardrails and clear workflows. Here are recommended best practices:

  • 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 workflow: let marketers make changes on a staging site or in a draft Page Builder page. Only push to production after review.
  • Use queued jobs for bulk updates to avoid spikes on inventory tables and to give you a retry mechanism.
  • Expose an easy revert: Page Builder content blocks can store previous setting as JSON or audit record to restore default quickly.

Example: queued sync (pattern sketch)

Instead of syncing in an observer directly, push a message to Magento Message Queue with the SKU and desired status. A consumer processes messages and updates StockRegistry. If a consumer fails, messages stay in queue and can be 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 examples: 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 controller saves force_stock_status = 1 for that product.
  4. Observer or consumer 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 preview badge on the product card in Page Builder: "Preview: Forced In Stock" so marketers know the live effect before publishing.
  • Provide an undo link near the widget to revert to product default (set value to 0).

Security and permissions

Because this approach allows non-developers to change product-level behavior, handle permissions carefully:

  • Limit access to the Page Builder edit interface to trusted marketing users.
  • Use an admin ACL for the AJAX controller so only users with the right role can call it.
  • Log all saves and changes to an audit log with user id, timestamp, old value and new value.

Performance considerations

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

  • Batch updates via queue to reduce index churn.
  • Reindex only the necessary indexers after a batch job.
  • Use Full Page Cache and Varnish invalidation rules appropriately. If showing/hiding products on category pages, 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 category pages, think about search 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 best practices if you present alternate content 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 backorder 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 deployment simple and compatible with managed hosting. If you use any third-party inventory or PIM system, the same pattern works: push Force Stock Status to that system via API or import/export automation.

Summary checklist for implementation

  1. Add product attribute force_stock_status with values (default, force in, force out).
  2. Create a Page Builder friendly widget or content type to render product cards and expose the attribute in-line.
  3. Create secure controller endpoint or REST API to allow safe updates by marketing users.
  4. Hook an observer or message queue consumer to sync attribute changes to StockRegistry.
  5. Use server-side conditional rendering in templates to show/hide products based on attribute and stock.
  6. Set up audit logging, permission roles, and staging workflow 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.

If you want, I can:

  • Write a complete module skeleton (files + di configs) you can drop into a dev instance.
  • Sketch a message queue consumer/producer implementation 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 files you can install in a dev environment.