WordPress & WooCommerce
Page builders
A huge portion of real-world WordPress sites use a page builder instead of vanilla Gutenberg. The Pitchbar plugin renders each supported builder's pages with the builder's own native API before sending HTML to your Pitchbar agent, so the synced content is what the visitor actually sees.
This page explains how detection + rendering works, which builders are supported, and how to override the behaviour with a filter.
Why this matters
Most page builders don't store the rendered page in
post_content. They keep the layout tree in
postmeta and recompose the HTML at render time. If
your sync pipeline naively reads post_content and
calls apply_filters('the_content', …), you'll get:
- Elementor: empty string. The layout lives entirely in
_elementor_datapostmeta. - Bricks: empty. Layout in
_bricks_page_content_2. - Oxygen: a single
[oxygen_html]shortcode stub. The real layout is inct_builder_shortcodespostmeta. - Beaver Builder: a stripped-down "fallback" HTML if the builder never gets a chance to override.
- Divi: the shortcode-encoded layout in
post_content. It only renders correctly when the post loop is properly primed.
The plugin's PageBuilderContent helper handles each
case explicitly. First match wins; a post that isn't owned by any
detected builder falls through to the vanilla
the_content path.
Supported builders
| Builder | Detection postmeta | Renderer |
|---|---|---|
| Elementor (Free + Pro) | _elementor_edit_mode = builder |
\Elementor\Plugin::$instance->frontend->get_builder_content_for_display($id, true) |
| Beaver Builder | _fl_builder_enabled = 1 |
FLBuilder::render_content_by_id($id) |
| Oxygen Builder | ct_builder_shortcodes (non-empty) |
do_shortcode($shortcodes) |
| Bricks Builder | _bricks_page_content_2 (non-empty) AND BRICKS_VERSION defined |
\Bricks\Frontend::render_content($payload) |
| Divi | _et_pb_use_builder = on |
Routes through the_content filter (with setup_postdata primed) |
Every renderer is wrapped in try/catch (Throwable).
A throwing builder renderer falls through silently to the next
detection or to the vanilla path, so an upgraded builder version
that changes its internal API can never fatal-out the sync.
Why Divi is different
Divi does store its layout in post_content —
as shortcode-encoded markup like
[et_pb_section][et_pb_row][et_pb_column][et_pb_text]….
The shortcodes resolve correctly through the WordPress filter
chain only when the post loop is primed: $GLOBALS['post']
set and setup_postdata() called.
Plenty of third-party plugins (Yoast, Jetpack, embed handlers)
also gate their the_content callbacks on a valid
$post global. Without postdata priming, every one of
them early-returns and the synced HTML is missing the chrome that
makes the page actually useful.
The fix shipped in plugin v2.0.0:
PostContentExtractor always primes
$GLOBALS['post'] + calls setup_postdata()
around the filter chain, wraps the work in
try/finally, and restores $GLOBALS['post']
+ calls wp_reset_postdata() even if a filter throws.
What the wire looks like
For an Elementor page titled "Pricing", the plugin sends the
fully-rendered HTML to /api/v1/wp/posts/sync exactly
as the browser would receive it — including section wrappers,
widget HTML, and Elementor's own classnames. Pitchbar then
strips tags server-side for chunking, so the classnames don't
end up in the embedding.
Sample sliced payload (truncated for readability):
{
"wp_id": 142,
"post_type": "page",
"permalink": "https://shop.example/pricing",
"title": "Pricing",
"content_html": "<div class=\"elementor elementor-142\"><section class=\"elementor-section …\">…</section>…</div>",
"excerpt": "Three plans, two outcomes…",
"content_hash": "ab12…ef90",
"modified_at": "2026-05-09T14:30:00+00:00",
"language": "en-us",
"taxonomy_terms": []
}
Override the rendered HTML
The pitchbar_post_content_html filter receives the
final HTML after the builder renderer runs (or after the
vanilla filter chain runs, for non-builder posts) — so you can
post-process it without caring which builder owned the page:
add_filter('pitchbar_post_content_html', function ($html, $post, $builder) {
// $builder is one of: 'elementor', 'beaver', 'oxygen', 'bricks',
// 'divi', or null for plain Gutenberg/Classic posts.
if ($builder === 'elementor') {
// Strip Elementor's helper iframes that crawlers don't see.
$html = preg_replace('#<iframe[^>]*data-elementor-[^>]*>.*?</iframe>#is', '', $html);
}
return $html;
}, 10, 3);
Returning an empty string suppresses sync for that post (Pitchbar
will accept the empty content but the agent won't have anything
to retrieve from). Returning null short-circuits the
rest of the filter chain.
When a builder upgrades and breaks
Page builders periodically rename their internal renderers — when that happens, the plugin's reflection-based call falls through to the next path and the post syncs as if it were a vanilla Gutenberg post. The result is degraded (you may get an empty string or a shortcode stub), but the sync itself doesn't fatal out.
If you notice a specific builder version producing empty content,
open an issue against the Pitchbar repo with the builder name +
version. The fix is usually a single-line update to the renderer
call in PageBuilderContent.
Limitations
- Dynamic data widgets. Elementor Pro's "Dynamic Tags" that pull live data (cart counts, user names) render with their default fallback when there's no real visitor in scope.
- Conditional display rules. Visibility rules that depend on the requesting visitor (logged-in only, geolocation, A/B variants) resolve against the sync request context, which is server-side. Sections gated to "logged-in users only" are not synced.
- Lazy-loaded inline blocks. JavaScript-only content (React micro-frontends embedded as Elementor HTML widgets) never executes server-side, so the synced HTML reflects the placeholder, not the hydrated content.
These are inherent to server-side rendering of a builder layout and apply to every CMS plugin that does the same thing (Yoast SEO sitemap generators, schema markup plugins, and so on).