fix(#111): invalidate Elementor 4.2 element cache on data writes

MCP-created pages rendered empty on Elementor 4.2.x: create-page writes empty
[] data, an intermediate render caches an empty _elementor_element_cache, and
the plugin's raw update_post_meta() write path never invalidated it (Elementor
only clears it in Document::save(), which the CLI/proxy fallback bypasses). On a
persistent object cache (WP Engine) the stale empty entry survives every content
write, so the page serves the cached empty render forever.

Add EMCP_Tools_Data::init() — an added/updated_post_meta hook that clears
_elementor_element_cache whenever _elementor_data changes, from ANY write path —
plus an explicit clear in the save fallback. Also fixes delayed visibility of
edits to existing pages. Root-caused + fix live-verified on Elementor 4.2.2.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Mian Shahzad Raza
2026-08-11 22:06:26 +05:00
co-authored by Claude Opus 4.8
parent b5303c1c11
commit a63ed73c93
4 changed files with 84 additions and 0 deletions
+3
View File
@@ -326,6 +326,9 @@ class EMCP_Tools_Bootstrap {
99
);
// Invalidate Elementor's rendered-element cache on any _elementor_data
// write, so MCP-created/edited pages never serve a stale empty render (#111).
EMCP_Tools_Data::init();
// Content search index: install-on-init + incremental re-index on save/delete.
EMCP_Tools_Search_Index::init();
EMCP_Tools_Change_Blobs::init();
+42
View File
@@ -20,6 +20,42 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class EMCP_Tools_Data {
/** Elementor 4.2's rendered-element cache meta (Document::CACHE_META_KEY). */
const ELEMENT_CACHE_META = '_elementor_element_cache';
/**
* Register global hooks. Invalidates Elementor's rendered-element cache
* (`_elementor_element_cache`) whenever a post's `_elementor_data` changes,
* from ANY write path — our MCP tools' raw `update_post_meta()` writes, the
* editor, or an import.
*
* Elementor 4.2's Element Cache stores rendered HTML in post meta. Elementor
* clears it in `Document::save()`, but our CLI/proxy fallback writes data with
* raw `update_post_meta()` and bypasses that. On a persistent object cache
* (e.g. WP Engine) a stale/empty entry then survives every content write, so a
* page created via MCP — written while its data was still the empty `[]`,
* rendered (caching empty), then filled — keeps serving the cached empty
* render forever. This restores Elementor's invalidation universally, and also
* fixes the delayed visibility of edits to existing pages. See issue #111.
*/
public static function init(): void {
add_action( 'added_post_meta', array( __CLASS__, 'flush_element_cache_on_data_write' ), 10, 3 );
add_action( 'updated_post_meta', array( __CLASS__, 'flush_element_cache_on_data_write' ), 10, 3 );
}
/**
* Clear the rendered-element cache when `_elementor_data` is written.
*
* @param int $meta_id Meta row id (unused).
* @param int $post_id Post id.
* @param string $meta_key Meta key.
*/
public static function flush_element_cache_on_data_write( $meta_id, $post_id, $meta_key ): void {
if ( '_elementor_data' === $meta_key ) {
delete_post_meta( (int) $post_id, self::ELEMENT_CACHE_META );
}
}
/**
* Whether Elementor's document manager is available.
*
@@ -316,6 +352,12 @@ class EMCP_Tools_Data {
// Invalidate Elementor CSS cache so it regenerates on next page view.
delete_post_meta( $post_id, '_elementor_css' );
// Invalidate Elementor 4.2's rendered-element cache — the update above
// fires the meta hook that clears it, but do it explicitly too so a
// re-save of identical data (which skips the hook) still refreshes a
// stale/empty cached render. See init() + issue #111.
delete_post_meta( $post_id, self::ELEMENT_CACHE_META );
$upload_dir = wp_get_upload_dir();
$css_path = $upload_dir['basedir'] . '/elementor/css/post-' . $post_id . '.css';
if ( file_exists( $css_path ) ) {
+31
View File
@@ -0,0 +1,31 @@
<?php
/**
* Public unit test for EMCP_Tools_Data::flush_element_cache_on_data_write() —
* the issue #111 fix that invalidates Elementor 4.2's rendered-element cache
* whenever `_elementor_data` is written (so MCP-created pages never serve a
* stale/empty cached render). Behaviour is fully live-verified on a real
* Elementor 4.2.2 site; this pins the key-matching logic.
*
* @package EMCP_Tools
*/
require_once dirname( __DIR__ ) . '/includes/class-elementor-data.php';
class ElementCacheFlushTest extends \PHPUnit\Framework\TestCase {
protected function setUp(): void {
emcp_test_reset();
$GLOBALS['emcp_test']['deleted_meta'] = array();
}
public function test_clears_element_cache_when_elementor_data_written() {
EMCP_Tools_Data::flush_element_cache_on_data_write( 5, 42, '_elementor_data' );
$this->assertContains( array( 42, '_elementor_element_cache' ), $GLOBALS['emcp_test']['deleted_meta'] );
}
public function test_ignores_other_meta_keys() {
EMCP_Tools_Data::flush_element_cache_on_data_write( 5, 42, '_edit_lock' );
EMCP_Tools_Data::flush_element_cache_on_data_write( 5, 42, '_elementor_css' );
$this->assertSame( array(), $GLOBALS['emcp_test']['deleted_meta'] );
}
}
+8
View File
@@ -153,6 +153,14 @@ function get_post( $post_id ) {
return $GLOBALS['emcp_test']['posts'][ (int) $post_id ] ?? null;
}
if ( ! function_exists( 'delete_post_meta' ) ) {
// Records deletions in $GLOBALS['emcp_test']['deleted_meta'] as [post_id, key].
function delete_post_meta( $post_id, $key, $value = '' ) {
$GLOBALS['emcp_test']['deleted_meta'][] = array( (int) $post_id, (string) $key );
return true;
}
}
function get_permalink( $post = null ): string {
return 'http://example.test/?p=' . ( is_object( $post ) ? (int) $post->ID : (int) $post );
}