Migrate from Strapi to UnfoldCMS
From Strapi to a self-hosted Laravel CMS in half a day — without the v5 upgrade fight
Strapi is the headless CMS most teams reach for first. It's also the one most teams quietly drop a year later — when the plugin ecosystem cracks, when the v4-to-v5 upgrade breaks every custom field, or when Strapi Cloud's $499/month bill arrives without warning. This page is for developers and teams who've decided enough is enough.
TL;DR: Migrating from Strapi to UnfoldCMS takes about half a day for typical content sites. Export your data with strapi export, transform the JSON, import via UnfoldCMS REST API, migrate uploads, point your frontend at the new endpoints. One-time UnfoldCMS license at $39–$799 replaces Strapi Cloud's $29–$499/month bill — and the upgrade churn that comes with major Strapi versions every 18 months. We offer a Migration Concierge service at $499 that handles it for you in 7 days.
Why Teams Migrate Off Strapi
Three reasons account for ~90% of Strapi migrations. None are speculative — every "we left Strapi" thread on r/node, r/strapi, and Hacker News repeats the same pattern.
1. Major version upgrade pain. Strapi v3 → v4 was a full rewrite. v4 → v5 broke the entity service API, custom field formats, and most third-party plugins. Teams running custom code or older plugins routinely lose 1–3 weeks per major upgrade — and Strapi ships a new major version every 18–24 months. Postponing the upgrade locks you on an unsupported version with security backports for ~12 months.
2. Plugin ecosystem churn. The community plugin marketplace has hundreds of entries, but a meaningful percentage are abandoned, lag behind major versions, or silently break on minor releases. Production sites running 5+ plugins typically hit one broken plugin per quarter. The cost shows up as developer hours, not invoices — but it's real.
3. Strapi Cloud pricing. Strapi Cloud is the managed hosting option Strapi pushes to teams who don't want to run Node themselves. Pricing starts at $29/month for the Essential tier, jumps to $99/month for Pro, and lands at $499/month for Team. For agencies running multiple Strapi sites, multiplying by 5 or 10 client projects gets expensive fast. Self-hosting is free in license, but operational cost is hidden — Node runtime, managed Postgres, build pipelines, monitoring.
We covered the trade-offs in detail in UnfoldCMS vs Strapi — including when self-hosting Strapi makes sense and when it doesn't.
What "Migrate from Strapi" Actually Involves
Five concrete steps. None require Strapi Enterprise or paid migration tools.
- Export your Strapi data using the official
strapi exportCLI - Transform the JSON — Strapi's structure (Content Types, Components, Dynamic Zones, Media references) maps cleanly to UnfoldCMS posts/pages with custom fields
- Import to UnfoldCMS via the REST API (single bulk endpoint, accepts the transformed JSON)
- Migrate uploads — copy files from Strapi's
public/uploadsdirectory to your UnfoldCMS media store - Update your frontend — replace Strapi REST/GraphQL calls with UnfoldCMS endpoints
For a site with 200 entries and 500 uploads, total work is 4–8 hours including testing.
Step-by-Step: Strapi to UnfoldCMS Migration
Step 1: Verify your Strapi version and prep the export
The official strapi export command ships with Strapi v4 and v5. If you're on v3, upgrade to v4 first or export via the database directly — v3's export tooling is fragile.
cd your-strapi-project
yarn strapi version # confirm v4.x or v5.x
yarn strapi export --no-encrypt --file strapi-export
This produces strapi-export.tar.gz containing:
entities/— JSON files for every Content Type (one file per type)assets/— every uploaded file with original filenames preservedlinks/— relational data (entry-to-entry references)configuration/— Content Type definitions, components, roles
The --no-encrypt flag keeps the archive readable. For production sites, omit it and supply a key — but for migration purposes, plain JSON is easier to work with.
Extract the archive:
mkdir strapi-export && tar -xzf strapi-export.tar.gz -C strapi-export
cd strapi-export && ls
# entities assets links configuration
Step 2: Inspect your Content Types
Strapi's Content Types map roughly to UnfoldCMS like this:
| Strapi concept | UnfoldCMS equivalent |
|---|---|
Collection Type (e.g. api::article.article) |
Post or Page (depending on use) |
Single Type (e.g. api::about.about) |
Page (single instance) or Setting |
| Component (reusable field group) | JSON in extra_attributes |
| Dynamic Zone (mixed component array) | Section Placements or HTML body |
| Media field | Spatie Media Library collection |
| Relation (one-to-many, many-to-many) | Categories, tags, or custom relation |
Open entities/api::article.article.jsonl (or your equivalent) and confirm the field names you need to preserve. Strapi stores one JSON object per line:
{
"id": 1,
"title": "My Article",
"slug": "my-article",
"content": "<p>Body content as HTML</p>",
"publishedAt": "2025-04-01T10:00:00.000Z",
"createdAt": "2025-03-28T10:00:00.000Z",
"seo": {
"metaTitle": "My Article — SEO Title",
"metaDescription": "Description for search engines"
}
}
Step 3: Transform the JSON to UnfoldCMS format
UnfoldCMS expects flat post data:
{
"title": "My Article",
"slug": "my-article",
"body": "<p>Body content as HTML</p>",
"seo_title": "My Article — SEO Title",
"meta_desc": "Description for search engines",
"posted_at": "2025-04-01T10:00:00Z",
"is_published": true,
"extra_attributes": { "strapi_id": 1, "strapi_type": "api::article.article" }
}
A 60-line Node script handles the transform. Reference implementation:
// transform.js
import fs from 'fs';
import readline from 'readline';
const posts = [];
const rl = readline.createInterface({
input: fs.createReadStream('strapi-export/entities/api::article.article.jsonl'),
});
for await (const line of rl) {
if (!line.trim()) continue;
const entry = JSON.parse(line);
posts.push({
title: entry.title ?? 'Untitled',
slug: entry.slug ?? entry.title?.toLowerCase().replace(/\s+/g, '-'),
body: entry.content ?? '',
seo_title: entry.seo?.metaTitle ?? null,
meta_desc: entry.seo?.metaDescription ?? null,
posted_at: entry.publishedAt ?? entry.createdAt,
content_type: 'post',
is_published: !!entry.publishedAt,
extra_attributes: {
strapi_id: entry.id,
strapi_type: 'api::article.article',
},
});
}
fs.writeFileSync('unfold-import.json', JSON.stringify(posts, null, 2));
console.log(`Transformed ${posts.length} entries`);
If you use Strapi Components or Dynamic Zones, flatten them into the body HTML or store the structured data in extra_attributes for later use.
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 returns a result summary:
{
"imported": 487,
"skipped": 13,
"errors": [
{ "index": 42, "reason": "duplicate slug" }
]
}
For 500 entries, the import takes 30–60 seconds.
Step 5: Migrate uploads
Strapi stores uploads in public/uploads/ (default local provider) or in S3/Cloudinary if configured. Either way, the export archive contains every asset under assets/.
Under 1,000 uploads — script the upload to UnfoldCMS:
// upload-assets.js
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
import FormData from 'form-data';
const assetsDir = 'strapi-export/assets';
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 uploads — keep the existing storage in place (S3, Cloudinary) and rewrite URLs in your post bodies to point at the same CDN. Strapi's S3 provider isn't tied to Strapi's runtime — your bucket keeps serving files regardless of whether Strapi is running.
Step 6: Set up redirects
If your URL structure is changing (it usually is — Strapi sites often expose /api/articles/:id while UnfoldCMS uses /blog/:slug), set up 301 redirects.
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
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 Strapi REST or GraphQL calls with UnfoldCMS fetches. Before:
const res = await fetch(`${process.env.STRAPI_URL}/api/articles?populate=*`, {
headers: { Authorization: `Bearer ${process.env.STRAPI_TOKEN}` },
});
const { data } = await res.json();
const posts = data.map((entry) => ({
...entry.attributes,
id: entry.id,
}));
After:
const res = await fetch(`${process.env.UNFOLD_API_URL}/api/blog/posts`, {
next: { revalidate: 60 },
});
const { data: posts } = await res.json();
UnfoldCMS returns flat post objects — no attributes wrapper, no populate parameter, no nested data shape to flatten on every request.
What Breaks (and How to Handle It)
Real migrations always hit edge cases. Here are the common ones for Strapi.
Components and Dynamic Zones. Components are reusable field groups (a hero block, a CTA pair, a feature list). Dynamic Zones are arrays of mixed component types. Neither has a direct UnfoldCMS equivalent — flatten them into HTML at migration time, or store the structured component data in extra_attributes and render it client-side.
Relations between Content Types. Strapi relations (one-to-one, one-to-many, many-to-many) export as separate JSON in the links/ directory. Resolve them in a second pass — match strapi_id in extra_attributes to the new UnfoldCMS post IDs and update the body or set up category/tag joins.
Roles and Permissions. Strapi's role-based access control exports under configuration/, but it doesn't transfer directly — UnfoldCMS uses its own role system (Spatie Permissions). Map Strapi roles to UnfoldCMS roles manually after import.
Custom plugins and field types. Plugins like draft-and-publish, internationalization (i18n), and custom field types (rich text editors, code blocks, color pickers) export their data into the entity JSON, but the field definitions don't transfer. UnfoldCMS has built-in drafts, scheduled publishing, and SEO records. For i18n content, run one migration per locale.
Lifecycle hooks and policies. Strapi beforeCreate, afterUpdate, and route policies are JavaScript code tied to Strapi's runtime — they don't transfer. Rebuild them in UnfoldCMS using Laravel events, observers, and middleware. Most lifecycle hooks become trivial Laravel observers.
GraphQL schema customization. If your Strapi GraphQL schema is heavily customized (extended types, custom resolvers), the queries don't translate. UnfoldCMS exposes REST + GraphQL with a different schema shape. Plan a frontend rewrite of every query.
Timeline: How Long Does Strapi Migration Take?
For typical content sites, the breakdown:
| Site size | Entries | Uploads | 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 entries, custom quote above that. We handle the export, transform, import, asset migration, redirects, and frontend updates.
When NOT to Migrate Off Strapi
Strapi is genuinely strong for some teams. Honest answer:
Stay on Strapi if:
- Your stack is fully JavaScript and adding PHP/Laravel adds operational friction your team won't absorb
- You depend on Strapi-specific plugins (i18n, custom field types, marketplace integrations) that your team has invested heavily in
- Your team uses Strapi's RBAC granularity (per-field permissions, conditional roles) at production scale
- You're already on Strapi Enterprise with paid support and the contract covers your operational budget
- You need GraphQL with deeply customized resolvers and the frontend is built around that schema shape
- Your team is comfortable with the upgrade cycle and has bandwidth to absorb major-version migrations every 18–24 months
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:
| Strapi (Cloud Pro) | UnfoldCMS | |
|---|---|---|
| Year 1 cost | $1,188 ($99 × 12) | $39–$799 (one-time) + $5/mo hosting |
| Per-project fees | Effectively yes (per Cloud project) | No |
| Per-seat fees | Yes (Team tier) | No (unlimited) |
| Major version churn | Every 18–24 months | None — buy once, update at your pace |
| Plugin ecosystem | Large but inconsistent quality | Built-in features (no plugin economy) |
| Hosting model | Node + Postgres + Redis | PHP shared hosting works |
| Build pipeline required | Yes (Node, env vars, Docker common) | No (composer install + git pull) |
| Real-time collaborative editing | No | No |
| Built-in SEO records | Plugin | Yes |
| Built-in redirects table | Plugin | Yes |
| Built-in comments | Plugin | Yes (optional) |
| Built-in forms | Plugin | Yes |
| Includes themed frontend | No (headless only) | Yes |
| Source code access | Yes (MIT) | Yes (commercial) |
| Self-hosted option | Yes | Yes |
Trade-offs are real — Strapi's MIT license and plugin ecosystem are advantages for teams who can absorb the upgrade churn. But for the 80% of teams who don't need plugins beyond the standard set, UnfoldCMS replaces Strapi for ~95% less recurring cost and zero major-version migrations.
FAQ
How much does it cost to migrate from Strapi? 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 entries + media.
Will my SEO survive a Strapi 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 Strapi 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. Strapi stays as your safety net.
How do I migrate Strapi Components and Dynamic Zones?
Components are flattened into HTML at migration time, or stored in extra_attributes for client-side rendering. Dynamic Zones map naturally to UnfoldCMS's Section Placement system if you want the same modular page-builder behavior — otherwise, render the components as HTML blocks in the post body.
What about the Strapi i18n plugin and locale content?
Strapi's i18n stores localized entries as separate documents linked by a localizations field. Run one migration pass per locale, importing each as a separate UnfoldCMS post (with the locale stored in extra_attributes or as a category). UnfoldCMS doesn't ship multi-locale routing out of the box — for sites that need it, plan a custom routing layer or pick a different CMS.
Will Strapi Cloud charge me during the migration? Yes — Strapi Cloud bills monthly until you delete the project or downgrade to the free tier (where available). After cutover, downgrade and verify your frontend doesn't need it for 30 days, then delete the project.
How do I migrate Strapi roles and permissions?
Roles export under configuration/ but don't transfer directly. UnfoldCMS uses Spatie Permissions — define equivalent roles in the UnfoldCMS admin and assign users post-import. For most sites, two or three roles cover the actual use cases.
Is the Strapi REST API still available during migration? Yes. Strapi keeps the API running until you stop the Node process. Run the migration with both APIs available and switch your frontend's environment variable to cut over.
Do I need to upgrade to Strapi v5 before migrating? No. The export tooling works on v4 and v5, and the JSON formats are similar enough that the same transform script handles both with minor field-name adjustments. If you're on v3, upgrade to v4 first — v3's export tooling is fragile.
Methodology
Pricing data is from strapi.io/pricing 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 Strapi's official strapi export CLI documentation and UnfoldCMS API docs. Edge cases (Components, Dynamic Zones, Relations, i18n) are sourced from real migration tickets in our support queue and from public Strapi v4-to-v5 upgrade discussions on r/strapi and the Strapi forum.
Migrate from Strapi 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 Strapi project, identify migration risks, and write you a tailored migration plan. You execute it.
- Migration Concierge ($499) — done for you. We export, transform, import, migrate uploads, set up redirects, and update your frontend. 7 days, up to 500 entries. 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 Strapi dataset.
For more context, see UnfoldCMS vs Strapi, Migrate from Contentful, Migrate from Sanity, or the Best CMS for Next.js breakdown.
If after reading this you decide Strapi 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 WordPress, CMS for SaaS marketing sites, CMS for agencies, or browse all migration guides.