Design Fundamentals

Elementor Data Inspector: What Is Inside an Elementor Page

An Elementor page lives in one postmeta row of JSON. Where that row sits, why every url in it is absolute, which of the attachment id and the url actually renders, and how to move a site without losing half the images.

Elementor Data Inspector: What Is Inside an Elementor Page

You move a site to a new domain. The media library looks perfect: every thumbnail renders, every file sits where you left it, the uploads folder came across intact. Then you open the homepage and half the images are still being fetched from the old host, the hero background is a broken icon, and two buttons link to a domain that is not yours any more.

Nothing is wrong with the media library. The problem is that the page is not stored where you are looking. The post_content column for an Elementor page holds whatever the classic editor had before you converted it, or the literal string <!-- Created With Elementor --> on a page that was built in the editor from the start. Search that column for your old domain and you will find nothing, because the design is somewhere else.

It is in one postmeta row. Containers, widgets, every setting you ever touched, every image url, every attachment id, every colour and font size, encoded as a single long JSON string. That row is the page. For most people it is also a black box, right up until the day it has to move.

One row per page

A page built with Elementor carries a handful of postmeta rows. On a live install running Elementor 4.2.2 they read like this:

  • _elementor_edit_mode with the value builder. This is the render switch. Delete it and WordPress falls back to the theme template and post_content, and the design vanishes even though every byte of it is still in the database.
  • _elementor_template_type, holding wp-page, wp-post, section, container or a Pro template type.
  • _elementor_version and _elementor_pro_version, the versions that last saved the page, used to decide which upgrade routine has to run over it.
  • _elementor_page_settings, a serialized PHP array of page level settings: layout, page background, hide title.
  • _elementor_data, the element tree. This is the one that matters.

Elementor writes it in Document::save_elements() as wp_slash( wp_json_encode( $editor_data ) ), and update_metadata() strips the slashing straight back off, so what lands in the longtext column is plain JSON that starts with an opening bracket. You can look at your own in one command:

wp db query "SELECT post_id, LEFT(meta_value, 80) AS head
  FROM wp_postmeta
  WHERE meta_key = '_elementor_data'
  LIMIT 5;"

