How to Migrate From Wix to Payload CMS
Payload's convertHTMLToLexical does not upload images, it drops every img tag silently. That, plus turning denormalised CSV strings into relationship IDs, is most of the real work in a Wix to Payload migration. Here is the field-level mapping and the order to run it in.
MigrateLab Team
Migration Experts

The two conversions that actually cost you
A Wix to Payload migration is not difficult because Wix is closed. It is difficult because the two things you most want to carry across, rich text with images inside it and repeated values like author and category, are exactly the two things that do not survive a naive import.
Payload's own documentation is explicit about the first one. When converting HTML to Lexical, <img> tags are not automatically uploaded, because Payload cannot know which uploads-enabled collection to put them in. Nothing is flagged, queued or logged. The images are dropped silently. The post imports, the title is correct, the body reads fine in the admin list view, and every image in the article is gone. On a large blog you can reach client review before anyone notices.
The second failure is quieter. A Wix CSV carries the author's name as a plain string on every single row. Payload's relationship field stores a document ID. Handing it "Jane Doe" where it expects an ID gives you a validation error at best, and at worst a document that saves happily with an empty relation.
Everything below is the field-level mapping between the two models, and the order of operations that avoids both failures.
What Wix actually hands you, briefly
Wix has no code export. A CMS collection exports to CSV, capped at 1GB and 50,000 items, and store products export to a separate CSV. There is no blog export feature, and the RSS feed at /blog-feed.xml is capped at 20 truncated excerpts, so the archive has to be recovered by crawling the rendered pages listed in the sitemap.
That is the extent of what you have to work with. The full detail on what exports, what has to be crawled, and how to build the URL inventory lives in the Wix to code migration guide. This page assumes you have already got the CSVs and the crawled bodies in hand, and picks up at the point where you have to decide what shape they take inside Payload.
One dated fact worth carrying into the decision, because it is the clearest evidence of what a closed platform can do with your content: Wix discontinued ADI on 10 November 2024, and unpublished ADI sites that had been inactive for six months or more were deleted.
The two content models, side by side
Payload's hierarchy is Config, then Collections, then Fields. A Collection is the schema; there is no separate content-type layer sitting above it. Globals are singletons for things like site settings.
The field type values are a fixed list: array, blocks, checkbox, code, date, email, group, json, number, point, radio, relationship, richText, select, tabs, text, textarea, upload, plus presentational ones (collapsible, row, ui) and the virtual join.
Note the spelling: it is richText, camelCase. rich-text is only the docs URL slug, and typing it into a config is a common first-hour mistake.
Wix gives you the opposite shape. Every column is a scalar. Rich text arrives as an HTML blob or with its formatting already flattened. Images are absolute Wix CDN URLs, often carrying query-string transform parameters. Repeated values are denormalised onto every row. There are no stable IDs you can rely on across separate exports.
Column-by-column mapping
| What the Wix CSV gives you | Payload field | What you have to do to it | |---|---|---| | Title column | text | Trim. Wix exports frequently carry trailing whitespace. | | URL or slug column | text, indexed and unique | Keep the original Wix path verbatim in its own field too, for the redirect map. | | Short description | textarea | Direct. | | Post body HTML blob | richText (Lexical) | Run the image pre-pass first, then convertHTMLToLexical. See below. | | Date column | date | Cast explicitly. CSV gives you a string, and the format varies by locale setting. | | TRUE / FALSE column | checkbox | Cast explicitly. The string "FALSE" is truthy in JavaScript. | | Numeric column | number | Strip currency symbols and thousands separators before casting. | | Single-choice column | select | Only if the option set is closed. If editors must add options, model it as a relationship instead. | | Comma-separated tags in one cell | relationship, hasMany: true | Split, normalise, dedupe, create the tag documents in pass one. | | Author name string | relationship to an authors collection | Two passes. It cannot go in directly. | | Category name string | relationship to a categories collection | Two passes. | | Featured image (absolute Wix CDN URL) | upload | Download first. payload.create() takes filePath or file, not a remote URL. | | Inline images inside the body HTML | media documents plus rewritten <img> tags | The failure case. See the next section. | | Wix item identifier | text, indexed | Keep whatever identifier the export gives you. It is your reconciliation and resume key. | | Layout divs, inline styles, embedded widgets | no direct equivalent | Decide per construct, before the run: map to a blocks entry or drop it deliberately. |
The upload field is worth understanding structurally: it stores the media document's ID, so it behaves as a relationship, not as a file. That is why the media documents have to exist before anything that points at them.
Images: get the order right or lose them all
This is the single most important sequence in a Wix to Payload migration, and it is not intuitive, because the converter fails without complaining.
Parse the body HTML and extract every image source. Check
srcsetas well assrc. Strip the Wix CDN query-string transforms so two references to the same asset at different sizes resolve to one file.Download each image to disk. This is where the wall-clock time goes, not the writing.
Create the media document with
payload.create({ collection: 'media', filePath })and capture the returned ID.Build a source-URL to document-ID map and persist it to disk.
Rewrite each `<img>` tag in the HTML with
data-lexical-upload-idset to the new document ID anddata-lexical-upload-relation-toset to the uploads collection slug.Only then call `convertHTMLToLexical`.
Convert first and you get a clean-looking Lexical document with no images in it and no error to tell you.
Two further properties of the converter that shape how you run it. convertHTMLToLexical needs JSDOM as a peer dependency, so it runs in Node only and cannot be pushed into an edge function. And JSDOM normalises malformed source HTML on the way through, which with Wix output is usually a help: unclosed <p> tags wrapping block elements are the common case, and they come out sane.
There is no lossy mode. Payload will not park unrecognised HTML in a passthrough node for you to clean up later. Wix layout wrappers, inline style attributes and embedded widgets such as booking forms or gallery components simply have no matching Lexical node. That is a modelling decision you make in advance for each construct, not something the script can infer.
Denormalised strings become relationship IDs, in two passes
A relationship field stores a document ID, so it cannot be populated from the same row that names the thing it points at. That forces two passes, and the sequencing matters.
Pass one, build the reference collections. Read every CSV column that repeats, take the distinct set of values, normalise case and whitespace, and create one document per value. Skipping normalisation is how "jane doe", "Jane Doe" and "Jane Doe" become three author documents and a broken filter in the admin UI. Write the resulting name-to-ID map to disk as you go.
Pass two, create the content documents, resolving each denormalised string to the ID you recorded in pass one.
Lock the schema before pass two starts, because two flags change the stored shape of every relationship value:
| relationTo | hasMany | What is stored | |---|---|---| | string | false | a bare ID | | array | false | { relationTo, value } | | string | true | an array of IDs | | array | true | an array of { relationTo, value } |
Change either flag after the import and every document that uses that field needs rewriting. Deciding halfway through that categories should be polymorphic across two collections is not a config edit, it is a second migration.
Localization carries the same warning and is worse to discover late. It is set at project level with locales, defaultLocale and fallback, and individual fields opt in with localized: true. Turning that on changes a field's storage from a scalar to a locale-keyed object, so it has to be decided before the first import, not after. Query with locale=all to see what is actually stored.
One naming trap while you are here: Payload's migrate command handles database schema migrations. It has nothing to do with importing content. Readers conflate the two constantly, and the docs endorse the Local API for seed and import scripts instead.
The good news: throughput is not your bottleneck
The import path is the Local API, payload.create(), called directly from a script rather than over HTTP. Payload's docs endorse this explicitly for seeding. There is no rate limit. Your ceiling is whatever your database will take.
That matters more than it sounds, because it moves the bottleneck somewhere you control. The slow part of a Wix to Payload migration is downloading a few thousand images off Wix's CDN, not writing them into Payload.
So build the import as two separable stages. Stage one fetches assets: slow, retryable, and cached to disk so a failure never costs you the same download twice. Stage two writes to Payload: fast, and cheap to re-run from scratch after a schema fix. If a bad hasMany decision means starting the write over, that should be a coffee break, not a day.
Wix Stores, if you sell
Products come out as their own CSV. Wix does not support an XML store export.
The straightforward columns map exactly as above: text for name and SKU, number for price and inventory once cast from strings, richText for the description if it holds HTML, upload for product images.
Variants are the awkward part, and they are awkward because a flat CSV has only two ways to express them: extra columns per option, or extra rows per combination. Neither is a data model. In Payload you either model variants as an array field with its own subfields on the product document, or as a separate collection with a relationship back to the parent. Both are defensible. Choosing after the import is not, for the same reason as above.
Payload is a CMS, not a checkout. Payments go to a dedicated service such as Stripe or a headless commerce platform, and orders and customers are a separate data task from the site migration.
Vendor status and what it costs to run
Two facts a technical buyer should have before committing. Payload was acquired by Figma in June 2025. And Payload Cloud has paused new deployments, so there is no first-party hosting product to sign up for.
The software itself is MIT licensed and free. Hosting and a database are a separate bill. Free database tiers are marginal for client work, since Neon's free plan suspends compute at its limits, which means the client's admin panel goes down. Realistically, budget $8 to $30 per month for a client-grade database, and roughly $20 to $50 per month total on Vercel with a managed database.
One trap that catches agencies and freelancers regularly: Vercel's Hobby tier is non-commercial only, and Vercel's own definition of commercial usage covers a paid employee or consultant writing the code and receiving payment to create, update or host the site. A client site on Hobby is a terms violation. Vercel Pro at $20 per user per month is the floor for client work.
For the migration itself, budget $5,000 to $25,000 for a 50 to 100 page site. On this pair the price is driven by two things: how much of the archive has to be crawled rather than exported, and how much of the body HTML is Wix widgets that need a modelling decision rather than a conversion rule.
Agent access is not the reason to move
Worth saying plainly, because it is a claim made carelessly. Wix ships an official MCP server at mcp.wix.com/mcp. An AI agent can reach a Wix site.
The difference after migrating is not access, it is the artifact. Against a Payload repo an agent gets the collection configs, the frontend and the content model in one place, can run the build to check its own work, and a bad change reverts with git. Against a hosted builder API it edits the fields the API exposes, with no build to verify against and no history to roll back. That is a real difference, but it is a difference of leverage, not of possibility.
When Payload is the wrong destination for a Wix site
If your Wix site is a dozen static pages with no CMS collections behind it, Payload gives you an admin UI nobody needs plus a database bill. Flat Markdown files in a repo do the same job for nothing.
Payload earns its cost in one specific situation: non-technical editors have to change structured, repeating content regularly, and you want that content typed, related and queryable rather than sitting in files. A blog with authors and categories, a directory, a case-study library, a product catalogue. If that is not your site, the mapping work described above is effort spent building a machine you will not use.
Where to go next
For what Wix exports and what has to be crawled, read the Wix to code migration guide. If you have not settled on Payload yet and want the destination comparison, that is covered in the headless CMS migration guide. For the redirect and ranking side, the SEO migration survival guide is the one to read before you cut DNS.
If you want a second opinion on the mapping specifically, that is what a free migration review is for. We look at your actual collections, your actual body HTML, and tell you which parts convert cleanly and which parts are a modelling decision you have to make yourself.
0 images
What HTML to Lexical Carries
Payload does not upload img tags on conversion. They are dropped without an error
2 passes
To Fill a Relationship Field
Reference documents first, then content documents resolving names to IDs
No limit
Local API Write Rate
The bottleneck is downloading assets off Wix CDN, not writing into Payload
$8-$30/mo
Client-Grade Database
Payload is MIT licensed and free, but Payload Cloud has paused new deployments
Payload's docs state that converting HTML to Lexical does not automatically upload img tags. There is no warning and no error. Upload the images and rewrite the tags with data-lexical-upload-id before you convert, or the posts import looking correct in the list view with every body image missing.
Payload as the destination for a Wix site
Pros
- +Flat CSV columns become typed fields, so author, category and tags stop being strings repeated on every row
- +The Local API has no rate limit, so the write stage of the import is fast and cheap to re-run
- +Non-technical editors get a real admin UI with drafts, and the schema lives in the same repo as the frontend
- +TypeScript end to end, and the content model is version controlled rather than clicked into a dashboard
- +MIT licensed, so the software itself carries no per-seat cost as the team grows
Cons
- -Converting HTML to Lexical drops every img tag silently, so the image pipeline has to be built by hand
- -Relationship storage shape depends on relationTo and hasMany, so the schema must be locked before the bulk import
- -Wix layout wrappers, inline styles and embedded widgets have no Lexical equivalent and no lossy fallback mode
- -No first-party hosting since Payload Cloud paused new deployments, so you self-host and pay for a database
- -Overkill for a small brochure site with no repeating content, where flat files in a repo do the same job
How to migrate from Wix to Payload CMS
Model the collections from the CSV headers, not from the site
Open every exported CSV and decide, column by column, which Payload field type it becomes. Scalars are easy. The columns that repeat values, author, category and tags, are the ones that become relationship fields and drive the whole import order.
Tip: Decide relationTo, hasMany and any localized flags now. All three change how values are stored on disk.
Pass one: create the reference collections
Take the distinct set of values out of every repeating column, normalise case and whitespace so near-duplicates collapse into one, and create one document per value in its own collection. Persist the resulting name-to-ID map to disk before you write anything else.
Tip: Without normalisation, "jane doe" and "Jane Doe" become two author documents and the admin filters stop making sense.
Fetch and upload every image before touching the body HTML
Extract every image source from the crawled body HTML and the featured-image columns, strip the Wix CDN query-string transforms, download each file to disk, then create a media document for each with payload.create using filePath. Record a source-URL to document-ID map as you go.
Tip: Cache downloads to disk. This stage is the slow one, and you should never pay for the same download twice.
Rewrite the img tags, then convert to Lexical
Walk the HTML and replace each img tag with the data-lexical-upload-id and data-lexical-upload-relation-to attributes pointing at the media documents you just created. Only after that rewrite do you call convertHTMLToLexical. Converting first loses every image with no error raised.
Tip: convertHTMLToLexical needs JSDOM as a peer dependency, so this stage runs in Node, not at the edge.
Pass two: create the content documents
Write the actual documents, substituting each denormalised string for the ID recorded in pass one and attaching the Lexical body. Keep the original Wix identifier and URL path in indexed text fields so you can reconcile the import and build the redirect map from real data.
Tip: Re-running this stage should be cheap. If it is not, the asset stage and the write stage are too tightly coupled.
Reconcile, redirect, then launch
Count documents against source rows, spot-check that body images render rather than trusting the list view, map every old Wix URL to its new path with 301 redirects, and submit a fresh sitemap before cutting DNS.
Tip: Check the rendered post, not the admin list. A post with no images looks completely normal in a list of titles.
1// Wix body HTML -> Payload Lexical, with images surviving the trip.2// The order matters: uploads and tag rewriting BOTH happen before conversion.3import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'4import { JSDOM } from 'jsdom'5import config from '@payload-config'67const uploadCache = new Map<string, string>() // source URL -> media doc ID89async function uploadImage(payload, srcUrl: string) {10 const key = srcUrl.split('?')[0] // drop Wix CDN transform params11 if (uploadCache.has(key)) return uploadCache.get(key)!1213 const filePath = await downloadToDisk(key) // cached on disk, retryable14 const media = await payload.create({15 collection: 'media',16 data: { alt: '' }, // alt is a field on YOUR media collection, not a Payload default17 filePath,18 })1920 uploadCache.set(key, String(media.id))21 return String(media.id)22}2324export async function wixBodyToLexical(payload, html: string) {25 const dom = new JSDOM(html)2627 for (const img of dom.window.document.querySelectorAll('img')) {28 const src = img.getAttribute('src')29 if (!src) continue3031 const mediaId = await uploadImage(payload, src)32 img.setAttribute('data-lexical-upload-id', mediaId)33 img.setAttribute('data-lexical-upload-relation-to', 'media')34 }3536 // Only now. Convert first and every image is dropped, silently.37 return convertHTMLToLexical({38 // editorConfig is required and async. Omitting it throws.39 editorConfig: await editorConfigFactory.default({ config }),40 html: dom.window.document.body.innerHTML,41 JSDOM, // the imported constructor itself, not an instance42 })43}
Not sure how much of your Wix content converts cleanly?
Get a free migration review. We look at your actual CMS collections and your actual body HTML, and tell you which fields map straight into Payload and which parts are a modelling decision that has to be made before the import runs.
Frequently asked questions
- Why do images disappear from my post bodies after importing into Payload?
- Because Payload does not upload them, and does not tell you. The documentation states that when converting HTML to Lexical, img tags are not automatically uploaded, and the reason given is that Payload cannot know which uploads-enabled collection to add them to. The conversion succeeds, the document saves, and the images are simply absent. The fix is ordering: parse the HTML, extract every image source, download each one, create a media document with payload.create using filePath, build a source-URL to document-ID map, rewrite each img tag with data-lexical-upload-id and data-lexical-upload-relation-to, and run convertHTMLToLexical last.
- Can I import a Wix CSV author or category column straight into a Payload relationship field?
- No. A relationship field stores a document ID, and the CSV gives you a display name repeated on every row. That forces two passes. Pass one reads the distinct values out of each repeating column, normalises case and whitespace so near-duplicates collapse, creates one document per value, and records a name-to-ID map. Pass two creates the content documents, substituting the recorded ID for each string. Skipping the normalisation step is the usual cause of three author documents for one person.
- Why does the Payload schema have to be locked before the bulk import?
- Because relationship storage is not one shape, it is four, and two config flags decide which one you get. With relationTo as a string and hasMany false you store a bare ID. With relationTo as an array you store an object of relationTo and value. With hasMany true you store an array of either. Change a flag after importing and every document using that field has the wrong shape on disk. Localization behaves the same way: setting localized true changes a field from a scalar to a locale-keyed object, so it has to be decided before the first document is written.
- How long does a Wix to Payload import actually take to run?
- Less time than people expect on the writing side, and more on the fetching side. Payload imports go through the Local API with payload.create called directly from a script, which the docs endorse for seeding, and there is no rate limit. Your ceiling is the database. The real cost is downloading assets from Wix CDN one at a time. Build the script as two stages: a slow, retryable asset fetch that caches to disk, and a fast write stage you can re-run from zero after a schema change without re-downloading anything.
- What does the Wix rich text actually convert into?
- Payload rich text is Lexical JSON, handled by the richtext-lexical package, and note the field type is richText in camelCase rather than the hyphenated form used in the docs URL. Wix body HTML converts through convertHTMLToLexical, which requires JSDOM as a peer dependency and therefore runs in Node only. JSDOM does normalise malformed markup on the way through, which helps with Wix output where unclosed paragraph tags around block elements are common. There is no lossy fallback mode, so layout divs, inline styles and embedded Wix widgets need a decision per construct: map them into a blocks field, or drop them on purpose.
- How do Wix Stores products map into Payload?
- Products come out as a separate CSV, since Wix does not support an XML store export. Names and SKUs go to text fields, price and inventory to number fields after casting from strings, HTML descriptions to richText, and images to upload fields backed by a media collection. Variants are the hard part, because a flat CSV can only express them as extra columns or extra rows. In Payload you model them either as an array field on the product or as a separate collection with a relationship back, and that choice has to be made before the import for the same reason as any other schema decision. Checkout itself moves to Stripe or a headless commerce service.
- What does Payload cost to run after the migration?
- The software is MIT licensed and free, so the cost is hosting plus a database. Payload was acquired by Figma in June 2025, and Payload Cloud has paused new deployments, so there is no first-party hosting option to sign up for. Free database tiers are marginal for client work because compute gets suspended at the limits, which takes the admin panel offline. Budget $8 to $30 a month for a client-grade database, and roughly $20 to $50 a month on Vercel with a managed database. Vercel Hobby is non-commercial only and its terms specifically cover consultants being paid to build a site, so it is not an option for client work.
- What does a Wix to Payload migration cost to have done?
- Budget $5,000 to $25,000 for a 50 to 100 page site. On this specific pair the number is driven by two things rather than page count alone. First, how much of the archive has to be recovered by crawling rendered pages, since Wix has no blog export and its RSS feed is capped at 20 truncated excerpts. Second, how much of the body HTML is Wix widgets and layout wrappers, because each of those is a modelling decision about what it becomes in Payload rather than a conversion rule a script can apply.
Related Resources

Wix to Code Migration: What Actually Exports, and What Does Not
Wix has no code export, and the blog RSS feed everyone recommends returns 20 truncated excerpts instead of your archive. Here is what Wix genuinely gives you, what has to be crawled off the rendered pages, and what the rebuild costs.

Webflow to Headless CMS: Payload vs Sanity vs Storyblok (2026)
Leaving Webflow for a headless CMS comes down to Payload, Sanity and Storyblok. The destination barely changes the price, which runs $5,000 to $25,000 for a 50 to 100 page site. It changes your recurring bill, and who is on the hook when it breaks.

Webflow to Payload CMS: The Complete Migration Guide for 2026
Migrating from Webflow CMS to Payload keeps the design-first model of Collections, references and rich text, and adds code you own with TypeScript end to end. Payload's licence is free; hosting and a database are a separate bill. Typical: $5,000 to $25,000.

The SEO Migration Survival Guide: Keep Your Rankings When Switching Platforms
Platform migrations destroy SEO rankings if done wrong. Here's the checklist we use on every migration to protect (and improve) organic traffic.