Skip to content

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 returns false and 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_data programmatically across many posts (bulk template rollouts, field strips, hero swaps), bypass update_post_meta and write directly: $wpdb->update($wpdb->postmeta, ['meta_value' => $raw], ['meta_id' => $meta_id]) after fetching the meta_id with $wpdb->get_var(). Always follow with wp_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 with flex_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 with flex_wrap: 'nowrap'. Reserve wrap for 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_mode meta = builder), post_content is essentially ignored at render time. Edits must walk the JSON tree in _elementor_data post meta and surgically update the target widgets. Pattern: dump _elementor_data to backup, decode JSON, walk recursively, modify matching widgets by widgetType + content match, re-encode with wp_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 --activate should 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-flex rules have high specificity and use CSS custom properties (--width, --flex-basis). Even !important in an external stylesheet couldn't override. Rule: For Elementor layout overrides that external CSS can't beat, inject via wp_head at 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 with document.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_conditions post meta. Pages still didn't get the template. Elementor Pro reads from the elementor_pro_theme_builder_conditions wp_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_conditions and (2) wp option update elementor_pro_theme_builder_conditions with the full JSON. The option table is what Elementor actually reads. Always verify with wp 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 is delay_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, the editor field nested in widget_settings didn't apply. Rule: For update_elementor_widget: post_id must be a NUMBER (not string), and for text-editor/heading widgets use the top-level widget_content field — NOT widget_settings.editor. widget_settings is 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_page returns the full page object including _elementor_data in 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_data via 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 updates settings.title / settings.editor / settings.icon_list / settings.items[].item_title in place, (3) delete _elementor_css and _elementor_element_cache after to force re-render. Reference: build/elementor-content-swap.php. Also see feedback memory feedback_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_settings as wp_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's Page\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 to update_post_meta(). WordPress will serialize via PHP's serialize() and Elementor's get_post_meta(..., true) retrieval will auto-deserialize back to array. Don't JSON-encode. The wp_slash + wp_json_encode pattern is for _elementor_data and _evolve_schema (large JSON blobs that Elementor explicitly json_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=json flag 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 _elementor_data post meta, the literal pattern class="pp-trust__num">20< matched 0 rows even though we knew the data was there. The values were stored as JSON-encoded strings (Elementor uses wp_json_encode() which escapes " to \" by default — slashes too, /\/), so the actual database content was class=\"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:

$patterns = [
  'class="foo">20<'    => 'class="foo">17<',     // raw HTML
  'class=\"foo\">20<'  => 'class=\"foo\">17<',   // JSON-escaped form stored in _elementor_data
];
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 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 per nested-accordion widget that had faq_schema: yes in its _elementor_data settings. Tried to disable by writing faq_schema: no (and '' and fully unsetting) directly into _elementor_data via WP-CLI. Storage updated correctly — verified via wp post meta get AND 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 debug echo "<!-- 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_data post_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_url AND a settings.__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ïve is_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_data had the static title "The TEAM Framework: 4 Steps to Run Your Introduction Machine". Root cause: heading widget's __dynamic__.title pointed at lesson_title_1 ACF 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, dump wp_postmeta for the same post ID. Fields like lesson_title_N, lesson_desciption_N (typos preserved), video_N, highlight_title_N drive 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 Banner etc.) from bulk sweeps unless explicitly auditing them. 5. Triggers: any bulk-modify operation on _elementor_data across 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 html widget 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 is display: block from 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 via wp_head at priority 100, with !important on rules that conflict with theme defaults. Don't put production CSS in <style> blocks inside Elementor html widgets — brittle. Also: never put display:flex directly 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 to evolve-a11y.php mu-plugin v1.2 with !important everywhere; 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's evolve-a11y.php-style mu-plugin (or create one if none exists), not in <style> inside the widget 2. Use !important on every rule that affects typography or display — theme CSS WILL try to override 3. Never display:flex directly 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