Migrate text field to block array in nested Content objects
Good news! You're on the right track with learning migrations, and working with blocks is actually easier than you might think. The key is using the @portabletext/block-tools package, which has utilities specifically for converting text into Portable Text blocks.
Basic Strategy
Since you're converting plain text strings to Portable Text paragraphs, you can use the htmlToBlocks function from @portabletext/block-tools. Even though your source is plain text (not HTML), you can wrap it in paragraph tags to convert it easily.
First, install the package:
npm install -D @portabletext/block-tools jsdomMigration Script Example
Here's a pattern you can adapt for your Pages → blocks → Content → text to block migration:
import {htmlToBlocks} from '@portabletext/block-tools'
import {JSDOM} from 'jsdom'
import {getCliClient} from 'sanity/cli'
const client = getCliClient()
// Define your block content type schema
const blockContentType = {
type: 'block',
styles: [{title: 'Normal', value: 'normal'}],
lists: [],
marks: {decorators: [], annotations: []}
}
const migratePages = async () => {
// Fetch all Pages that need migration
const pages = await client.fetch(`*[_type == "page"]`)
for (const page of pages) {
const updatedBlocks = page.blocks?.map(block => {
if (block.content?.text && typeof block.content.text === 'string') {
// Convert plain text to HTML paragraphs
const textParagraphs = block.content.text
.split('\n\n')
.map(p => `<p>${p}</p>`)
.join('')
// Convert to Portable Text blocks
const portableTextBlocks = htmlToBlocks(
textParagraphs,
blockContentType,
{parseHtml: html => new JSDOM(html).window.document}
)
return {
...block,
content: {
...block.content,
block: portableTextBlocks
}
}
}
return block
})
// Update the document
await client.patch(page._id).set({blocks: updatedBlocks}).commit()
console.log(`Migrated ${page._id}`)
}
}
migratePages()Simpler Alternative for Plain Text
If your text content is really just plain paragraphs without any formatting, you can actually construct the Portable Text blocks manually without @portabletext/block-tools:
const textToBlocks = (text) => {
return text.split('\n\n').map(paragraph => ({
_type: 'block',
_key: Math.random().toString(36).substr(2, 9),
style: 'normal',
markDefs: [],
children: [{
_type: 'span',
_key: Math.random().toString(36).substr(2, 9),
text: paragraph,
marks: []
}]
}))
}This creates simple Portable Text blocks without needing the block-tools package. Each paragraph becomes a separate block.
Testing First
Before running on all documents, test with a single document:
const testPage = await client.fetch(`*[_type == "page"][0]`)
// Test your transformation logic
// Then update just that one documentThe data migration guide you've already read covers the transaction patterns well. For more complex HTML-to-blocks scenarios, check out the Sanity Learn course on migrating to block content, which has detailed examples of using @portabletext/block-tools with custom deserialization rules for handling images and other complex HTML structures.
Since you mentioned there aren't that many documents, you could also do a hybrid approach: export the text, convert it with a script, then paste it back into Studio. But writing the migration script is definitely a valuable skill to learn!
Show original thread9 replies
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.