Sanity migration: Learn how to migrate from Sanity to headless BCMS

How to Migrate from Sanity to BCMS thumbnails.jpeg
By James Oluwaleye
Read time 7 min
Posted on 25 Mar 2025

Content migrations from Sanity to BCMS might feel overwhelming at first, but if you break it down into easy steps and understand each part, it becomes a lot simpler. In this article, I’ll guide you through the whole process of Sanity migration using a basic blog as an example. Instead of giving you a strict list, I want to make it a smooth and interesting read while covering everything from planning before the move to taking care of things after the move.

Imagine you’ve created a blog on Sanity where all your posts, authors, categories, and media files are neatly organized. Now, you want to switch to BCMS because it has a cleaner API, better performance, or just because you're curious about trying something new. For more detailed info on BCMS and its capabilities, check out the official BCMS documentation.

This article is here to help you transfer your important content from Sanity to BCMS without losing any of its setup or quality. I’ll go over each part of the content migration process, share useful code examples, and explain why I did each step in clear language.

Sanity migration steps: Pre-migration planning

Before start exporting any data, I will plan out the whole process of moving our information. My main goal is to transfer everything from Sanity CMS to BCMS without any problems. I begin by looking over our current setup and making a backup of Sanity data to ensure we don’t lose anything. I also create a timeline that breaks the project into different stages. Here’s what I will concentrate on:

- Moving blog posts, author information, and media files to a new system without losing any data.

- Set specific deadlines for checking, exporting, changing, and combining the data.

- Make sure I have backups and that the team is prepared for each step of the process.

Auditing existing content

Next, you'll log into your Sanity dashboard and carefully check the content. Look at each blog post, category, and media file. While doing this, figure out what’s important and what I can get rid of. Make sure that your posts are current, categories are logical, and cover images are good quality. Aim to transfer only what really matters, making the process easier and keeping things organized in the new system. By regularly reviewing your content, you are prepared for a smooth transition.

Sanity studio

Exporting data from Sanity

Now that you’ve checked your content, it’s time to get your data from Sanity. If you want to export certain content, like just your blog posts, you can make a custom Node.js script using the Sanity client. First, make sure to install the Sanity CMS client if you haven't done that yet:

npm install @sanity/client

Next, create a file called export.js with this code:

const { createClient } = require('@sanity/client');
const fs = require('fs');
const client = createClient({
  projectId: 'your_project_id', // Replace with your actual project ID
  dataset: 'production',        // Or your specific dataset
  useCdn: false,
});
client.fetch('*[_type == "post"]').then(posts => {
  fs.writeFileSync('sanity-posts.json', JSON.stringify(posts, null, 2));
  console.log('Export complete: sanity-posts.json created');
}).catch(err => {
  console.error('Export failed:', err);
});

Run the script with:

node export.js

This custom script connects to your Sanity project, fetches all documents of type "post", and writes them into a JSON file named sanity-posts.json. If you don't want to use this method, then you can check out Sanity documentation for other methods.

Setting Up the BCMS Environment

Now, we’re going to change focus and set up our BCMS environment. First, we go to the BCMS Dashboard and log in. After that, we’ll create a new project specifically for our blog.

BCMS dashboard

In your BCMS project, make a template called Blog and another called Author. The blog template should look like the dashboard below, drag the entry pointer from the menu in the right to the center and select the Author as the entry. In your Author templates, you can add media and rich text for the bio.

blog template

Then, go to your BCMS dashboard and click on Administration, then Settings, and find API Keys. Create a new API key and keep the page open for the next steps. Make sure this key can access entries from your Blog template.

API Key

Setting up your BCMS environment is like building a strong base for a new building. It makes sure that when you connect your Next.js project to BCMS later, you have everything you need: a project with a clear template, and an API key that’s ready to be used. Now that your BCMS environment is set up, we can move on to matching our content models between Sanity and BCMS.

Mapping out content models for the successful Sanity migration

Now that your new BCMS project is ready, you need to make a detailed plan for how your Sanity content will fit into BCMS. This mapping will help you make sure that every piece of information from your blog has a specific place in the new system.

Review your Sanity schemas

Start by reviewing the content models you use in Sanity. For example, your blog post schema might look like this:

