GuideWixNext.jsastro

Wix to Storyblok Migration Guide (2026)

Wix gives you the least structure of any source and Storyblok demands the most. That gap is the project: components must exist before a single story can be posted, so the modelling outweighs the scripting. Here is the mapping and the rate limits.

M

MigrateLab Team

Migration Experts

11 min read· Last updated
Wix to Storyblok Migration: The Widest Structural Gap

The Verdict First

Wix to Storyblok is the most awkward source-to-destination pairing you can pick from the common options, and knowing that before you scope the project is worth more than any script.

The reason is structural, not incidental. Wix gives you the least structure of any source platform: a flat CSV per CMS collection, a separate CSV for Stores products, no code export at all, and a blog feed capped at 20 truncated excerpts. Storyblok demands the most structure of any destination: you cannot post a single story until the components it references already exist in the space.

So the gap between what one side hands you and what the other side accepts is as wide as it gets. Everything below follows from that one fact. The practical consequence: on this pairing, the modelling work outweighs the scripting work, and the modelling is a judgement call that no script makes for you.

If you are still deciding which headless CMS to move to, that is a different question and it is answered in the headless CMS migration guide. This page assumes you have already chosen Storyblok.

What Wix Actually Gives You

Briefly, because this is covered in full in the Wix to code migration guide.

Wix has no code export of any kind. CMS collections export to CSV, capped at 1GB or 50,000 items. Store products export to CSV, and XML store export is explicitly not supported. Blog posts have no export feature at all: the RSS feed at /blog-feed.xml is hard capped at 20 items with no pagination and carries plain-text excerpts of roughly 512 characters, so the archive has to be recovered by crawling the rendered post pages listed in sitemap.xml.

One thing to note, because it changes how you plan the extraction phase: Wix ships an official MCP server at mcp.wix.com/mcp, so an agent can reach your Wix content through Wix's own API surface. That helps with inventory and auditing. It does not produce a code export, because Wix never produces one.

And a reminder on why people leave: Wix discontinued ADI on 2024-11-10, and unpublished ADI sites that had been inactive for six months or more were deleted. Not archived, deleted. That is a concrete, dated illustration of what it means to hold your content somewhere with no export path.

Why Storyblok Is The Demanding Half

Storyblok's hierarchy is Space, then Folders, then Stories, then Blocks. There are two schema layers, and the vocabulary matters because Storyblok's own docs are consistent about it:

  • A component is the schema. It defines the fields.

  • A block is an instance of a component, sitting inside a story.

  • bloks is the field type that nests blocks inside other blocks.

Components come in three flavours: content type (is_root: true), nestable (is_nestable: true), and universal (both). Content types are the story-level schemas, and Storyblok's docs describe content types as a subset of components. A story is itself a type of block.

Here is the part that determines your migration order. When you POST /spaces/:id/stories against the Management API at https://mapi.storyblok.com/v1, the body needs name, slug, and a content object that contains at least {"component": "..."}. That component name must match a component that already exists in the space.

There is no schema-on-write. Storyblok will not infer a component from the payload you send. This is the single biggest difference from a CMS where you can push a document and let the shape follow. Components come first, always, and getting them wrong means re-modelling and re-importing rather than patching.

Storyblok's available field types are text, textarea, richtext, markdown, number, boolean, datetime, option, options, bloks, asset, multiasset, multilink, table, section, and custom. The older image and file types are deprecated and superseded by asset, so do not model against them. section groups fields visually in the editor without changing the API response, which is useful for editor experience and irrelevant to your import.

The Actual Gap: Flat Columns Into Composable Blocks

This is the heart of the pairing.

A Wix CSV row is scalars. Every column is a string, a number, or a date. Rich text arrives as an HTML blob or with its formatting already flattened. Images are absolute Wix CDN URLs, frequently carrying query-string transforms. There are no stable IDs across exports. Repeated values like author name or category are denormalised, written out in full on every single row.

A Storyblok story, used properly, is a tree. A page is a content-type block whose body field is a bloks field holding an ordered sequence of nestable blocks: a hero, then a rich text section, then a quote, then an image gallery, then a call to action.

