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:
@@ -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.
|
||||
Reference in New Issue
Block a user