Extending the Pattern

Extending the Pattern to WPBakery and Beaver Builder

⏱ 18 min

Everything built so far, structure-aware editing, the duplicate-and-approve workflow, and the environment guardrail, is a pattern, not something specific to Elementor. This lesson applies that same pattern to the other two builders named in this course: WPBakery Page Builder and Beaver Builder. Each has its own real, different storage format, so each needs its own small reader and writer, but nothing about the safety wrapper around them changes.

What you'll learn in this lesson
WPBakery's shortcode format
vc_row, vc_column, and vc_column_text in post_content, parsed with WordPress's own shortcode functions.
Beaver Builder's serialized post meta
The _fl_builder_data key, and the real security risk of unserializing it carelessly.
One safe pattern for both
Structure-aware edits, wrapped in the same duplicate-and-approve and environment-gated abilities from Lessons 5 and 6.
Why you must verify the exact shape on your own site
Neither format is guaranteed identical across every installed version.
Prerequisites

Lessons 4 through 6 completed, since this lesson reuses the same ability shapes, duplication logic, and environment gate against two new builders rather than introducing new safety concepts.

Step 1: WPBakery’s shortcode format

WPBakery stores its layout as nested WordPress shortcodes directly in post_content, in the same spirit as classic Divi (Lesson 3) but with its own tag names: [vc_row] for a row, [vc_column] for a column inside it, and content shortcodes like [vc_column_text] for the actual body content inside a column. As with Divi, parse this with WordPress’s own get_shortcode_regex() and shortcode_parse_atts() rather than regex you write by hand, for the same reason: nested, attribute-bearing shortcodes are genuinely easy to mis-match with a naive pattern.

// File: my-plugin.php
function my_plugin_find_wpbakery_shortcode( string $content, string $tag, string $match_attr, string $match_value ): ?array {
	preg_match_all( '/' . get_shortcode_regex( array( $tag ) ) . '/', $content, $matches, PREG_SET_ORDER, 0 );

	foreach ( $matches as $match ) {
		$atts = shortcode_parse_atts( $match[3] );
		if ( isset( $atts[ $match_attr ] ) && $atts[ $match_attr ] === $match_value ) {
			return array( 'full_match' => $match[0], 'atts' => $atts, 'inner_content' => $match[5] ?? '' );
		}
	}

	return null;
}

This matches a shortcode by a specific attribute value, for example a stable el_id="hero-heading" attribute if the site’s WPBakery shortcodes carry one, rather than by position in the string. If your site’s shortcodes don’t already carry a stable identifying attribute, add one (WPBakery supports a custom el_id on most elements) before building any editing ability against them, the same “never edit by position” rule from Lesson 4 applies here just as strongly.

Step 2: Beaver Builder’s serialized post meta

Beaver Builder stores its published layout in a post meta key, _fl_builder_data (with a parallel _fl_builder_draft key for unpublished draft state), as a serialized PHP array rather than JSON or shortcode text. Reading it means unserializing that array, and unserializing PHP data always carries a real, well-known security consideration:

Never unserialize without allowed_classes => false

PHP’s unserialize() can be made to instantiate arbitrary objects if the serialized data has been tampered with, a real, well-documented class of vulnerability known as PHP object injection. Always pass array( 'allowed_classes' => false ) as the second argument, which restricts unserialization to plain values and arrays only. Do this even though this data originates from your own database, defense-in-depth doesn’t assume every other plugin writing to that database is equally careful.

// File: my-plugin.php
function my_plugin_read_beaver_builder_data( int $post_id ): array {
	$raw = get_post_meta( $post_id, '_fl_builder_data', true );
	if ( ! $raw ) {
		return array();
	}

	$data = is_string( $raw )
		? unserialize( $raw, array( 'allowed_classes' => false ) )
		: $raw;

	return is_array( $data ) ? $data : array();
}

Beaver Builder’s own documentation describes this as an array of node objects, each keyed by a unique node identifier, but doesn’t publish a fixed field-by-field schema the way Elementor does. Don’t assume a specific set of keys inside each node from a blog post or an older plugin version, inspect your own site’s actual decoded array first:

# File: terminal
wp eval 'var_export( unserialize( get_post_meta( <post_id>, "_fl_builder_data", true ), array( "allowed_classes" => false ) ) );'

Find the node you want to edit by whatever stable identifier its structure actually uses on your installed version, edit only that node’s relevant field, then re-serialize with plain serialize() and save it back with update_post_meta(), wrapped in wp_slash() for the same reason as Elementor’s JSON in Lesson 4.

Step 3: The safety wrapper doesn’t change

Whichever builder you’re targeting, the ability that edits it should look structurally identical to Lesson 4’s Elementor ability: a permission_callback that requires the target post to carry _ai_edit_of (Lesson 5) and passes the environment check (Lesson 6), and an execute_callback that reads the builder’s real format, finds one component by a stable identifier, edits only that component, and re-saves correctly. Register a page-builder-ai/update-wpbakery-element ability and a page-builder-ai/update-beaver-element ability the same way, reusing my_plugin_is_safe_for_ai_editing() and the _ai_edit_of check from the earlier lessons rather than writing new safety logic for each builder.

What stays the same across all four builders
Duplicate first, always
The create-edit-duplicate ability from Lesson 5 works identically regardless of which builder made the original post.
Edit by stable id, never by position
Whether that id is Elementor's element id, a custom el_id attribute, or a Beaver Builder node key.
The environment gate wraps every builder's abilities
One shared function, checked in every destructive permission_callback.
Approval is the only path back to the original
apply-approved-edit from Lesson 5 doesn't care which builder produced the meta it's copying.

Test it

Confirm you can round-trip Beaver Builder’s data without corruption before writing any editing logic against it:

# File: terminal
wp eval '
$data = unserialize( get_post_meta( <post_id>, "_fl_builder_data", true ), array( "allowed_classes" => false ) );
$reserialized = serialize( $data );
update_post_meta( <post_id>, "_fl_builder_data", wp_slash( $reserialized ) );
'

Reload the page in Beaver Builder’s own editor afterward and confirm it still opens and renders correctly, an unserialize/serialize round trip with no actual edits is a good sanity check before you add real modification logic on top of it.

Assuming WPBakery or Beaver Builder shortcodes carry a stable id by default

Not every WPBakery element has an el_id attribute set out of the box, and Beaver Builder’s node keys can vary in how predictable they are across versions. If the component you need to edit doesn’t already have something stable to match against, that’s a real prerequisite you need to solve first (adding an identifying attribute through the builder’s own UI, for example), not something to work around with a brittle content-text match that breaks the moment the text itself changes.

Recap

WPBakery stores its layout as shortcodes in post_content, parsed safely with get_shortcode_regex() and shortcode_parse_atts(). Beaver Builder stores its layout as a serialized PHP array in _fl_builder_data, which must always be unserialized with allowed_classes => false to avoid PHP object injection. Both get wrapped in the exact same duplicate-and-approve workflow and environment gate built in Lessons 5 and 6, the only builder-specific work is the reader and writer for each format, not the safety pattern around them.

Resources & further reading

← Production Guardrails: Auto-Deactivating on Live URLs Course Recap: Where Page-Builder AI Editing Should and Shouldn't Go →