How to Migrate from Sanity to UnfoldCMS
Sanity's free tier is generous until you hit production scale. Then the bills start: $99/month per project, $15 per extra seat, $1 per 100k API requests over quota, and overages from preview deploys you didn't even know were running. By month nine, agencies typically pay 5–15× what they signed up for. This page is for developers and teams who've decided enough is enough.
TL;DR: Migrating from Sanity to UnfoldCMS takes about half a day for typical content sites. Export your dataset with sanity dataset export, transform the NDJSON, import via UnfoldCMS REST API, set up redirects, point your frontend at the new endpoints. One-time UnfoldCMS license at $39–$799 replaces Sanity's $99–$3,000+/month bill. We offer a Migration Concierge service at $499 that handles it for you in 7 days.
Why Teams Migrate Off Sanity
Three reasons account for ~90% of Sanity migrations. None are speculative — they show up in every "we left Sanity" thread on r/nextjs and Hacker News.
1. Per-project pricing. Sanity charges per project, not per organization. An agency running 10 client sites pays 10 × $99/month = $990/month, or $11,880/year, before any overages or seat upgrades. The math doesn't survive a portfolio.
2. API request overages. Sanity Growth includes a fixed quota of API requests. Real teams burn through quotas with branch deploys, ISR revalidation, and CI runs that fetch content during build. Sanity then bills $1 per additional 100k requests. The bill quietly climbs from $99 to $400+/month over six months — a pattern repeated in dozens of public threads.
3. Per-seat fees. The first 3 users are free. Every user after that is $15/month. Add a fourth team member and you're at $144/month before any overage. Add an eighth, and you're at $174/month — a 75% premium over the base price.
We covered the cost reality in detail in UnfoldCMS vs Sanity — including the math on when self-hosted breaks even.
What "Migrate from Sanity" Actually Involves
Five concrete steps. None require Sanity's enterprise tier or paid migration tools.
- Export your Sanity dataset using the official
sanity dataset exportCLI - Transform the NDJSON — Sanity's structure (Portable Text, references, asset hotspots) differs from any other CMS, so a transformer maps document types, references, and assets to the target schema
- Import to UnfoldCMS via the REST API (single endpoint, accepts the transformed JSON)
- Migrate assets — download from Sanity's CDN, upload to your media store (S3, Cloudflare R2, or local disk)
- Update your frontend — replace GROQ queries with UnfoldCMS REST/GraphQL calls
For a site with 200 documents and 500 assets, total work is 4–8 hours including testing.
Step-by-Step: Sanity to UnfoldCMS Migration
Step 1: Install the Sanity CLI and authenticate
npm install -g @sanity/cli
sanity login
sanity init --env # or use an existing project's sanity.config.ts
You'll need:
- Project ID — find it in
sanity.config.tsor at sanity.io/manage - Dataset name — usually
production(Sanity → Manage → Datasets) - API token — Sanity → Manage → API → Tokens → Add API Token (Read access is enough for export)
Step 2: Export the full dataset
sanity dataset export production sanity-export.tar.gz \
--asset-concurrency 10
This produces a .tar.gz archive containing:
data.ndjson— every document, one JSON object per lineassets.json— asset metadata (filenames, dimensions, hotspots)images/andfiles/directories — all uploaded assets
For a 500-document dataset, expect a 50–500 MB export depending on asset weight.
Extract the archive:
mkdir sanity-export && tar -xzf sanity-export.tar.gz -C sanity-export
cd sanity-export && ls
# data.ndjson assets.json images/ files/
Step 3: Transform the NDJSON to UnfoldCMS format
Sanity's export structure looks like this (one document per line in data.ndjson):
{
"_id": "abc123",
"_type": "post",
"_createdAt": "2025-04-01T10:00:00Z",
"title": "My Post",
"slug": { "current": "my-post" },
"body": [
{ "_type": "block", "children": [{ "_type": "span", "text": "Hello world" }] }
],
"author": { "_ref": "xyz789", "_type": "reference" }
}
UnfoldCMS expects flat post data:
{
"title": "My Post",
"slug": "my-post",
"body": "<p>Hello world</p>",
"extra_attributes": { "sanity_id": "abc123", "author_ref": "xyz789" }
}
A 70-line Node script handles the transform. Reference implementation:
// transform.js
import fs from 'fs';
import readline from 'readline';
import { toHTML } from '@portabletext/to-html';
const posts = [];
const rl = readline.createInterface({
input: fs.createReadStream('sanity-export/data.ndjson'),
});
for await (const line of rl) {
const doc = JSON.parse(line);
if (doc._type !== 'post') continue;
posts.push({
title: doc.title ?? 'Untitled',
slug: doc.slug?.current ?? doc.title?.toLowerCase().replace(/\s+/g, '-'),
body: toHTML(doc.body ?? [], { components: customSerializers }),
seo_title: doc.seo?.title ?? null,
meta_desc: doc.seo?.description ?? null,
posted_at: doc._createdAt,
content_type: 'post',
is_published: !doc._id.startsWith('drafts.'),
extra_attributes: {
sanity_id: doc._id,
sanity_type: doc._type,
author_ref: doc.author?._ref ?? null,
},
});
}
fs.writeFileSync('unfold-import.json', JSON.stringify(posts, null, 2));
console.log(`Transformed ${posts.length} posts`);
The @portabletext/to-html package converts Portable Text to HTML compatible with UnfoldCMS's body field. Custom serializers handle inline objects, embedded images, and custom marks — see Step 6 below for edge cases.
Step 4: Import to UnfoldCMS
UnfoldCMS exposes a bulk import endpoint:
curl -X POST https://your-unfoldcms.com/api/posts/bulk-import \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d @unfold-import.json
The endpoint accepts an array of post objects and returns a result summary:
{
"imported": 487,
"skipped": 13,
"errors": [
{ "index": 42, "reason": "duplicate slug" }
]
}
For 500 documents, the import takes 30–60 seconds.
Step 5: Migrate assets
Two paths depending on volume:
Under 1,000 assets — script the upload from Sanity's CDN to your UnfoldCMS media store:
// upload-assets.js
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
import FormData from 'form-data';
const assetsDir = 'sanity-export/images';
const files = fs.readdirSync(assetsDir);
for (const filename of files) {
const filePath = path.join(assetsDir, filename);
const buffer = fs.readFileSync(filePath);
const form = new FormData();
form.append('file', buffer, filename);
await fetch('https://your-unfoldcms.com/api/media', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.UNFOLD_TOKEN}` },
body: form,
});
console.log(`Uploaded ${filename}`);
}
Over 1,000 assets — keep them on Sanity's CDN and rewrite URLs in your post bodies. Sanity's free tier covers asset hosting indefinitely once you stop publishing new content, so you can read from cdn.sanity.io without paying for the dataset itself.
Step 6: Set up redirects
If your URLs are changing, redirect old Sanity-driven URLs to new UnfoldCMS slugs.
UnfoldCMS has a built-in redirects table. Bulk-import a CSV via the admin or API:
from,to,status
/blog/old-slug-1,/blog/new-slug-1,301
/blog/old-slug-2,/blog/new-slug-2,301
Or via API:
curl -X POST https://your-unfoldcms.com/api/redirects/bulk \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: text/csv" \
--data-binary @redirects.csv
Step 7: Update your frontend
Replace GROQ queries with UnfoldCMS fetches. Before:
import { createClient } from 'next-sanity';
const client = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: 'production',
apiVersion: '2024-01-01',
useCdn: true,
});
const posts = await client.fetch(`*[_type == "post"] | order(_createdAt desc)`);
After:
const res = await fetch(`${process.env.UNFOLD_API_URL}/api/blog/posts`, {
next: { revalidate: 60 },
});
const { data: posts } = await res.json();
For Next.js App Router, this lands directly in Server Components — no SDK, no token gymnastics, no GROQ to learn.
What Breaks (and How to Handle It)
Real migrations always hit edge cases. Here are the common ones for Sanity.
Portable Text custom marks and inline objects. Portable Text is Sanity's structured-content format — JSON arrays of blocks with marks (bold, link) and inline objects (embedded images, code blocks, callouts). The @portabletext/to-html package handles standard marks well, but custom marks (highlights, footnotes, internal-link annotations) need explicit serializers. Plan ~2 hours per custom mark or block type.
Document references. Sanity uses _ref fields heavily — every linked document is a reference, not an embedded copy. After import, you need a second pass to resolve these — match the sanity_id stored in extra_attributes to the new UnfoldCMS post IDs and update the body HTML or relational joins.
Asset hotspots and crops. Sanity assets carry hotspot/crop metadata used by @sanity/image-url for on-the-fly transformations. UnfoldCMS doesn't preserve hotspot data by default — if your frontend depends on focal-point cropping, store the hotspot JSON in extra_attributes and apply it client-side, or pre-render cropped versions during migration.
Drafts and document states. Sanity stores drafts as separate documents with the drafts. prefix on _id. Decide whether to migrate drafts (filter doc._id.startsWith('drafts.') and import as is_published: false) or skip them entirely. Most teams skip drafts during migration and republish from scratch.
Datasets and environments. Sanity datasets are content namespaces (production, staging, preview). Each is migrated separately. UnfoldCMS handles environments through separate installs — one license per domain, with the Agency tier covering unlimited domains.
Webhooks and integrations. Sanity webhooks and GROQ-powered listeners don't transfer — these are rebuilt in UnfoldCMS. UnfoldCMS has webhooks for content changes; map them to your existing integrations (Algolia, deploy hooks, Slack notifications).
Timeline: How Long Does Sanity Migration Take?
For typical content sites, the breakdown:
| Site size | Documents | Assets | DIY time | Concierge time |
|---|---|---|---|---|
| Small | < 100 | < 500 | 4 hours | 3 days |
| Medium | 100–500 | 500–2,000 | 1 day | 7 days |
| Large | 500–2,000 | 2,000–10,000 | 2–3 days | 14 days |
| Enterprise | > 2,000 | > 10,000 | 1–2 weeks | Custom quote |
DIY time assumes a developer who's read this guide and is comfortable writing 100 lines of Node.
Concierge time is the Migration Concierge service — $499 for sites up to 500 documents, custom quote above that. We handle the export, transform, import, asset migration, redirects, and frontend updates.
When NOT to Migrate Off Sanity
To be honest about it: Sanity is genuinely strong for some teams.
Stay on Sanity if:
- Multiple editors collaborate on the same documents in real-time, daily — Sanity's live cursors and presence are class-leading
- Your team uses Sanity Studio extensions, custom block types, and bespoke editorial workflows heavily
- You have approved budget for $99–$3,000/month in CMS infrastructure
- Your content model leans on structured-content patterns that Portable Text expresses naturally (richly annotated text, embedded data widgets)
- Your stack is fully JavaScript and adopting PHP/Laravel adds operational friction
- You depend on Sanity's image pipeline (focal points, on-the-fly crops, global CDN) and don't want to manage CDN configuration yourself
If two or more of these apply, migration probably isn't the win you're hoping for. Read the honest comparison and decide accordingly.
What You Get After Migrating
Direct comparison for a typical mid-size content site:
| Sanity | UnfoldCMS | |
|---|---|---|
| Year 1 cost | $1,200–$11,880 | $39–$799 (one-time) + $5/mo hosting |
| Per-project fees | Yes | No |
| Per-seat fees | Yes ($15/seat after 3) | No (unlimited) |
| API request limits | Yes (metered, $1/100k overage) | No |
| Content modeling | Excellent (Portable Text, references) | Strong (custom fields + JSON) |
| Real-time collaborative editing | Yes | No |
| Studio customization | Excellent (React extensions) | Theme-level (shadcn/ui) |
| Image CDN + transformations | Built-in | Self-managed (Cloudflare works well) |
| Built-in SEO records | No (plugin) | Yes |
| Built-in redirects table | No | Yes |
| Built-in comments | No | Yes (optional) |
| Built-in forms | No | Yes |
| Includes themed frontend | No (headless only) | Yes |
| Source code access | No | Yes (full ownership) |
| Self-hosted option | No | Yes |
Trade-offs are real — Sanity's real-time collaboration and Studio customization are genuinely better. But for the 80% of teams who don't need real-time multi-cursor editing, UnfoldCMS replaces Sanity for ~95% less.
FAQ
How much does it cost to migrate from Sanity? Free if you DIY (just developer time). The Migration Concierge service is $149 for a 30-min consultation + written plan, or $499 for done-for-you migration of one site up to 500 documents + media.
Will my SEO survive a Sanity to UnfoldCMS migration? Yes if you set up 301 redirects from old URLs to new ones, preserve content per URL, regenerate your sitemap, and resubmit to Google Search Console. The most common SEO mistake is shipping the migration without redirects — that drops you from page 1 to page 5 for weeks.
Can I run Sanity and UnfoldCMS in parallel during migration? Yes. Common pattern: import to UnfoldCMS, run both side-by-side for 2 weeks, run a sitemap diff to catch missing entries, then cut DNS or revalidate your frontend's API URLs to point at UnfoldCMS. Sanity stays as your safety net.
What about Sanity Studio extensions and custom plugins? Studio extensions don't transfer — they're React components tied to Sanity's content store. Most editorial workflows rebuild trivially in UnfoldCMS using built-in features (custom fields, scheduled publishing, drafts, SEO records). Truly bespoke editorial UIs need custom Inertia pages — not a drop-in port, but typically smaller scope than the original Studio extensions.
How do I migrate Portable Text to UnfoldCMS?
Use @portabletext/to-html to convert Portable Text JSON to HTML. Standard blocks (paragraphs, headings, lists, links) convert cleanly. Custom marks and inline objects (highlights, footnotes, embedded data widgets) need explicit serializers — plan 1–2 hours per custom type.
Will Sanity charge me during the migration? Yes — Sanity bills monthly until you delete the project or downgrade. After cutover, downgrade to the free tier (3 users, 10k documents, 500k API requests) and verify your frontend doesn't need it for 30 days, then delete the project.
How do I migrate Sanity references between documents?
Sanity references (_ref fields) are migrated in two passes. First pass: import all documents with their sanity_id stored in extra_attributes. Second pass: walk every document, resolve _ref values against the sanity_id map, and update the body HTML or set up relational joins in UnfoldCMS.
Is the Sanity Content Lake API still available during migration? Yes. Sanity keeps the API running until you delete the project. Run the migration with both APIs available and switch your frontend's environment variable to cut over.
What about asset URLs in Portable Text?
The @portabletext/to-html serializer can be configured to rewrite asset URLs during conversion. Map Sanity asset references ({ _type: "image", asset: { _ref: "image-abc-1200x800-png" } }) to your new UnfoldCMS media URLs in a single transform pass — no second pass needed.
Methodology
Pricing data is from Sanity's public pricing page as of May 2026. Migration time estimates are based on production migrations our team has executed for content sites of varying sizes. Step-by-step instructions reference Sanity's official CLI documentation and UnfoldCMS API docs. Edge cases (Portable Text, references, hotspots) are sourced from real migration tickets in our support queue.
Migrate from Sanity to UnfoldCMS
Three paths depending on how hands-on you want to be:
- DIY — follow this guide, write the transform script yourself, ship it in a day. Free.
- Migration Starter ($149) — 30-min call where we audit your Sanity dataset, identify migration risks, and write you a tailored migration plan. You execute it.
- Migration Concierge ($499) — done for you. We export, transform, import, migrate assets, set up redirects, and update your frontend. 7 days, up to 500 documents. Custom quote above.
Get started with UnfoldCMS — one-time license, full source, no per-environment fees. The Core tier at $39 is enough to test the full migration on a real Sanity dataset.
For more context, see UnfoldCMS vs Sanity, Migrate from Contentful, or the Best CMS for Next.js breakdown.
If after reading this you decide Sanity is still the right call, that's a fine answer too. The point of this page is to give you the information to choose, not to push every team toward migration.
Related: Migrate from Strapi, Migrate from WordPress, CMS for SaaS marketing sites, or browse all migration guides.