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}))),
]
})
}
},
},
})
import {at, defineMigration, del, setIfMissing, unset} from 'sanity/migrate'
export default defineMigration({
title: 'Delete posts and pages',
documentTypes: ['post', 'page'],
migrate: {
document(doc) {
// Note: If a document has incoming strong references, it can't be deleted by this script.
return del(doc._id)
},
},
})
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.
This migration will attempt to delete any file asset metadata documents over 50MB in size. Deleting the metadata document will also delete the asset from your dataset.
- Update
documentTypes
to includesanity.imageAsset
to remove images - Update
filter
to adjust the maximum file size (in bytes) - Note: The migration will fail if there are any references to the metadata document. The second
filter
example will filter out any large file assets already referenced by other documents.
import { defineMigration, delete_ } from "sanity/migrate";
export default defineMigration({
title: "Delete large files",
documentTypes: ["sanity.fileAsset"],
// Size is greater than 50MB
filter: "size > 50000000",
// Additionally only target unreferenced assets
// filter: "size > 50000000 && count(*[references(^._id)]) == 0",
migrate: {
document(doc) {
return delete_(doc._id);
},
},
});