DEV Community

Cover image for How to Migrate from Contentful to Cosmic in 30 Minutes
Tony Spiro
Tony Spiro

Posted on • Originally published at cosmicjs.com

How to Migrate from Contentful to Cosmic in 30 Minutes

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/sdk package
  • 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
Enter fullscreen mode Exit fullscreen mode

Authenticate with your Management API token:

contentful login
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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": []
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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 },
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 6: Set Up URL Redirects

Next.js (next.config.js):

module.exports = {
  async redirects() {
    return [{ source: '/blog/:slug', destination: '/articles/:slug', permanent: true }];
  },
};
Enter fullscreen mode Exit fullscreen mode

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`);
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)