Those two shapes have almost nothing in common, and here is what happens if you ignore that.

Import the CSV literally and you get a component with a dozen flat `text` fields. It will work. The API will accept it. The stories will exist. And you will have built a Storyblok space that does not behave like Storyblok: no composability, no reusable blocks, no meaningful visual editor, and a content team that cannot assemble a new page without a developer. You have paid for a block-based CMS and received a spreadsheet with a login.

The alternative is to infer block structure from the body blob. Parse the HTML, detect that this run of markup is a quote, that one is a gallery, that heading and the two paragraphs under it are a feature section, and emit the right nested blocks.

That inference is heuristic, not mechanical. It works well on consistently authored content and degrades on anything hand-styled inside the Wix editor, which is a lot of Wix content, because the Wix editor encourages per-page styling. A script can propose the split. Deciding which recurring visual pattern deserves to become a reusable component, and which is a one-off that should stay inside rich text, is a design decision about how the content team will work after launch.

This is the honest reason this pairing costs what it costs. It is not that the scripting is hard. It is that the modelling is a judgement call made once, early, with the whole space downstream of it. Nobody should sell you a script that magically solves it.

The Half That Is Genuinely Solved

Storyblok has an official HTML converter, and this is a real advantage worth planning around.

@storyblok/richtext v5 ships htmlToStoryblokRichtext and markdownToStoryblokRichtext, plus renderRichText for going the other way. It handles `img`. Older Storyblok packages were render-only, which is where the outdated claim comes from, and it is no longer true.

Storyblok's rich text is JSON based on TipTap, which is built on ProseMirror. The root node is type: "doc" and formatting lives in a marks array on text nodes. The format also includes a blok node, which means components can be embedded inline inside rich text.

That last detail is more useful than it first looks. When your HTML parser hits a construct that has no rich text equivalent, a Wix layout <div>, an inline-styled callout, an embedded widget, you are not forced to choose between dropping it and mangling it. You can map it to a blok node pointing at a component you defined for exactly that purpose. Layout divs, inline styles and widgets still fall outside the supported element list, so this is lossy at the edges, but you have somewhere to put the edges.

Throughput: The Constraint You Will Feel On Import Day

Storyblok's write path is an HTTP funnel and it is narrow.

There is no bulk story endpoint. One POST per story. Rate limits are 3 requests per second on Starter and 6 on Growth and above. Combined, that is the dominant throughput constraint on any Storyblok migration, and it does not go away with better code.

Assets are worse, because each file is a three-step sequence:

  1. A signed request to POST /spaces/:id/assets.

  1. A multipart POST to the returned post_url, forwarding every key in the returned `fields` object verbatim, with the file appended last.

  1. An optional finish step.

Field order in that multipart body is a common silent failure. Put the file anywhere but last, or drop a field you assumed was optional, and the upload fails in ways that are not obvious from the response.

The quota detail that catches people out: the S3 POST does not consume API quota, but the signed request does. So an asset-heavy import hits the rate limit roughly twice as fast as the story count alone would predict. Budget from stories plus assets, not stories.

Three things every Storyblok import script needs, and at this scale they are not optional:

  • A token bucket, sized to your plan tier, gating every call that touches the Management API.

  • 429 backoff, because you will hit the limit even with the bucket, and retrying immediately makes it worse.

  • Resumability via a ledger on disk. Persist a source-ID-to-story-ID map as you go. A 40-minute import that dies at minute 30 must resume, not restart. Without the ledger, a restart either duplicates everything or leaves you hand-cleaning a half-populated space.

Relations add another pass. Storyblok relations are UUID strings and are not resolved by default. The clean approach is two-phase: create every story with relation fields empty, record the returned UUIDs in your ledger, then PUT to backfill the relations. That doubles your request count against an already tight limit, so account for it in the timeline rather than discovering it on the day.

Two more things to know before launch. resolve_relations takes component.field format, and a maximum of 50 stories can be resolved in a single request, which is a modelling constraint disguised as an API detail. And internal links carried over from Wix must be rewritten to Storyblok slugs, or they will silently keep pointing at the old site. Multilink fields store {id, url, linktype, fieldtype, cached_url}, with linktype being either story or url, so an unrewritten link is a valid-looking url link to a domain you are about to stop paying for.