export const postType = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    { name: 'title', type: 'string' },
    { name: 'slug', type: 'slug', options: { source: 'title' } },
    { name: 'author', type: 'reference', to: { type: 'author' } },
    { name: 'mainImage', type: 'image', options: { hotspot: true } },
    { name: 'categories', type: 'array', of: [{ type: 'reference', to: { type: 'category' } }] },
    { name: 'publishedAt', type: 'datetime' },
    { name: 'body', type: 'blockContent' }
  ]
});

Similarly, your author and category schemas define the structure for those content types. Take note of every field and its data type. In BCMS, you don't have a specific section for "content models" like you do in Sanity. Instead, you create templates that do the same job: they outline how your content should be organized.

Define templates in BCMS

In BCMS, you will create templates that reflect the structure of your Sanity content. For your blog, you will need three main templates:

  1. Blog Post Template: This includes sections for the title, slug, rich text content, cover image, published date, author name, and categories.

  2. Author Template: This has sections for the author’s name, slug, image, and a short biography.

  3. Category Template: This includes fields for the category title, slug, and a description.

Even though BCMS doesn't have a specific “content models” section, it's important to keep a record of your mappings (like in a spreadsheet) to show how each Sanity model and its fields connect to BCMS. Finally, you should create these templates in your BCMS project using the dashboard and add some sample data to make sure everything works correctly. Do the same for Author and Category templates.

5. Define Templates in BCMS.png

Transforming data formats

I exported Sanity data using a special script that created a standard JSON file named sanity-posts.json. This file holds a list of documents from your Sanity project. Your next step is to change the blog posts into a format that BCMS needs. I believe that blog posts have a field called _type that is set to "post." For each blog post, we need to adjust its structure. For instance, a usual Sanity post might look like this:

{
  "_id": "post-123",
  "title": "Migrating to BCMS",
  "body": "Here's how to migrate from Sanity...",
  "author": { "_ref": "author-456" }
}

You want to convert it to the BCMS format:

{
  "title": "Migrating to BCMS",
  "content": "Here's how to migrate from Sanity...",
  "author": "author-456"
}

Below is a complete Node.js script that does exactly that. It reads sanity-export.json line by line, parses each JSON object, filters for documents where _type is "post", transforms the structure, and finally writes all the transformed posts to bcms-posts.json. Save the code to a file, for example, transformPosts.js

const fs = require('fs');
function transformData() {
  // Read the exported JSON file that contains an array of documents.
  const rawData = fs.readFileSync('sanity-posts.json', 'utf8');
  let documents = [];
  try {
    documents = JSON.parse(rawData);
  } catch (err) {
    console.error('Error parsing sanity-posts.json:', err);
    return;
  }
  // Filter for blog posts and transform the structure.
  const transformedPosts = documents
    .filter(doc => doc._type === 'post')
    .map(post => ({
      title: post.title,
      content: post.body,
      author: post.author && post.author._ref ? post.author._ref : null,
    }));
  // Write the transformed data to a new JSON file.
  fs.writeFileSync('bcms-posts.json', JSON.stringify(transformedPosts, null, 2));
  console.log('Data transformed and saved as bcms-posts.json');
}
transformData();

Open your terminal and run the script with Node.js:

node transformPosts.js

The script takes your sanity-posts.json file removes the blog posts, changes how the information is organized, and then saves the new data in a file called bcms-posts.json. This new file is ready to be imported into BCMS.

Import data into BCMS

First, we need to bring the changed data into BCMS. Here’s the code that worked for me. It reads the JSON file called bcms-posts.json and creates an entry for each post in BCMS. Each post is handled one at a time, creating the content sections (like bullet lists, headings, and paragraphs) in the way BCMS requires.

const { Client } = require('@thebcms/client');
const fs = require('fs');
const path = require('path');
 // replace the below with your actual IDs
