
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeGood 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.
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 jsdomHere'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()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.
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!
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store