For a second pass over content already inside Storyblok, @storyblok/migrations is genuinely useful. It is not an external-CMS importer, though. It transforms content that is already in the space, so it does not help with getting Wix content in.

The Seat Problem, Which Is A Client Handoff Problem

Storyblok's free tier is one seat.

For a solo operator that is fine. For any project that ends with a handoff, it is a real constraint and it belongs in the scoping conversation rather than the launch email. One seat means the client's content team and you cannot both be in the space. Growth is $99 per month for 5 seats.

That is not an argument against Storyblok. It is a line item that belongs in the proposal, because a client who discovers it a week after launch experiences it as a surprise bill rather than a plan.

On the front end, hosting a static-first build costs $0 to $20 per month on Cloudflare Pages, or roughly $20 to $50 per month on Vercel with a managed database. Storyblok's own subscription sits on top of that.

Migration Order, And What Breaks If You Get It Wrong

  1. Inventory from `sitemap.xml`, not from the Wix editor. Wix generates utility and member pages people forget exist. This list is your scope and your redirect map.

  1. Extract everything Wix will give you. CMS collections to CSV, Stores products to CSV, contacts and form submissions to CSV, and every asset from the Media Manager. Download media while the subscription is active, because Wix CDN URLs stop resolving once the plan lapses.

  1. Crawl the blog archive. The feed reaches 20 posts. Everything older comes from crawling the rendered pages, extracting body HTML, and collecting inline images rather than just featured ones.

  1. Model the components. Do not skip ahead. Audit the recovered HTML for recurring patterns, decide which become nestable components, define content types for each page kind, and create them in the space. Nothing can be imported before this exists, and changing it later means importing again.

  1. Convert and import stories, relations empty. Run HTML through htmlToStoryblokRichtext, map unsupported constructs to blok nodes, POST one story at a time through the token bucket, and write every source-ID-to-story-ID pair to the ledger as you go.

  1. Upload assets and rewrite references. Three steps per file, quota-aware, resumable.

  1. Backfill relations and rewrite internal links using the ledger from step 5.

  1. Map 301 redirects from every URL in the sitemap and test them before DNS moves.

The two failure modes worth naming: modelling after importing means doing the import twice, and importing without a ledger means a failed run leaves you with a space you can neither cleanly resume nor cleanly delete.

When You Should Not Do This

A Wix site with fewer than 20 pages, no blog archive worth preserving, and one person editing it does not need Storyblok. The seat limit will not bite, but neither will you recover the modelling cost. A static site with content in files, or a simpler CMS, is the better answer.

Storyblok earns its keep when a content team needs to assemble pages from reusable blocks without a developer in the loop, and when there is enough content that the block library gets reused. If your content is genuinely a flat list of posts, you are paying a modelling cost for composability you will not use.

What It Costs

Budget $5,000 to $25,000 for a 50 to 100 page site. On this pairing the balance inside that band tilts toward the modelling and content-recovery phases rather than the build, for the reasons above: Wix hands over the least structure, Storyblok requires the most, and the work in between is the project.

MigrateLab scopes the component model before quoting, because on Wix to Storyblok that decision drives everything downstream. A free migration review looks at your live Wix site, tells you exactly what can be extracted, proposes the Storyblok component model, and gives you a fixed price and timeline before you commit.

0

Structure From Wix

Flat CSV from collections and Stores. No code export at all

3-6/s

Storyblok Write Rate

Starter 3 req/s, Growth and above 6. No bulk story endpoint

1

Free Tier Seats

A real client handoff constraint. Growth is $99/mo for 5 seats

3 steps

Per Asset Upload

Signed request, multipart POST, optional finish step

Storyblok has no schema-on-write. A story cannot be created until the component it references already exists in the space, because the content object must contain at least a component key matching a defined component. This inverts the usual migration order: you model the components first, then import, and changing the model afterwards means importing again rather than patching. On a Wix source there is nothing in the export that tells you what the components should be, so this step is a human decision made once, early, with the whole space downstream of it.

