Elementor¶
Bulk Elementor _elementor_data updates: use direct SQL UPDATE, not update_post_meta¶
From legacy section: WordPress / MCP
Why:
update_post_meta($pid, '_elementor_data', wp_slash($json))silently returnsfalseand writes nothing when WP's serialize-then-unslash comparison decides the value matches what's already stored. Bit me on the May 13–14 Ianniello Anderson rollout — the strip-recaptcha-v3 script reported "removed 1" repeatedly but never persisted because update_post_meta no-op'd. Discovered via in-process diagnostic:Old length == New length, then SQL UPDATE worked instantly. How to apply: When modifying_elementor_dataprogrammatically across many posts (bulk template rollouts, field strips, hero swaps), bypassupdate_post_metaand write directly:$wpdb->update($wpdb->postmeta, ['meta_value' => $raw], ['meta_id' => $meta_id])after fetching the meta_id with$wpdb->get_var(). Always follow withwp_cache_delete($post_id, 'post_meta')so subsequent reads in the same request see the new value. Date: 2026-05-14
Elementor flex-wrap is unreliable at viewport breakpoints — use explicit rows + nowrap¶
From legacy section: WordPress / MCP
Why: Built a single flex container with
flex_wrap: 'wrap'for /personal/ hub (9 cards at 30% width). On a 970px boxed container with 18px gap, math says 3×30% + 36px = ~94% — should fit 3-per-row. In practice the third card kept dropping to a new row at certain widths because Elementor's gap interacts with percentage widths differently than vanilla CSS flexbox. Master template's mt0027 service grid solves it with TWO inner containers, each a 3-card row withflex_wrap: 'nowrap'. Switched the hub generator to that pattern and the layout became deterministic. How to apply: When building grid layouts in Elementor that need a guaranteed column count, split into explicit row-containers each withflex_wrap: 'nowrap'. Reservewrapfor grids where any wrapping behavior is acceptable. Master-template-aligned widths: 47% for 2-col, 31% for 3-col, 23% for 4-col, paired with 16–18px gap. Date: 2026-05-14
WebFetch Returns Minified Code on Elementor Sites¶
From legacy section: WordPress / MCP
Pattern: WebFetch on a WordPress/Elementor URL returned minified JS/CSS bundles instead of readable content. Rule: Never use WebFetch to read WordPress/Elementor page content. Read source files from disk and rebuild. WebFetch works for competitor pages and non-WP sites only. Date: 2026-03-05
Gutenberg Blocks Don't Render on Elementor Pages¶
From legacy section: Elementor
Pattern: Added a Custom HTML block via Gutenberg — it saved but never rendered on the frontend. Rule: On Elementor pages, ALWAYS use Elementor's own HTML widget to inject code. Use the Elementor JS API or MCP tools — never Gutenberg blocks. Date: 2026-03-05
Elementor Renders from _elementor_data Meta, NOT post_content¶
From legacy section: Elementor
Pattern: On POLY's contact page, ran
wp post update 26 --post_content="..."with the new Jerry-focused copy. DB updated successfully. Page kept rendering the OLD "Hours / Address / Contact Info" sections. Cleared WP Rocket cache, hard-purged at the file level, cache-busted with query strings — old content still served. Cost ~30 minutes before realizing the page is Elementor-built. Rule: For any Elementor-built page (check_elementor_edit_modemeta =builder),post_contentis essentially ignored at render time. Edits must walk the JSON tree in_elementor_datapost meta and surgically update the target widgets. Pattern: dump_elementor_datato backup, decode JSON, walk recursively, modify matching widgets bywidgetType+ content match, re-encode withwp_slash(wp_json_encode()), save. Reference:clients/_active/poly/website-update-2026-04/scripts/03_update_contact_elementor.php. Date: 2026-04-28
Custom Code Requires Valid Elementor Pro License¶
From legacy section: Elementor
Pattern: Elementor Pro's "Custom Code" feature is disabled when the license is mismatched/expired. Rule: When Elementor Pro license is inactive, use the HTML widget approach instead of Custom Code for injection. Date: 2026-03-05
Block Themes Don't Support Elementor Theme Builder¶
From legacy section: Hostinger / Server
Pattern: TwentyTwentyFive (a block/FSE theme) was active. Created Elementor header/footer templates with display conditions, but they never rendered — the block theme bypasses Elementor's template system entirely. Rule: Always use Hello Elementor (or another classic theme) when building with Elementor Pro Theme Builder. Block themes (TwentyTwentyFive, TwentyTwentyFour) are incompatible.
wp theme install hello-elementor --activateshould be step 1 on any new Elementor build. Date: 2026-03-24
Elementor Container CSS Specificity — Use Inline Styles for Overrides¶
From legacy section: Hostinger / Server
Pattern: Tried to override Elementor container layout via external CSS file (
display: grid !important). Elementor's.e-con.e-flexrules have high specificity and use CSS custom properties (--width,--flex-basis). Even!importantin an external stylesheet couldn't override. Rule: For Elementor layout overrides that external CSS can't beat, inject viawp_headat priority 999 as inline<style>. This loads after all Elementor CSS and bypasses WP Rocket's CSS combining. Target elements with.elementor-element-{id}classes. Date: 2026-03-24
WebFetch Truncates CSS-Heavy Elementor Pages Before Reaching the Head¶
From legacy section: Schema Audit
Pattern: Ran WebFetch on putnamplace.com to check meta tags and JSON-LD schema. It reported "no meta description found" and "no schema blocks" — but Chrome MCP confirmed both were present. WebFetch was truncating the HTML inside the first
<style>block (Elementor inline CSS) before reaching the head meta tags or script blocks. Rule: For schema audits and meta tag verification on Elementor/WordPress sites with heavy inline CSS, never trust a "missing" verdict from WebFetch. Use one of: (1) Chrome MCP withdocument.querySelectorAll('script[type="application/ld+json"]'), (2)curl -s URL | grep -oE '<script[^>]+application/ld\\+json[^>]*>', or (3) WP-CLI via SSH to read post meta directly. WebFetch can confirm presence but CANNOT reliably confirm absence on CSS-heavy pages. Date: 2026-04-16
Elementor Pro Theme Builder Conditions — Two Storage Locations¶
From legacy section: Schema Audit
Pattern: Added new pages to template 784's
_elementor_conditionspost meta. Pages still didn't get the template. Elementor Pro reads from theelementor_pro_theme_builder_conditionswp_options entry at runtime — the post meta alone is not enough. Rule: When adding pages to Elementor Pro theme builder templates (single, footer, header, archive), update BOTH: (1)wp post meta update [template_id] _elementor_conditionsand (2)wp option update elementor_pro_theme_builder_conditionswith the full JSON. The option table is what Elementor actually reads. Always verify withwp option get elementor_pro_theme_builder_conditions --format=json. Date: 2026-04-14
New Elementor Pages Need Full Setup — Not Just Content¶
Moved to sops/elementor-page-setup.md on 2026-05-20 — see SOP for the full 8-step procedure.
Elementor Batch Save via WP-CLI — No Manual Editor Saves Needed¶
Moved to sops/elementor-page-setup.md on 2026-05-20 — additional context for the page setup SOP (batch save step after setting all meta). Key gotcha: after batch save, verify _wp_page_template is still default — the save pipeline can change it to elementor_header_footer.
WP Rocket "Delay JavaScript Execution" Is the #1 LCP Fix for Jet/Elementor Sites¶
From legacy section: WP Rocket / Cache
Pattern: putnamplace.com was loading at 5.6s LCP on mobile with 75 JS files and 59 CSS files (Elementor + JetElements + JetTricks + JetBlocks + Smart Slider + Events Calendar). WP Rocket was active but Delay JS Execution was OFF. Enabling it dropped LCP to 1.3s and Performance score from 79 → 100. Two additional toggles helped: Lazy Load CSS Background Images + Auto Preload Fonts. Rule: On any heavy plugin-stack site (Elementor Pro + multiple Jet plugins + any slider), verify WP Rocket settings at
/wp-admin/options-general.php?page=wprocket#file_optimization. Required toggles:minify_css,optimize_css_delivery,minify_js,defer_all_js,delay_js,lazyload,lazyload_css_bg_img,auto_preload_fonts,manual_preload,preload_links. The single biggest win isdelay_js— it delays ALL JS until user interaction, which is safe because nothing above-the-fold needs JS on a content-first site. Date: 2026-04-16
WordPress MCP update_elementor_widget Parameter Shape¶
From legacy section: Elementor / WordPress MCP
Pattern: Tried to update text-editor content with
{post_id: "2408", widget_settings: {editor: "..."}}. Got type error (post_id must be number) and even with that fixed, theeditorfield nested inwidget_settingsdidn't apply. Rule: Forupdate_elementor_widget:post_idmust be a NUMBER (not string), and for text-editor/heading widgets use the top-levelwidget_contentfield — NOTwidget_settings.editor.widget_settingsis for non-content properties (typography, colors, etc.). The MCP response confirms with"updated_content": true. Date: 2026-04-16
WordPress MCP update_page Response Overflow on Elementor Pages¶
From legacy section: Elementor / WordPress MCP
Pattern:
update_pagereturns the full page object including_elementor_datain the response. For Elementor pages that's 200KB+, which overflows MCP token limits and gets saved to disk as a tool-results file. Rule: The update still SUCCEEDS even when the response overflows. To verify the change, grep the saved response file for the changed field (e.g.,grep -o '"status":"[^"]*"') or verify server-side via WP-CLI (wp post list --post__in=... --fields=...). Don't re-run the update — it already worked. Date: 2026-04-16
Elementor Clone + Walker = Fastest Multi-Page Content Swap¶
From legacy section: Elementor / WordPress MCP
Pattern: Needed to create 4 new service pages matching the Local SEO page's styling exactly. Widget-by-widget API updates would have been 140+ round trips. Instead: cloned
_elementor_datavia raw DB insert, wrote a PHP walker that updated widgets by ID in one pass. Rule: When you need N pages that match an existing Elementor template's styling: (1) clone Elementor meta via$wpdb->insert, (2) write a recursive walker that takes[widget_id => new_content]maps and updatessettings.title/settings.editor/settings.icon_list/settings.items[].item_titlein place, (3) delete_elementor_cssand_elementor_element_cacheafter to force re-render. Reference:build/elementor-content-swap.php. Also see feedback memoryfeedback_elementor_clone_and_swap.md. Date: 2026-04-16
_elementor_page_settings Must Be Stored as PHP Array, Not JSON String¶
From legacy section: Elementor / WordPress MCP
Pattern: Built fresh location pages and stored
_elementor_page_settingsaswp_slash(wp_json_encode(['hide_title' => 'yes']))— a JSON string. Page rendered HTTP 500 on the frontend with:Fatal error: Uncaught TypeError: Cannot access offset of type string on string in elementor/core/settings/page/manager.php:255. Elementor'sPage\Manager->get_saved_settings()calls$saved['key']directly, expecting a PHP array (which WP auto-serializes/deserializes). My JSON string broke that. Rule: For Elementor settings meta keys (_elementor_page_settings,_elementor_settings, etc.), pass a PHP array directly toupdate_post_meta(). WordPress will serialize via PHP'sserialize()and Elementor'sget_post_meta(..., true)retrieval will auto-deserialize back to array. Don't JSON-encode. Thewp_slash+wp_json_encodepattern is for_elementor_dataand_evolve_schema(large JSON blobs that Elementor explicitlyjson_decodes); it does NOT apply to settings meta. From WP-CLI:wp post meta update <id> _elementor_page_settings '{"hide_title":"yes"}' --format=json— the--format=jsonflag tells WP-CLI to decode and pass as array. Date: 2026-04-25
Elementor HTML widget content is JSON-escaped — search-replace needs both raw and \" forms¶
From legacy section: SEO NEO / Workbook
Pattern: When updating CSS class anchored values in Elementor's
The same logic applies to any HTML stored inside JSON-string post meta (Gravity Forms display_meta, ACF flexible content with HTML fields, etc). Always run BOTH substitutions; the unmatched form returns 0 rows harmlessly. Never assume the database stores HTML in the same form you wrote it. Date: 2026-04-28_elementor_datapost meta, the literal patternclass="pp-trust__num">20<matched 0 rows even though we knew the data was there. The values were stored as JSON-encoded strings (Elementor useswp_json_encode()which escapes"to\"by default — slashes too,/→\/), so the actual database content wasclass=\"pp-trust__num\">20<. The same UPDATE with the escaped pattern matched 25 rows. Rule: Any wp-cli or direct SQL search-replace against Elementor post meta needs to handle BOTH forms:
Elementor v4: stored faq_schema setting can diverge from render-time value (legacy data quirk)¶
From legacy section: SEO NEO / Workbook
Pattern: During the evolvebusiness.com schema audit, the FAQ page (
/frequently-asked-questions/, post 1982) emitted 6 duplicate<meta @type=FAQPage>blocks — one pernested-accordionwidget that hadfaq_schema: yesin its_elementor_datasettings. Tried to disable by writingfaq_schema: no(and''and fully unsetting) directly into_elementor_datavia WP-CLI. Storage updated correctly — verified viawp post meta getAND via direct$widget->get_settings_for_display()call in WP-CLI which returned'no'. But at front-end render time, the widget read'yes'. Patched the widget temporarily with a debugecho "<!-- DEBUG faq_schema=" . var_export(...) . " -->"and confirmed: render-time value is'yes'regardless of stored'no'. No filter override found in any plugin/theme/mu-plugin. No transient cache responsible. WP object cache, WP Rocket page cache, Elementor CSS cache, Elementor files manager cache all cleared.wp_update_post()to fire the full save chain didn't help. The page was originally built with Elementor 3.35.5 and the install is now 4.0.4 — likely a back-compat path overrides the SWITCHER setting for content authored under the older version. Rule: When you need to toggle a Switcher control on an Elementor widget AND the page was built with an older Elementor version, don't trust direct_elementor_datapost_meta writes — the render path may ignore them. Three options in order of preference: (1) Edit the page in the actual Elementor admin UI and toggle each control through the visual editor — UI saves go through the full Elementor save chain and persist correctly. (2) Programmatically open the document via\Elementor\Plugin::$instance->documents->get($post_id)and use$doc->save(['elements' => $data])(untested for this specific case but matches how the editor saves). (3) Accept the divergence and document — for cosmetic-only issues like duplicate-but-valid schema, the cost of fighting the render path may not be worth the polish. Always verify with both: (a)wp post meta get(storage state), (b) curl + grep the rendered output (actual emitted state). They can disagree. Spend a hard time-cap on direct-meta approaches before pivoting — 30 min max. Date: 2026-04-30
JetEngine __dynamic__ ACF overrides win over Elementor static values — never bulk-strip without an Elementor-placeholder deny-list¶
From legacy section: SEO NEO / Workbook
Elementor heading/text/video widgets often carry both a static
settings.title/youtube_urlAND asettings.__dynamic__.<field>override pointing at an ACF/JetEngine field via an[elementor-tag]shortcode. At render time, if the ACF field is populated, the dynamic value wins. If empty, Elementor falls back to the static value. Updating the static field while__dynamic__is present accomplishes nothing visually. Removing__dynamic__flips the widget to static — but is dangerous if the static is an Elementor placeholder like "Add Your Heading Text Here" / "Click here" / "Lorem ipsum…", which are non-empty strings that pass naïveis_meaningful_static()checks but render as garbage. Why: Diagnosed on joetemplin.com 2026-05-11. Module 1's lesson cards showed correct YouTube videos but lesson title "The $250 Introduction" — even though_elementor_datahad the static title "The TEAM Framework: 4 Steps to Run Your Introduction Machine". Root cause: heading widget's__dynamic__.titlepointed atlesson_title_1ACF field, which still had old Julius template placeholder "The $250 Introduction". Also: when I sweep-removed__dynamic__across 240 pages with a "static value is non-empty → preserve static" heuristic, the header/footer template's heading widgets (which had ACF-driven titles + Elementor default placeholder text as static) flipped to showing "Add Your Heading Text Here" sitewide. How to apply: 1. Two parallel sources of truth pattern — when a JetEngine page shows wrong content but you've verified_elementor_data, dumpwp_postmetafor the same post ID. Fields likelesson_title_N,lesson_desciption_N(typos preserved),video_N,highlight_title_Ndrive the visible widgets via__dynamic__tags. 2. Two fix paths: (a) update the ACF post_meta fields (cleanest — preserves design system, change propagates everywhere ACF is referenced); (b) strip__dynamic__.<field>AND set static to the correct value (only when ACF approach won't work, e.g., the field is unset or shared across pages where you can't risk side effects). 3. Bulk__dynamic__removal needs an Elementor-placeholder deny-list — at minimum: "Add Your Heading Text Here", "Click here", "Lorem ipsum dolor sit amet", "Type your text here", common default button text. A static value matching any of those should be treated as "not meaningful" — leave__dynamic__intact. 4. Hero/footer/header templates are highest risk — they're inherited across many pages and often carry ACF-driven content with placeholder static fallbacks. Always exclude template-type posts (Header Template,Elementor Footer,Hero Banneretc.) from bulk sweeps unless explicitly auditing them. 5. Triggers: any bulk-modify operation on_elementor_dataacross multiple posts; any "the page renders X but my data says Y" debugging; any rebuild of pages cloned from an ACF-integrated template. Date: 2026-05-11
Elementor html-widget <style> blocks render but lose the cascade to theme CSS — keep custom CSS in a mu-plugin¶
From legacy section: SEO NEO / Workbook
Pattern: Built IC's homepage "Why Choose" section as an Elementor
htmlwidget with a top-level<style>block defining.ic-why-card h3 { display: flex; gap: 10px; }and<h3><span class="ic-num">1</span>Former Prosecutors</h3>. On render: badges appeared inline with no gap — "1Former Prosecutors". The<style>block survived the save (visible in page source) but the h3 flex rule was losing the cascade fight against Hello Elementor's theme h3 (which isdisplay: blockfrom the theme's stylesheet loaded in<head>). Theme CSS loaded in head with higher specificity wins over inline<style>in body content. Rule: Any custom section CSS for HTML widgets on iclawny.com (and most Hello Elementor sites) must live in a mu-plugin enqueued viawp_headat priority 100, with!importanton rules that conflict with theme defaults. Don't put production CSS in<style>blocks inside Elementor html widgets — brittle. Also: never putdisplay:flexdirectly on a heading element if a wrapper div is an option — wrap badge+heading in<div class="ic-foo-head">and put flex on that, so theme h3 styles can't break the layout. Why: Surfaced 2026-05-13 on IC homepage "Why Choose" section. Inline<style>rendered correctly in source but theme CSS won the cascade for the h3. Fix: moved all section CSS toevolve-a11y.phpmu-plugin v1.2 with!importanteverywhere; rewrote the HTML to use<div class="ic-why-head"><span class="ic-num">1</span><h3>Former Prosecutors</h3></div>so the flex container is a div, not the h3. How to apply: Any new custom HTML-widget section on an Elementor + Hello Elementor site: 1. Write the CSS in the site'sevolve-a11y.php-style mu-plugin (or create one if none exists), not in<style>inside the widget 2. Use!importanton every rule that affects typography or display — theme CSS WILL try to override 3. Neverdisplay:flexdirectly on<h1>/<h2>/<h3>— wrap in a div 4. Verify with Chrome DevTools after deploy: computed style on the element, confirm your selector won the cascade Date: 2026-05-13