const client = new Client(
  ORG_ID,
  INSTANCE_ID,
  { id: API_KEY_ID, secret: API_KEY_SECRET },
  { injectSvg: true }
);
async function importPosts() {
  try {
    const rawPosts = fs.readFileSync(path.resolve('./bcms-posts.json'), 'utf8');
    const posts = JSON.parse(rawPosts);
    console.log('Found', posts.length, 'posts to process');
    for (const [index, post] of posts.entries()) {
      try {
        console.log(`\nProcessing post ${index + 1}/${posts.length}: ${post.title}`);
        if (!post?.title) throw new Error('Missing title');
        if (!post?.content) throw new Error('Missing content');
        if (!post?.slug) throw new Error('Missing slug');
        const nodes = [];
        // Add a heading node for the post title.
        nodes.push({
          type: 'heading',
          attrs: { level: 1 },
          content: [{ type: 'text', text: post.title }]
        });
        // Process each block in post.content.
        post.content.forEach((block, blockIndex) => {
          let node = null;
          // If block is a bullet list, use the exact structure.
          if ((block._type === 'bulletList' || block.type === 'bulletList') && Array.isArray(block.content)) {
            // Map each list item.
            const listItems = block.content.map((item) => {
              // Assume each list item has children with text.
              const itemText = item.children && Array.isArray(item.children)
                ? item.children.map(child => child.text || '').join(' ').trim()
                : (item.text ? item.text.trim() : '');
              return {
                type: 'listItem',
                attrs: { list: true },
                content: [
                  {
                    type: 'paragraph',
                    content: [{ type: 'text', text: itemText }]
                  }
                ]
              };
            });
            node = {
              type: 'bulletList',
              content: listItems
            };
          }
          // Else if it's a standard block with children.
          else if (block._type === 'block' && Array.isArray(block.children)) {
            const text = block.children
              .map(child => (child.text ? child.text.trim() : ''))
              .filter(Boolean)
              .join(' ');
            let nodeType = 'paragraph';
            let attrs = {};
            if (block.style && typeof block.style === 'string' && block.style.startsWith('h')) {
              nodeType = 'heading';
              attrs = { level: parseInt(block.style.substring(1)) };
            }
            if (block.listItem) {
              nodeType = 'listItem';
              attrs = {
                listType: block.listItem === 'bullet' ? 'bullet' : 'ordered',
                level: block.level || 1
              };
            }
            node = {
              type: nodeType,
              ...(Object.keys(attrs).length > 0 ? { attrs } : {}),
              content: [{ type: 'text', text }]
            };
          }
          // Fallback if block has a direct text property.
          else if (block.text) {
            node = {
              type: 'paragraph',
              content: [{ type: 'text', text: block.text.trim() }]
            };
          }
          if (node) {
            nodes.push(node);
            console.log(`Processed block ${blockIndex} for post "${post.title}"`);
          }
        });
        // Build plainText by concatenating text from all nodes.
        const plainText = nodes
          .map(node => node.content.map(c => c.text).join(' '))
          .join(' ')
          .trim();
        // Build the payload with meta and content.
        const payload = {
          meta: [{
            lng: 'en',
            data: {
              title: post.title,
              slug: post.slug.current || post.slug
            }
          }],
          content: [{
            lng: 'en',
            nodes,
            plainText
          }],
          statuses: []
        };
        console.log('Payload for post:', JSON.stringify(payload, null, 2));
        const response = await client.entry.create(TEMPLATE_ID, payload);
        if (response && response._id) {
          console.log(`✅ Successfully created entry for "${post.title}": ${response._id}`);
        } else {
          console.error(`⚠️ No entry returned for "${post.title}"`, response);
        }
      } catch (error) {
        console.error(`🚨 Error processing post "${post.title || 'Unknown'}":`, error.response?.data || error.message);
      }
    }
    console.log('\nAll posts processed');
  } catch (error) {
    console.error('Fatal error:', error.message);
  }
}
importPosts();

Then you run the command:

node import.js

The script opens the bcms-posts.json file and goes through each post one by one. It changes the content into the format that BCMS needs, which includes organizing bullet lists. After that, it creates the entries using the BCMS API. Now, you can do the same process for your author and category or do it by manually in the dashboard. Your content will now be displaced on the BCMS dashboard.

6. Import Data into BCMS.png

Validate and test migration

After you import the data into BCMS, the next step is to check and test the migration. This means looking at the entries in the BCMS dashboard to make sure that every blog post is there and that the content—like headings, paragraphs, bullet points, and other styles—looks right. Take some time to review a few posts to confirm that all the important fields, such as the title and slug, are set up correctly.

migration test

