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.
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())
]
}
}
})
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')),
])
}
}
})
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())
]
}
}
})
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: [],
},
])
}
},
},
})
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[]))
}
},
},
})
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}))),
]
})
}
},
},
})
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.