Page builders

Custom Elementor Widgets, Gutenberg Blocks and Divi Modules

Approved, on-brand widgets and blocks that pull live data and that editors cannot break. About 40 Elementor widgets, 18 Gutenberg blocks, 12 ACF blocks and 6 Divi modules across 9 projects.

Client
Multi-vendor marketplace, membership site, real-estate and corporate clients
My role
WordPress developer: custom widgets, blocks and builder templates
  • Elementor
  • Elementor Pro Theme Builder
  • Gutenberg
  • block.json
  • ACF
  • Divi
  • WPBakery
  • Dokan
  • LearnDash
  • WooCommerce
  • PHP
widgets and blocks shipped across 9 projects
40

widgets and blocks shipped across 9 projects

build time for a standard landing page
2 days → ~4 hrs

build time for a standard landing page

pages built with the custom block library
≈ 6,000

pages built with the custom block library

PageSpeed mobile score on marketplace landing pages
44 → 86

PageSpeed mobile score on marketplace landing pages

"layout broke after update" tickets in 12 months
0

"layout broke after update" tickets in 12 months

The challenge

A multi-vendor marketplace and a membership site both had content teams that could not touch layouts without a developer. Every new vendor landing page or course promo was a ticket.

The brief: give non-technical editors a set of approved, on-brand blocks and widgets that pull live data (vendor ratings, course progress, pricing tables) and cannot be broken by the editor.

What I built

  1. 01

    About 40 custom Elementor widgets

    Pricing table with toggle billing, vendor card with live Dokan rating, course progress ring (LearnDash), testimonial slider with schema, mega-menu panel, post grid with AJAX filter, countdown and comparison table. Each is registered with elementor/widgets/register, with controls and responsive defaults.

  2. 02

    Custom Elementor dynamic tags

    Vendor rating, vendor store URL, course completion %, member expiry date and an ACF repeater field. They are registered through elementor/dynamic_tags/register under a custom group.

  3. 03

    About 18 Gutenberg dynamic blocks

    Built with block.json and render.php: Vendor Card, Course Syllabus, Member Badge and a Query-Loop-compatible Related Products block. They are server-rendered, so content stays fresh and SEO-safe.

  4. 04

    About 12 ACF blocks for marketing pages

    Hero, feature grid, logo wall and FAQ accordion, registered with acf_register_block_type, with preview mode and locked field groups so editors cannot remove required fields.

  5. 05

    6 Divi modules for a real-estate client

    Custom ET_Builder_Module classes for a client who refused to leave Divi, including a map with filter module and a mortgage calculator.

  6. 06

    3 WPBakery elements for a legacy corporate site

    Added with vc_map. The rest of the site was migrated to Gutenberg over time.

  7. 07

    Builder and WooCommerce templates

    Elementor Pro Theme Builder product and archive templates for Woodmart and Flatsome, and a Gutenberg block-based product archive that replaced a shortcode-heavy page.

Gotchas and how I fixed them

  • Elementor registration hook changed

    elementor/widgets/widgets_registered is deprecated. Use elementor/widgets/register and $widgets_manager->register( new My_Widget() ). For dynamic tags, register the group before the tag, or the tag silently does not appear.

  • Elementor CSS regeneration is the #1 "it works for me" bug

    After changing a widget's render(), call \Elementor\Plugin::$instance->files_manager->clear_cache() in a one-off admin action. Otherwise the old CSS file keeps being served.

  • Gutenberg block deprecations

    Once a block is in use, changing its saved markup without a deprecated entry in JS makes every existing instance show "This block contains unexpected or invalid content." I version blocks from day one and keep a deprecated array.

  • ACF blocks inside Query Loop

    get_field() in an ACF block render template uses the block instance context, but reusable blocks can cache the first instance's values. Use $block['id'] as a key when you hit this.

  • Divi static CSS cache

    After adding or altering a module, Divi serves stale et_cache CSS. Clear et_core_clear_cache() and the et_cache transient on deploy instead of debugging CSS that is not being served.

  • WPBakery vc_map timing

    vc_map must run on vc_before_init. Call it earlier and the element never appears; call it later and you fight the editor's init order.

Code from the build

Elementor widget and dynamic tag registrationphp
add_action( 'elementor/widgets/register', function ( $widgets_manager ) {
    require_once __DIR__ . '/widgets/class-vendor-card.php';
    $widgets_manager->register( new \MyPlugin\Widgets\Vendor_Card() );
} );