Also, test the API responses by getting data from BCMS using your application or a tool like Postman. This will help ensure that the data format matches what your front-end needs. Make sure that the plainText field and content nodes are returned in the right format so your application shows the posts as expected. Lastly, do some cross-browser testing to find any display problems on different devices, and keep track of any issues for future improvements.

Update application integrations

After checking that the migration worked and the data is correctly imported into BCMS, the next step is to update your application to use the BCMS APIs instead of the ones from Sanity. This means you'll need to change your code so that it gets data from BCMS and shows it using BCMS components. You can check BCMS Documentation for the well-explained process of setting up your project.

For example, take a look at this React component that gets all the blog entries from BCMS. It then shows each blog post along with its cover image and content by using the BCMSContentManager component:

import { bcms } from "@/bcms"; // Import the BCMS client configuration
import { BlogEntry } from "../../bcms/types/ts"; // Import the TypeScript type for blog entries
import { BCMSContentManager, BCMSImage } from "@thebcms/components-react"; // Import BCMS components for displaying content and images
// The Home component fetches blog entries asynchronously and renders them
export default async function Home() {
  // Fetch all blog entries for the "blog" template from BCMS
  const blogs = (await bcms.entry.getAll("blog")) as BlogEntry[];
  return (
    <main>
      {/* Container for all blog posts with spacing */}
      <div className="flex flex-col gap-4">
        {blogs.map((blog, blogIdx) => {
          // Check if meta and content exist for the 'en' locale before rendering
          if (!blog.meta.en || !blog.content.en) {
            return "";
          }
          return (
            // Render each blog post in its own container
            <div key={blogIdx} className="flex flex-col gap-8">
              {/* Render the blog title as an h1 element */}
              <h1 className="mb-10 text-3xl">{blog.meta.en.title}</h1>
              {/* Render the cover image using BCMSImage */}
              <BCMSImage
                className="w-full h-60 object-cover"
                media={blog.meta.en.cover_image} // Cover image data from BCMS meta
                clientConfig={bcms.getConfig()} // Pass the BCMS configuration to the image component
              />
              {/* Render the blog content using BCMSContentManager which handles various content nodes */}
              <BCMSContentManager items={blog.content.en} />
            </div>
          );
        })}
      </div>
    </main>
  );
}

SEO Considerations

Making sure your website is friendly for SEO is an important part of moving it to a new platform. Here are some important steps to remember:

  • Redirects and URL structure: Use short, keyword-rich slugs that clearly explain what your content is about, so users can easily understand each page. Also, make sure to set up a permanent 301 redirect from your Sanity URL to BCMS. This helps both users and search engines find the new URL from the old one, which helps keep your SEO value intact.

  • Metadata preservation: After moving your blog posts to BCMS, it's important to change or add your site's metadata directly in the code. This means you should improve title tags and meta descriptions to boost your SEO and make sure you have Open Graph and social media tags set up for easy sharing. Regularly checking your site with SEO tools helps ensure that each page has the right metadata, which is essential for keeping your site visible and performing well in search results.

  • Sitemap & robots.txt: Make sure to update your XML sitemap to include any new URLs and send it to search engines so they can re-index your site faster. Also, check your robots.txt file to make sure important parts of your site aren't blocked.

  • Performance & mobile optimization: Make sure your new setup works well and is optimized for mobile devices. Quick loading times and a design that adjusts to different screen sizes are really important for search engine optimization (SEO). You can use tools like Google PageSpeed Insights to find and fix any performance problems.

Conclusion: You don't need Sanity migration examples, you need BCMS

After you finish moving your data and updating your application, it's really important to make sure everything keeps running smoothly. This means you need to keep a close watch on everything. Regularly check your API responses and server logs to spot any errors or performance problems early. Also, pay attention to what users are saying. If they report missing content or display issues, you might need to make some changes to your setup or how the data is shown. It’s normal to need to adjust a few things as you see how people are using it in real life.

Lastly, make sure your URLs are clean and easy to understand, using keywords in them, and set up 301 redirects for any URL changes to keep your search rankings high. Regularly review your integration and update your settings or migration scripts to help keep your application's performance and SEO strong over time.

It takes a minute to start using BCMS

Gradient

Join our Newsletter

Get all the latest BCMS updates, news and events.

You’re in!

The first mail will be in your inbox next Monday!
Until then, let’s connect on Discord as well:

Join BCMS community on Discord

By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.

Gradient