FeatureWhat Wix Hands YouWhat Storyblok Requires
Content shapeFlat CSV rows of scalar columnsNested tree of blocks inside a bloks field
SchemaNone. Column headers onlyComponents defined in the space before any import
Rich textHTML blob, or formatting already flattenedTipTap JSON. Official htmlToStoryblokRichtext handles it
ImagesAbsolute Wix CDN URLs with query-string transformsasset field, uploaded via a three-step signed flow
Blog archiveNo export. Feed capped at 20 truncated excerptsOne story per post, one POST per story
Repeated values (author, category)Denormalised strings on every rowUUID relations, backfilled in a second pass
Stable IDsNone across exportsYour own ledger has to supply them
Write throughputNot applicable, export is a file3 req/s Starter, 6 on Growth. No bulk endpoint
SeatsWix plans from $17/mo annualFree tier is 1 seat. Growth $99/mo for 5

The Wix to Storyblok Playbook

1

Inventory from the sitemap

Tip: Wix generates utility and member pages people forget exist. Diff the sitemap against the editor page list before quoting.

2

Extract everything Wix will give you

Tip: Download media while the subscription is active. Wix CDN URLs stop resolving once the plan lapses, and those URLs are the only pointer you have.

3

Crawl the blog archive

Tip: Check your selector against the oldest posts as well as the newest. Older Wix posts often predate an editor change and use different markup.

4

Model the components, before importing anything

Tip: This is the step that decides whether the space behaves like Storyblok. Importing CSV columns literally gives you a component of flat text fields and no composability.

5

Convert and import stories with relations empty

Tip: Leave relation fields empty on this pass. Filling them requires UUIDs that only exist after the target stories are created.

6

Upload assets, quota-aware

Tip: The upload itself does not consume API quota but the signed request does, so asset-heavy imports hit the rate limit about twice as fast as story counts suggest.

7

Backfill relations and rewrite internal links

Tip: Unrewritten internal links stay valid-looking url links pointing at the Wix domain you are about to stop paying for. They fail silently.

8

Map redirects, launch, and monitor

Tip: Build the map from the sitemap, never from the blog feed. The feed only sees 20 posts, so a feed-driven map silently drops the archive.

Wix to Storyblok: Honest Trade-offs

Pros

  • +HTML ingestion is genuinely solved: htmlToStoryblokRichtext is official and handles img
  • +The blok node lets unsupported Wix constructs become components instead of being dropped
  • +A well-modelled space lets the content team assemble pages without a developer
  • +Content leaves a platform with no export path and lands somewhere with a documented API
  • +Front-end hosting drops to $0 to $20 per month on Cloudflare Pages, or $20 to $50 on Vercel

Cons

  • -The widest structure gap of any common pairing: flat CSV in, composable blocks required
  • -Components must be modelled before any import, and changing them later means importing again
  • -Inferring blocks from a body blob is heuristic and needs human review, not just a script
  • -No bulk endpoint and a 3 to 6 req/s ceiling make import day slow and failure-prone without a ledger
  • -The free tier is one seat, so any client handoff means Growth at $99 per month

Importing a Wix CSV literally will succeed. The API accepts it, the stories exist, and you have built a Storyblok space that does not behave like Storyblok. The failure is in the model, not the script, which is why the model is the thing to scope.

MigrateLab Team, Migration Specialists

Get a Wix to Storyblok plan for your specific site

On this pairing the component model drives everything downstream, so it is worth settling before anyone writes an import script. A free migration review looks at your live Wix site, tells you exactly what can be extracted, proposes the Storyblok component model, and gives you a fixed price and realistic timeline before you commit to anything.

Frequently asked questions

