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
- build time for a standard landing page
- 2 days → ~4 hrs
- pages built with the custom block library
- ≈ 6,000
- PageSpeed mobile score on marketplace landing pages
- 44 → 86
- "layout broke after update" tickets in 12 months
- 0
widgets and blocks shipped across 9 projects
build time for a standard landing page
pages built with the custom block library
PageSpeed mobile score on marketplace landing pages
"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
- 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.
- 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.
- 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.
- 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.
- 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.
- 06
3 WPBakery elements for a legacy corporate site
Added with vc_map. The rest of the site was migrated to Gutenberg over time.
- 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
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.
{
"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.
<?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.
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.