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).
144 lines
7.3 KiB
Markdown
144 lines
7.3 KiB
Markdown
---
|
|
name: wordpress-elementor-safe-cli-editing
|
|
description: 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](https://github.com/cosmincraciun97/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:
|
|
|
|
```php
|
|
$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
|
|
|
|
```php
|
|
$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.
|