Can you migrate a Wix site to Storyblok?
Yes, but it is the most structurally awkward of the common pairings, and scoping it as a scripting job is the usual mistake. Wix exports flat CSV from its CMS collections and a separate CSV from Stores, with no code export and no blog export. Storyblok will not accept a story until the component it references already exists in the space, because the content object must contain at least a component key matching a defined component. So the work splits unevenly: designing the component model is the hard part, and pushing content through the Management API is the routine part. Plan the modelling phase explicitly rather than treating it as setup.
Why does a Wix to Storyblok migration need so much modelling work?
Because the two shapes have nothing in common. A Wix CSV row is scalars: every column is a string, a number or a date, rich text arrives as an HTML blob, and repeated values like author or category are written out in full on every row. A Storyblok story used properly is a tree, where a content-type block holds a bloks field containing an ordered sequence of nestable blocks. Nothing in the CSV tells you where one block ends and the next begins. You either import the columns literally, which produces a component with a dozen flat text fields and no composability, or you infer block structure from the body HTML, which is heuristic and needs human review.
Does Storyblok have an official HTML to rich text converter?
Yes. The @storyblok/richtext package at v5 ships htmlToStoryblokRichtext and markdownToStoryblokRichtext, alongside renderRichText for going the other way, and it does handle img elements. Older Storyblok packages were render-only, which is where the outdated claim that you have to write your own parser comes from. Storyblok rich text is JSON based on TipTap, which is built on ProseMirror: the root is type doc and formatting lives in a marks array on text nodes. The format also includes a blok node, so when your HTML hits a construct with no rich text equivalent, such as a Wix layout div or an embedded widget, you can map it to a component instead of dropping it.
How long does importing content into Storyblok take?
Longer than you expect, because there is no bulk story endpoint. It is one POST per story, rate limited to 3 requests per second on Starter and 6 on Growth and above, and that funnel is the dominant throughput constraint on any Storyblok migration. Assets make it worse: each file is a signed request, then a multipart POST to the returned URL forwarding every key in the fields object verbatim with the file appended last, then an optional finish step. The signed request consumes API quota even though the upload itself does not, so an asset-heavy import hits the limit roughly twice as fast as the story count alone suggests. Relations need a second pass too, which doubles requests again.
What does a Storyblok import script actually need?
Three things that are easy to skip and painful to add later. A token bucket sized to your plan tier, gating every call that touches the Management API. Backoff on 429 responses, because you will hit the limit even with the bucket, and retrying immediately makes it worse. And resumability through a source-ID-to-story-ID ledger persisted to disk, written as each story is created. A 40-minute import that dies at minute 30 has to resume rather than restart, and without the ledger a restart either duplicates everything or leaves a half-populated space that has to be cleaned by hand. The same ledger is what you use afterwards to backfill relations and rewrite internal links.
Is Storyblok free, and does the free tier work for a client project?
The free tier exists but it is one seat, and that is a genuine constraint the moment a project involves a handoff. One seat means you and the client content team cannot both be in the space, so it works for a solo operator and does not work for a delivery with an editorial team on the other side. Growth is $99 per month for 5 seats. This belongs in the proposal rather than the launch email, because a client who finds out a week after go-live experiences it as a surprise bill. Front-end hosting is separate and modest: $0 to $20 per month on a static-first stack with Cloudflare Pages, or roughly $20 to $50 per month on Vercel with a managed database.
What is the right order for a Wix to Storyblok migration?
Inventory from sitemap.xml rather than the Wix editor, since Wix generates pages people forget. Export the CMS collections and Stores products to CSV and download all media while the subscription is still active, because Wix CDN URLs stop resolving once the plan lapses. Crawl the blog archive from the sitemap, since the feed only reaches 20 posts. Then model and create the components, before importing anything at all. Import stories with relation fields left empty, recording every ID pair in the ledger. Upload assets. Backfill relations and rewrite internal links from the ledger. Map and test 301 redirects before DNS moves. Modelling after importing means doing the import twice.
When is Storyblok the wrong destination for a Wix site?
When the content is genuinely flat. A Wix site under 20 pages with no blog archive worth preserving and one person editing it will not recover the modelling cost, and the one-seat limit never bites because there is only one editor. Content in files, or a simpler CMS, is the better answer there. Storyblok earns its keep when a content team needs to assemble pages from reusable blocks without a developer in the loop, and when there is enough content that the block library actually gets reused. Paying for composability you will not use is a real way to overspend on this pairing.

Related Resources