Files
agent-skills/skills/wordpress-elementor-safe-cli-editing/SKILL.md
T
Malin 8f34371a9f skills: add wordpress-elementor-safe-cli-editing
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).
2026-08-13 18:16:39 +02:00

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-paragraphtext-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:

  1. wp_slash() before update_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.
  2. JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE — without these, wp_json_encode escapes / 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.
  3. Update all three meta keys together, not just _elementor_data — a stale _elementor_version after 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:

  1. 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.
  2. Clear Elementor's CSS cache — otherwise the page keeps rendering old generated CSS. From a PHP/wp eval context: \Elementor\Plugin::$instance->files_manager->clear_cache(); From pure WP-CLI without PHP eval, use wp elementor flush-css if the site has WP-CLI Elementor commands available, or clear wp-content/uploads/elementor/css/post-<id>.css directly and let it regenerate on next view.
  3. 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.