👋 Next.js Conf 2024: Come build, party, run, and connect with us! See all events

Content migration cheat sheet

Common content migration patterns that can be run by the Sanity CLI

Below are content migration code snippets you can copy-paste and fit for your purposes. Requires familiarity with Sanity's schema and content migration tooling.

Rename a field in a document

import {defineMigration, at, setIfMissing, unset} from 'sanity/migrate'

export default defineMigration({
  title: 'Rename field from "oldFieldName" to "newFieldName"',
  migrate: {
    document(doc, context) {
      return [
        at('newFieldName', setIfMissing(doc.oldFieldName)),
        at('oldFieldName', unset())
      ]
    }
  }
})

Add a field with default value to all documents missing the field

Note: This example uses an async generator pattern (*migrate) to read out the document ID (_id) one by one and return the patch. This prevents the script from loading all documents into memory.

import {defineMigration, patch, at, setIfMissing} from 'sanity/migrate'

export default defineMigration({
  title: 'Add title field with default value',
  // documentTypes: ['post', 'article'], // only apply to certain document types
  async *migrate(documents, context) {
    for await (const document of documents()) {
      yield patch(document._id, [
        at('title', setIfMissing('Default title')),
      ])
    }
  }
})

Migrate a reference field into an array of references

import { defineMigration, at, setIfMissing, append, unset} from 'sanity/migrate'

export default defineMigration({
  title: 'Convert a reference field into an array of references',
	documentTypes: ['product'],
	filter: 'defined(category) && !defined(categories)',
  migrate: {
    document(product) {
      return [
        at('categories', setIfMissing([])),
        // use `prepend()` to insert at the start of the category array
        at('categories', append(product.category)),
        at('category', unset())
      ]
    }
  }
})

A string field into a Portable Text array

import {pathsAreEqual, stringToPath} from 'sanity'
import {defineMigration, set} from 'sanity/migrate'

const targetPath = stringToPath('some.path')

export default defineMigration({
  title: 'Convert a string into a Portable Text array',

  migrate: {
    string(node, path, ctx) {
      if (pathsAreEqual(path, targetPath)) {
        return set([
          {
            style: 'normal',
            _type: 'block',
            children: [
              {
                _type: 'span',
                marks: [],
                text: node,
              },
            ],
            markDefs: [],
          },
        ])
      }
    },
  },
})

A Portable Text field into plain text

import {pathsAreEqual, stringToPath, type PortableTextBlock} from 'sanity'
import {defineMigration, set} from 'sanity/migrate'

// if the portable text field is nested, specify the full path to it
const targetPath = stringToPath('some.path')

function toPlainText(blocks: PortableTextBlock[]) {
  return (
    blocks
      // loop through each block
      .map((block) => {
        // if it's not a text block with children,
        // return nothing
        if (block._type !== 'block' || !block.children) {
          return ''
        }
        // loop through the children spans, and join the
        // text strings
        return (block.children as {text: string}[]).map((child) => child.text).join('')
      })
      // join the paragraphs leaving split by two linebreaks
      .join('\n\n')
  )
}
export default defineMigration({
  title: 'A Portable Text field into plain text (only supporting top-leve',
  documentTypes: ['pt_allTheBellsAndWhistles'],

  migrate: {
    // eslint-disable-next-line consistent-return
    array(node, path, ctx) {
      if (pathsAreEqual(path, targetPath)) {
        return set(toPlainText(node as PortableTextBlock[]))
      }
    },
  },
})

Migrate inline objects into references

This example shows how to convert an inline object in an array field into a new document and replace the array item with a reference to that new document.

You can also use this in Portable Text fields and use .filter({_type}) => _type == "blockType") to convert only specific custom blocks.

// npm install lodash
import {deburr} from 'lodash'
import {at, createIfNotExists, defineMigration, replace, patch} from 'sanity/migrate'

/**
 * if you want to make sure you don't create many duplicated
 * documents from the same pet, you can generate an ID for it 
 * that will be shared for all pets with the same name
 **/
function getPetId(pet: {name: string}) {
  return `pet-${deburr(pet.name.toLowerCase())}`
}

export default defineMigration({
  title: 'Convert an inline object in an array into a document and reference to it',
  documentTypes: ['human'],
  filter: 'defined(pets) && count(pets[]._ref) > 0',
  migrate: {
    document(human) {
      const currentPets = human.pets
      // migrate any pet object to a new document
      if (Array.isArray(currentPets) && currentPets.length > 0) {
        return currentPets
          // skip pets that have already been converted to a reference
          .filter((pet) => !pet._ref)
          .flatMap((pet) => {
            const petId = getPetId(pet)

            // avoid carrying over the array _key to the pet document
            const {_key, ...petAttributes} = pet

            return [
              createIfNotExists({
                _id: petId,
                _type: 'pet',
                ...petAttributes,
              }),
							patch(human._id, at(['pets'], replace([{_type: 'reference', _ref: petId}], {_key}))),
            ]
          })
      }
    },
  },
})

Migrate a document type

Gotcha

The _id and _type attributes/fields on documents are immutable; that is, they can't be changed once they are set; there is no straightforward way to change these using the content migration tooling.

The simplest and most controlled way of approaching the migration of a document _type and _id, is:

  • export your dataset (sanity dataset export <dataset>, add --no-assets if you're not planning to do anything with these)
  • Untar the export file (tar -xzvf <dataset>.tar.gz)
  • Open the NDJSON of your dataset (<dataset>.ndjson)
  • Use whatever method to find and replace all that you find suitable
  • Re-import your dataset with the --replace flag (sanity dataset import <dataset>.ndsjon <dataset> --replace)

Always ensure you have a backup of your dataset and triple-check before changing content in production.

Was this article helpful?