Evaluated the Stonewright WP MCP repo (github.com/cosmincraciun97/ stonewright-wp-mcp) per user request -- decided against adopting it or its MCP server (this fleet already does WP work via shell+WP-CLI, and its Gutenberg layer is thin wrapping over parse_blocks/serialize_blocks we can already call directly). Its Elementor V3 _elementor_data read/write pipeline is genuinely well-engineered, non-obvious, and worth having as a reference: double-encoding trap, wp_slash-before-persist, surgical patches that preserve unrecognized settings keys, and readback+cache-clear+visual verification after write. Distilled into this skill for CLI/wp-eval-based Elementor edits (not the MCP itself).
7.3 KiB
name, description
| name | description |
|---|---|
| wordpress-elementor-safe-cli-editing | Use when editing an Elementor page/section via WP-CLI or a raw PHP snippet (wp eval, wp eval-file) instead of the Elementor editor UI — reading and mutating the `_elementor_data` JSON tree safely. Covers the double-encoding trap, wp_slash, unknown-setting stripping, and verifying a write actually took. |
WordPress Elementor Safe CLI Editing
Elementor stores a page's layout as a JSON array in the _elementor_data
post meta key — a tree of {id, elType, settings, elements, widgetType?}
nodes. Editing it directly via wp eval/wp eval-file/a REST call is much
faster than driving the visual editor, but the format has several sharp
edges that silently corrupt the page if you're not careful. This skill is
the CLI-safe way to do it. For how to actually invoke wp against a
site living in a remote jail, see wordpress-cli-remote-execution first.
Credit: these techniques are distilled from reading the (AGPL-3.0)
Stonewright WP MCP
plugin's ElementorData read/write pipeline — a real, tested Elementor
integrity layer — not reinvented from scratch. This fleet doesn't run
Stonewright itself (no MCP dependency), just the underlying technique,
applied via plain WP-CLI/PHP.
Reading: handle the double-encoding trap
get_post_meta($id, '_elementor_data', true) can return either a real
array (rare) or a JSON string — and that JSON string can itself already be
double-encoded (a JSON string whose decoded value is itself a JSON
string) if some prior tool re-encoded an already-encoded value. Decode,
and if the result is still a string, decode once more:
$raw = get_post_meta( $post_id, '_elementor_data', true );
$tree = is_array( $raw ) ? $raw : json_decode( (string) wp_unslash( $raw ), true );
if ( is_string( $tree ) ) { // was double-encoded
$tree = json_decode( $tree, true );
}
if ( ! is_array( $tree ) ) {
// empty/missing document — treat as []
$tree = [];
}
Read for convenience, but never write the double-encoded form — that's how documents end up double-encoded in the first place (see below).
Editing: surgical patch, not full-tree rewrite
Don't decode the whole tree, hand-edit it broadly, and write the whole
thing back to fix one control. Find the specific element by id (or by
walking elements for a matching elType/widgetType), mutate only the
keys you actually intend to change, and leave everything else — including
settings keys you don't recognize — untouched.
Why this matters: Elementor's settings schema grows over time and varies by widget version/Pro-vs-free. A setting your script doesn't recognize isn't dead weight to strip on the way to "clean" JSON — it's real configuration (padding, a Pro feature's data, a third-party extension's data) that a full rewrite through your own limited understanding of the schema will silently drop.
Never change widgetType unless that's explicitly the intent — e.g.
e-paragraph → text-editor look similar but are different widgets with
different settings shapes; changing it without care produces a widget that
renders wrong or not at all.
When you need a new element's id, don't use a random string —
Elementor IDs are conventionally short (7-char) hex strings. Deriving them
deterministically (e.g. substr(sha1($stable_key_path), 0, 7)) means
re-running the same generation logic produces the same ID, which makes
future diffs/updates to that element addressable instead of having to
re-discover it by content matching.
Writing: the encode/persist sequence, exactly
$json = wp_json_encode( $tree, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
update_post_meta( $post_id, '_elementor_data', wp_slash( $json ) );
update_post_meta( $post_id, '_elementor_edit_mode', 'builder' );
update_post_meta( $post_id, '_elementor_version', defined('ELEMENTOR_VERSION') ? ELEMENTOR_VERSION : '3.0.0' );
Three things people get wrong here, in order of how often they bite:
wp_slash()beforeupdate_post_meta(), always. WordPress's meta APIs expect slashed input and will mangle quotes/backslashes in your JSON on save if you pass it unslashed — this is the classic root cause of a page that "saved fine" but renders broken or throws a JS parse error in the Elementor editor afterward.JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE— without these,wp_json_encodeescapes/and non-ASCII characters, which Elementor's own JS-side parser doesn't expect in the same normalized form it writes itself; it's usually harmless but produces spurious diffs that make real changes hard to review, and can trip the double-encoding check on a subsequent read if another tool round-trips it.- Update all three meta keys together, not just
_elementor_data— a stale_elementor_versionafter a plugin update, or a missing_elementor_edit_mode, can make Elementor treat the document as needing conversion/repair on next edit.
After writing: readback is not verification
A successful update_post_meta() call only means the write didn't error —
it doesn't mean the stored value round-trips to what you intended (a
serialization bug or a WordPress filter mutating meta on save would both
still return success). Always:
- Read the meta back and compare it (structurally, e.g. by hashing the decoded tree) against what you tried to write. A quick hash comparison catches "nothing actually happened" or "something else silently changed it" without a full diff.
- Clear Elementor's CSS cache — otherwise the page keeps rendering
old generated CSS. From a PHP/
wp evalcontext:\Elementor\Plugin::$instance->files_manager->clear_cache();From pure WP-CLI without PHP eval, usewp elementor flush-cssif the site has WP-CLI Elementor commands available, or clearwp-content/uploads/elementor/css/post-<id>.cssdirectly and let it regenerate on next view. - Load the actual page in a real browser and check it — meta
readback alone is not completion. Check desktop, tablet, and mobile
breakpoints (Elementor's responsive settings live in the same tree
under per-breakpoint keys and are easy to verify wrong from source
alone). For a boxed/contained section, check both the outer element
and its
.e-con-inner— a padding or width fix applied to only one of the two is a common half-fix that looks right in the tree and wrong on screen.
Before touching a page at all: back it up
Elementor pages are WordPress posts — a plain wp post revision (or even
just reading and stashing the current _elementor_data value yourself
before writing) gives you a real rollback path if the edit goes wrong.
Don't skip this because the change "looks small" — a bad write to
_elementor_data can make the page fail to load in the Elementor editor
at all, at which point the only fix is restoring the meta value.
Don't run two edits to the same page concurrently
If more than one process/session might edit the same post around the same
time, treat each post's edit as serialized: read → mutate → write → verify
as one uninterrupted sequence, not overlapping with another edit to the
same post. Concurrent writes to the same _elementor_data key are a
last-write-wins race with no merge — whichever write lands second silently
discards the first.