// Dynamic tag group must exist before the tag registers.
add_action( 'elementor/dynamic_tags/register', function ( $dynamic_tags ) {
    \Elementor\Plugin::$instance->dynamic_tags->register_group( 'my-plugin', [
        'title' => __( 'My Plugin', 'my-textdomain' ),
    ] );
    require_once __DIR__ . '/tags/class-vendor-rating-tag.php';
    $dynamic_tags->register( new \MyPlugin\Tags\Vendor_Rating() );
} );

The old widgets_registered hook is the most common reason a widget disappears after an Elementor update.

Gutenberg dynamic block: block.jsonjson
{
  "apiVersion": 3,
  "name": "my-plugin/vendor-card",
  "title": "Vendor Card",
  "category": "widgets",
  "icon": "store",
  "supports": { "html": false, "align": true },
  "attributes": {
    "vendorId": { "type": "number" },
    "showRating": { "type": "boolean", "default": true }
  },
  "render": "file:./render.php",
  "editorScript": "file:./index.js",
  "style": "file:./style.css"
}

Pointing render at a PHP file makes the block server-rendered, so it stays correct when vendor data changes.

Gutenberg dynamic block: render.phpphp
<?php
// render.php
$vendor_id = isset( $attributes['vendorId'] ) ? (int) $attributes['vendorId'] : 0;
if ( ! $vendor_id ) {
    return;
}
$store  = dokan_get_store_info( $vendor_id );
$rating = dokan_get_seller_rating( $vendor_id );
?>
<div <?php echo get_block_wrapper_attributes( [ 'class' => 'vendor-card' ] ); ?>>
    <h3><?php echo esc_html( $store['store_name'] ?? '' ); ?></h3>
    <?php if ( ! empty( $attributes['showRating'] ) ) : ?>
        <span class="vendor-card__rating"><?php echo esc_html( $rating['rating'] ); ?>/5</span>
    <?php endif; ?>
</div>

get_block_wrapper_attributes() is what keeps the align and className supports working, and server rendering avoids the invalid-block problem.

ACF block registrationphp
add_action( 'acf/init', function () {
    if ( ! function_exists( 'acf_register_block_type' ) ) {
        return;
    }
    acf_register_block_type( [
        'name'            => 'testimonial',
        'title'           => __( 'Testimonial', 'my-textdomain' ),
        'render_template' => get_stylesheet_directory() . '/blocks/testimonial/render.php',
        'category'        => 'formatting',
        'icon'            => 'format-quote',
        'mode'            => 'auto',
        'supports'        => [ 'align' => false, 'jsx' => true ],
    ] );
} );

acf/init is the safe hook because ACF may not be loaded earlier, and the function_exists guard keeps the site alive if ACF is deactivated.

Results

  • 40 widgets and blocks shipped across 9 projects; standard landing page build time for the content team went from 2 days to about 4 hours.
  • Elementor editor page load went from about 9s to about 3.5s after widget asset splitting and disabling unused Elementor experiments.
  • About 6,000 pages built with the custom block library, with zero "layout broke after update" tickets in 12 months because blocks are server-rendered and versioned.
  • Mobile LCP on the marketplace landing pages went from 4.1s to 2.0s; PageSpeed mobile from 44 to 86.
  • The marketing team shipped about 35% more campaign pages with the same headcount after the block library landed.

Questions people ask

Can you build a custom Elementor widget that shows live marketplace or LMS data?

Yes. I have built about 40 custom Elementor widgets, including a vendor card with a live Dokan rating and a LearnDash course progress ring. I also add dynamic tags such as vendor rating, course completion % and member expiry date.

Should I build custom Gutenberg blocks or Elementor widgets?

It depends on the team's editor. I build both. Gutenberg blocks use block.json and render.php, so they are server-rendered, stay fresh and are SEO-safe. In my builds, no "layout broke after update" tickets came in over 12 months.

Why do my Elementor widget changes not show up after I update the code?

Elementor keeps serving the old generated CSS file. After changing a widget's render(), clear Elementor's cache with files_manager->clear_cache() in a one-off admin action. Also check you use elementor/widgets/register, since the old registration hook is deprecated.

Do you still work with Divi or WPBakery?

Yes. I built 6 custom Divi modules for a real-estate client, including a map with filter module and a mortgage calculator, and 3 WPBakery elements for a legacy corporate site that I later migrated to Gutenberg.