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).
This commit is contained in:
2026-08-13 18:16:39 +02:00
parent 09a96c74dc
commit 8f34371a9f
@@ -0,0 +1,143 @@
---
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.