Migrate text field to block array in nested Content objects

9 replies
Last updated: Dec 2, 2025
Hey, I’ve got a bunch of documents (Pages) with arrays of blocks, which all have a common object (Content). The Content object has a
text
object which needs to be migrated to a
block
object, as one or more plain text paragraphs.
There aren’t that many that I couldn’t do it by hand, but I thought I should learn how. I’ve read this article:
https://www.sanity.io/docs/migrating-data , but I’m a bit stuck on how to work with blocks. It seems like it’d be really onerous. Does anyone have any pointers?
AI Update

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 jsdom

Migration 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 document

The 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 thread
9 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.

Was this answer helpful?