Originally published on the Cosmic blog.
Since Salesforce completed its acquisition of Contentful, teams across the industry have been re-evaluating their CMS stack. Pricing changes, roadmap uncertainty, and enterprise-first repositioning are pushing developers and content teams to look for a more focused alternative. If you've already decided to move on, this guide covers the practical how-to. For the "why," see our posts on Contentful alternatives and what the Salesforce acquisition means for your team.
This walkthrough takes roughly 30 minutes for a typical project. Larger spaces with thousands of entries or complex localization setups may take longer, but the steps are the same.
What You'll Need
- Node.js 18+ installed
- A Contentful account with space access and a Management API token
- A Cosmic account (free plan works — sign up here, no credit card required)
- The
@cosmicjs/sdkpackage - Basic familiarity with the command line
Step 1: Export Your Content from Contentful
Contentful provides a first-party CLI that handles the full export to JSON. Install it globally:
npm install -g contentful-cli
Authenticate with your Management API token:
contentful login
Then run the export:
contentful space export \
--space-id YOUR_SPACE_ID \
--management-token YOUR_MANAGEMENT_TOKEN \
--include-drafts \
--download-assets \
--content-file contentful-export.json
This produces a single contentful-export.json file containing your contentTypes, entries, assets, and locales. The --download-assets flag pulls the actual media files to your local machine alongside the JSON. You'll need them in Step 4.
What the export file looks like:
{
"contentTypes": [],
"entries": [],
"assets": [],
"locales": []
}
Keep this file. Every subsequent step reads from it.
Step 2: Map Contentful Content Types to Cosmic Object Types
This is the most important step and the one that takes the most thought. The concepts map closely but are not identical.
| Contentful | Cosmic |
|---|---|
| Space | Bucket |
| Content Type | Object Type |
| Field | Metafield |
| Entry | Object |
| Asset | Media (imgix CDN) |
| Environment | Bucket (separate) |
Field type mapping reference:
| Contentful Field Type | Cosmic Metafield Type |
|---|---|
| Symbol (short text) | text |
| Text (long text) | textarea |
| RichText |
rich-text or markdown
|
| Integer / Number | number |
| Boolean | switch |
| Date | date |
| Link (Asset) | file |
| Link (Entry) | object |
| Array of Links (Entries) | objects |
| Array of Symbols | multi-select |
| JSON | json |
| Color | color |
Cosmic supports over 20 metafield types in total. A key difference worth noting: Cosmic requires no schema migrations. You define Object Types and their metafields once in the dashboard or via the SDK, and you can modify them at any time without downtime or a migration script.
Step 3: Create Your Object Types in Cosmic
You can create Object Types in the Cosmic dashboard under Bucket Settings > Object Types, or programmatically using the @cosmicjs/sdk:
import { createBucketClient } from '@cosmicjs/sdk';
import fs from 'fs';
const cosmic = createBucketClient({
bucketSlug: 'YOUR_BUCKET_SLUG',
readKey: 'YOUR_READ_KEY',
writeKey: 'YOUR_WRITE_KEY',
});
const exportData = JSON.parse(fs.readFileSync('./contentful-export.json', 'utf-8'));
function mapFieldType(contentfulType: string, linkType?: string): string {
const typeMap: Record<string, string> = {
Symbol: 'text', Text: 'textarea', RichText: 'rich-text',
Integer: 'number', Number: 'number', Boolean: 'switch',
Date: 'date', Object: 'json',
};
if (contentfulType === 'Link') return linkType === 'Asset' ? 'file' : 'object';
if (contentfulType === 'Array') return linkType === 'Entry' ? 'objects' : 'multi-select';
return typeMap[contentfulType] ?? 'text';
}
for (const ct of exportData.contentTypes) {
const metafields = ct.fields.map((field: any) => ({
key: field.id,
title: field.name,
type: mapFieldType(field.type, field.linkType ?? field.items?.linkType),
required: field.required ?? false,
}));
await cosmic.objectTypes.insertOne({
title: ct.name,
slug: ct.sys.id.toLowerCase().replace(/_/g, '-'),
metafields,
});
}
Step 4: Import Your Entries via the TypeScript SDK
import { createBucketClient } from '@cosmicjs/sdk';
import fs from 'fs';
const cosmic = createBucketClient({
bucketSlug: 'YOUR_BUCKET_SLUG',
readKey: 'YOUR_READ_KEY',
writeKey: 'YOUR_WRITE_KEY',
});
const exportData = JSON.parse(fs.readFileSync('./contentful-export.json', 'utf-8'));
const locale = exportData.locales.find((l: any) => l.default)?.code ?? 'en-US';
for (const entry of exportData.entries) {
const contentTypeId = entry.sys.contentType.sys.id;
const fields = entry.fields;
const title = fields.title?.[locale] ?? fields.name?.[locale] ?? entry.sys.id;
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
const metadata: Record<string, any> = {};
for (const [key, value] of Object.entries(fields)) {
const fieldValue = (value as any)[locale];
if (fieldValue !== undefined) {
metadata[key] = fieldValue?.sys?.type === 'Link' ? fieldValue.sys.id : fieldValue;
}
}
await cosmic.objects.insertOne({
title, slug,
type: contentTypeId.toLowerCase().replace(/_/g, '-'),
status: entry.sys.publishedAt ? 'published' : 'draft',
metadata,
});
}
For RichText fields, convert Contentful's nested JSON to HTML or Markdown first using @contentful/rich-text-html-renderer.
Step 5: Migrate Assets to the imgix CDN
Cosmic serves all media through imgix, so every asset gets automatic image optimization, resizing, and format conversion with zero configuration.
for (const asset of exportData.assets) {
const file = asset.fields.file?.[locale];
if (!file?.url) continue;
const response = await fetch(`https:${file.url}`);
const buffer = Buffer.from(await response.arrayBuffer());
await cosmic.media.insertOne({
media: { originalname: file.fileName ?? asset.sys.id, buffer },
});
}
Once assets are in Cosmic, you get URL-based transformations for free:
https://imgix.cosmicjs.com/your-image.jpg?w=800&fm=webp&q=80
Step 6: Set Up URL Redirects
Next.js (next.config.js):
module.exports = {
async redirects() {
return [{ source: '/blog/:slug', destination: '/articles/:slug', permanent: true }];
},
};
If you maintained the same slug structure in your import (recommended), you may need zero redirects at all.
Step 7: Validate with the Cosmic SDK
const objectTypes = ['blog-post', 'author', 'category'];
for (const type of objectTypes) {
const { total } = await cosmic.objects.find({ type }).props('id,title,slug').limit(1);
console.log(`${type}: ${total} objects in Cosmic`);
}
Cross-reference the object counts against your Contentful export. If they match, update your frontend's environment variables and go live.
Realistic Time Estimate
- Install CLI + export from Contentful: 5 minutes
- Review export, map content types: 5-10 minutes
- Create Object Types via SDK: 5 minutes
- Import entries via SDK script: 5-10 minutes
- Upload assets via SDK: 3-5 minutes
- Set up redirects: 2-5 minutes
- Validate with SDK: 5 minutes
- Total: ~25-40 minutes
Let Cosmic AI Agents Help
If you'd rather not write the migration scripts by hand, Cosmic AI Agents can help. From inside your Cosmic dashboard, you can prompt an agent to inspect your export file, generate a schema mapping, write the import scripts, and validate the results, all from a natural language interface.
You're Live on Cosmic
Update your frontend's environment variables, then redeploy. Your content is now served from Cosmic's global CDN, with assets on imgix.
Pricing starts at $0/month (Free plan: 1 Bucket, 2 team members, 1,000 Objects). Paid plans start at $49/month (Builder) and scale to $499/month (Business, 50,000 Objects, 10 team members). Additional users are $29/user/month on any paid plan.
Next Steps
- Start for free on Cosmic — no credit card required
- Book a 30-minute migration walkthrough with Tony
- Browse the Cosmic documentation
Top comments (0)