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.
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:
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.
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.
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
- WPBakery Knowledge Base: Nested Shortcodes, kb.wpbakery.com
- Beaver Builder Knowledge Base: Data Storage, docs.wpbeaverbuilder.com
- unserialize() manual page, php.net
- get_shortcode_regex() function reference, developer.wordpress.org