post_id  head
1197     [{"id":"dhero","elType":"container","settings":{"content_width":"full"

Every node in that array has the same four or five keys: an id (seven characters, unique within the page), an elType of container, section, column or widget, a widgetType when it is a widget, a settings object, and an elements array of children. Everything else is settings, and settings are where the urls hide. Paste a row into the inspector below and the Elementor data JSON becomes a tree you can actually read. It runs entirely in your browser tab: the JSON is parsed in the page, nothing is uploaded, and there is no request to any server.

Elementor data inspector

Paste the JSON out of _elementor_data, or drop an exported Elementor template, and read what is actually in it: the element tree, every image with its attachment id, every link, font, colour and icon, every widget type with a count, and a domain rewrite for the day after a migration. Everything is parsed in this browser tab. Nothing is uploaded, nothing is stored, nothing leaves the page.

empty
Or a file

Nothing loaded yet.

Domain rewrite

This covers the page data and nothing else. Elementor keeps more than one copy of a page. Rewriting here changes the JSON you pasted, so the _elementor_data of one post or the content of one exported template. It does not touch the generated CSS under wp-content/uploads/elementor/css/ or the _elementor_css meta that points at it, the page settings in _elementor_page_settings, the site kit that holds the global colours and fonts, the widget cache, or any url another plugin has stored. After a domain change, put this back, then regenerate the Elementor CSS and check the kit as well.

Nothing rewritten yet.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

Absolute urls, and why they are absolute

Every url Elementor stores is absolute. When you pick an image in the editor, the media library hands the control the full url from wp_get_attachment_url() and that exact string is saved into settings.image.url. There is no relative path to resolve and no site_url() lookup at render time, because the value is not a path, it is one string among thousands in a settings object that every widget class reads in its own way.

That is the whole explanation for the symptom. A database copied to a new domain gets a new siteurl and home option, and the media library rebuilds every url it shows you from that option, which is why it looks healthy. The Elementor JSON rebuilds nothing. It still contains the literal text of the day you clicked, and any widget that falls back to the url prints it.

Look closely at that literal text, because there is a detail in it that will cost you an afternoon later:

"image": {
    "url": "https://old.example/wp-content/uploads/2026/07/bg-5.png",
    "id": 3327,
    "source": "library"
}

wp_json_encode() escapes forward slashes by default, so the column holds https://old.example/wp-content/ with a backslash in front of every slash. Your eye skips it. A find and replace does not.

Table of the postmeta rows an Elementor page writes, with what each one holds, its storage format and whether it contains urls, plus the generated stylesheet on disk that no database query reaches.

The id and the url, and which one renders

Elementor image controls store a pair, an attachment id and a url, and keep both up to date. Which of the two reaches the page is decided at render time in Group_Control_Image_Size::get_attachment_image_html(), and the rule is short enough to memorise.

The id is validated first. If it is not empty and wp_attachment_is_image() says no, the id is thrown away on the spot. If an id survives and the chosen size is one of get_intermediate_image_sizes() plus full, Elementor calls wp_get_attachment_image( $image['id'], $size ), and core builds the src, the srcset and the sizes attribute from the attachment metadata. The url in your JSON is never printed at all. Only when there is no usable id, or the size is custom and no file can be produced, does the code fall through to $image['url'] and print it verbatim in a plain img tag with loading="lazy" and no srcset.

That single branch explains the pattern everyone reports after a move: some images are fine and some are not, in the same section, on the same page. The good ones are rendering from an id that still resolves, so they pick up the new domain through the srcset core assembles from the database. The broken ones have an empty id, or an id pointing at an attachment that did not survive the migration, and the old absolute url is the only thing left to print. If you are working through that on a live site rather than in the JSON, the layer by layer version is in WordPress images not showing.

The pair also drifts for reasons that have nothing to do with moving. Copy a section from another site and you get that site’s url next to an id from your library. Upload the same picture twice and one file has two ids, so deleting the obvious duplicate breaks the page that used the other one. Those are the disagreements the Images tab counts: an id with no url, one id across two different files, one file under two ids, and an id from this site sitting next to a url on another host. Sizes are ignored when it compares, so a thumbnail next to its full size original is not reported as a clash. The library side of that argument is finding and deleting duplicate images safely.

Elementor 4 changed the shape for atomic widgets. An atomic image stores a typed value, an image-attachment-id carrying the number, and the url next to it is genuinely null, because the id is the only source of truth there. That combination is normal and the inspector reports it as normal. On a classic 3.x image widget the same thing is worth a second look.

Two panels comparing an image whose attachment id still resolves, rendered by wp_get_attachment_image, against one whose id is empty and falls back to the stored url, with a table of the id and url mismatches worth checking.

What a template export contains

Export a page from the template library and you get a file named like elementor-1197-2026-08-19.json. Its top level is not the element array, it is an object with five keys: content (the same tree, one level down), page_settings, version, title and type. Elementor 4 attaches the global classes the content uses. The inspector takes that shape and the bare array equally, along with a single element object and a tree that some plugin wrapped in a second layer of JSON, which it unwraps.

What the file does not contain is the images. It contains their urls. On import, Import_Images::import() runs wp_safe_remote_get() against each one, and anything other than a 200 response makes it return false and skip that image without comment. An export taken from a site that is now offline, behind basic auth, or behind a bot challenge produces a page with a perfect layout and no pictures. The source site has to be reachable at the moment of import, which is precisely the thing nobody verifies before pulling the plug on the old server.

Importing twice does not duplicate the media, at least. Every sideloaded attachment gets an _elementor_source_image_hash meta, and the next import that references the same source url finds that row and reuses the existing attachment. It is also why the ids in an export rarely match the ids in the destination library, and why a template is not a backup.

Beyond that, an export carries no site settings unless you export the kit itself, no uploaded fonts or icon sets, no generated CSS, and nothing another plugin stored in its own tables.

Why a plain SQL replace misses Elementor

The good news first. _elementor_data is not serialized PHP, it is JSON, and JSON carries no byte length prefix. Swapping a short host for a longer one inside it cannot corrupt the structure the way it wrecks s:19:"https://old.example" in a serialized option. If you have ever watched a site go white after a find and replace, that prefix is the mechanism, and the bytes that break a migration is worth reading before you touch anything.

The bad news is the escaping. Search the column for https://old.example and you get zero rows, because the column says https://old.example. Elementor’s own Replace URL tool, under Elementor > Tools, deals with it by escaping your input before it builds the query. Reduced to its essentials, this is what it runs:

UPDATE wp_postmeta
SET meta_value = REPLACE(meta_value, 'https://old.example', 'https://new.example')
WHERE meta_key = '_elementor_data' AND meta_value LIKE '[%';

Two details in there are easy to miss. The LIKE '[%' limits the update to rows whose value starts with an opening bracket, so a row that a backup plugin left double encoded, starting with a quote instead, is skipped in silence and you are never told. And both inputs pass through FILTER_VALIDATE_URL first, so you have to supply complete urls with a scheme; a bare host is refused, and so are two identical urls.

WP-CLI is the better instrument for the rest of the database, for one specific reason: wp search-replace unserializes, replaces and reserializes, which recomputes every length prefix on the way past, including _elementor_page_settings and the site kit. It knows nothing about JSON escaping, so hand it the host on its own rather than a url with slashes in it:

# dry run first, and read the report
wp search-replace 'old.example' 'new.example' --all-tables --precise --dry-run

# then for real, leaving the guid column alone
wp search-replace 'old.example' 'new.example' --all-tables --precise --skip-columns=guid

A bare host matches inside the escaped JSON because a host name has no slashes in it to escape. It also matches inside notold.example, old.example.net and cdn.old.example, and inside any string that merely mentions the domain, such as a tracking parameter or a piece of body copy. That is the trade you are making. It is also why the rewrite in the inspector anchors a match to a scheme or a protocol relative // by default, leaves those three neighbours alone, and makes you tick a box before it will match the host anywhere in a string.

Moving an Elementor site without losing the images

The order matters more than the tooling.

  1. Copy the files and the database as they are. Avoid the WordPress XML exporter for an Elementor site: it does carry postmeta, but the importer assigns new post ids, and every attachment id inside your JSON refers to the old numbering.
  2. On the new host, take a database dump you can restore before you run anything.
  3. Run wp search-replace with the bare host, dry run first. In the report, the row count for wp_postmeta should be conspicuously large. Those are your Elementor rows, plus one per revision, because every revision keeps its own copy of the tree.
  4. If the scheme changed as well, run a second pass from http://new.example to https://new.example.
  5. Regenerate Files and Data, under Elementor > Tools. This step is not optional, see below.
  6. Spot check two or three heavy pages for stragglers, ids as well as urls.

Step five is the one people skip. Elementor writes a stylesheet per page to wp-content/uploads/elementor/css/post-1197.css, and those files hold absolute urls too. The one for the page I opened while writing this is 168 KB and its background image rules point at three full https:// urls. No database replace will ever touch them, because they are files on disk, not rows. Regenerating rebuilds them from the current JSON and the current home option. Until you do, backgrounds keep pointing at the host you just left, which is why the complaint is so often “the images came across but the backgrounds did not”.

Then there is the library itself. An id that points at nothing renders nothing, and an id that points at the wrong attachment renders the wrong picture, neither of which shows up in a url scan. That is a media library job rather than an Elementor job, the same work as cleaning up a media library without breaking pages: find the orphans, find the duplicates, and check what is actually referenced before you delete anything.

What the inspector deliberately does not touch

It reads the page data, and only the page data. The boundary is worth stating plainly, because everything outside it is exactly what makes a half fixed site look fixed.

  • The generated CSS under uploads/elementor/css/ and the _elementor_css meta that tracks it. Regenerate those, do not edit them.
  • _elementor_page_settings and the site kit. Global colours, fonts and the site background live in the kit post’s settings, not in any page’s tree.
  • Another plugin’s stored urls: a slider’s own table, a form action, a theme option, a cached page in an object store.
  • Custom CSS. Colours are counted only where a value is a hex, an rgb() or an hsl() on its own, or a global reference in the __globals__ form or as var(--e-global-color-primary). A colour buried in a custom CSS block is not scanned for.
  • Dynamic tags. A __dynamic__ entry holds a shortcode such as [elementor-tag id="8f3" name="post-featured-image"], and the image it resolves to does not exist in this data at all. It is listed so you know it is there, and it is not rewritten, because there is nothing in the file to rewrite.
  • The package a widget comes from. That column is a guess from the widget name and says so on the label.

Anything the parser meets that it does not recognise goes into the Not recognised tab rather than into a guess: an unfamiliar elType, a missing id, a settings value that is not an object, an elements value that is not an array, an unknown key on an element, an atomic $$type it has never seen. All of it is carried through the rewrite untouched. Nothing is ever dropped on the way out, and the JSON you copy back comes out in the shape it went in.

An Elementor page is a document, not a rendering. One postmeta row holds the whole thing, every url in it is a literal string written at the moment you clicked, and nothing recomputes those strings when the site moves. Once you accept that, the failure modes stop being mysterious. The media library looks healthy because it derives its urls from an option every time you load it. The page looks broken because it derives nothing.

The practical version fits in three lines. Replace the bare host rather than the full url, because the slashes in that column are escaped and your search string is not. Regenerate the CSS afterwards, because those stylesheets are files and no query reaches them. And check the attachment ids, not only the urls, because an id that resolves silently overrules whatever url sits next to it, which is how a page can render correctly on the new host and still be full of the old one.

Everything else is just reading. The tree is not obscure, it is only long, and it feels like a black box because nothing ever prints it in a shape a person can scan. Paste the row in, expand it, and it turns back into what it always was: a list of decisions you made, each one with an address attached.

Elementor Data Inspector: What Is Inside an Elementor Page

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Security & Privacy

Content Security Policy Generator: The Header Nobody Dares Switch On

A Content Security Policy is the header everyone knows they should have and nobody dares enforce. What it actually stops, why a policy with only default-src is either useless or breaks everything, the two keywords that undo the whole thing, and what fails first on a real WordPress site.

Photo Editing

Color Grading in Five Moves: From Flat Snapshot to Something With Mood

A snapshot and a photograph often hold the same content. The difference is that somebody decided how the second one should feel.

SEO & Structured Data

Making a Share Card That Does Not Look Like an Accident

The tag is rarely the problem. The picture is. Draw a 1200 by 630 card in the page, then work through the crops, the floors and ceilings the platforms enforce, and the design rules that survive being seen on a phone.

AI Images

AI Product Photos That Do Not Look Fake: Light, Shadow, Perspective

A real product photo on a generated background gives itself away on exactly three axes: light direction, shadow quality and perspective. Here is the physics in plain words, a browser tool that walks you through all three checks on your own composite, and the prompt lines that stop the mismatch happening in the first place.

Design Fundamentals

Why Your Drop Shadow Looks Fake, and the Two Minute Fix

The default shadow describes light that does not exist anywhere. Once you see it you cannot unsee it, and correcting it takes about a minute per element.

Speed & Performance

WordPress Lazy Loading Images: What Core Does and When It Hurts

WordPress has added loading="lazy" itself since 5.5, and it skips the first three media elements on purpose, because the top one is usually the largest contentful paint element. Here is the real mechanism: the functions, the threshold of 3, fetchpriority, and the places where the logic never runs.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.