'Sanity Migration Guide 2025: From WordPress to Headless CMS

>-

Migrating to Sanity: A Complete Guide

As businesses increasingly embrace headless architectures, migrating to Sanity has become a strategic priority for teams seeking modern content management capabilities. Sanity's real-time collaboration, powerful GROQ query language, and customizable studio experience make it our recommended headless CMS for most projects. Having executed numerous migrations across various content platforms, we've developed a comprehensive approach that minimizes disruption while maximizing the benefits of Sanity's modern architecture.

This guide draws from our experience implementing Sanity as our default CMS choice, covering the complete migration journey from initial planning through post-launch optimization. Whether you're moving from WordPress's traditional architecture or transitioning between headless platforms like Contentful, these strategies will help ensure a successful migration.

Why Migrate to Sanity?

The decision to migrate your content management system represents more than just a technical upgrade—it's an investment in your content team's future productivity and developer experience. Sanity's headless architecture separates content management from presentation, enabling content to be delivered across multiple channels through a unified API.

Modern headless architecture benefits include enhanced scalability, as your content can serve websites, mobile apps, and digital experiences without being tied to a specific frontend framework. This architectural flexibility reduces technical debt and allows teams to adopt modern development practices without content management limitations.

Sanity's real-time collaboration capabilities transform how content teams work together. Multiple editors can simultaneously work on the same document with live updates, similar to Google Docs but structured for professional content workflows. This collaborative approach reduces review cycles and enables faster content publication without sacrificing quality.

The GROQ query language provides developers with unprecedented flexibility in content retrieval. Unlike traditional REST endpoints, GROQ allows complex queries that can join related content, filter based on custom criteria, and return exactly the data structure needed for each use case. This query power translates to faster page loads and more efficient content delivery.

What truly sets Sanity apart is its custom studio capabilities. The Sanity Studio is an open-source React application that you can customize to match your exact content workflows. From custom input components to tailored desk structures, the studio adapts to your team's needs rather than forcing your team to adapt to rigid CMS interfaces.

Migration Planning & Preparation

Assess Your Current Content Structure

A successful migration begins with thorough content inventory and analysis. Before writing any migration code, you need a comprehensive understanding of your existing content ecosystem. This assessment phase prevents costly surprises and ensures your Sanity schema design accommodates all existing content types.

Start by conducting a complete content inventory that documents every content type, custom field, and relationship in your current system. For WordPress sites, this includes posts, pages, custom post types, taxonomies, and meta fields. Contentful users should catalog all content types, fields, and entry relationships. This inventory becomes your migration roadmap.

Data structure analysis involves examining how content pieces relate to each other and identifying potential schema optimizations. Look for opportunities to simplify complex structures or normalize repeated data patterns. This is also when you should identify any content that might need restructuring rather than direct migration.

Create detailed relationship maps showing how content types reference each other. WordPress sites often have complex taxonomy relationships, while Contentful entries may reference multiple other content types. Understanding these relationships ensures your Sanity schema can maintain all necessary connections.

Document all custom fields and their validation rules. WordPress ACF fields, Contentful validations, and any custom business logic need to be preserved or enhanced in your Sanity implementation. Pay special attention to fields that contain structured data or have specific formatting requirements.

Finally, conduct a thorough media asset assessment. Catalog all images, videos, documents, and their metadata. Note any custom media processing, CDN configurations, or specialized handling requirements. This information becomes crucial during the media migration phase.

Pro Tip

Create a migration spreadsheet that tracks each content type, field count, estimated records, and complexity rating. This helps prioritize migration efforts and identify potential challenges early.

Choose Your Migration Strategy

Your migration approach should align with your content volume, technical resources, timeline, and budget constraints. Understanding the available strategies helps you select the most appropriate path for your specific situation.

Manual migration works best for smaller sites with limited content (typically fewer than 100 pages). While time-consuming, manual migration provides an opportunity to review and optimize each piece of content during transfer. This approach also helps teams become familiar with Sanity's interface and capabilities early in the process.

Semi-automated scripts offer a balance between efficiency and control. This strategy typically involves exporting data from your current CMS, transforming it through custom scripts, and importing it into Sanity. The semi-automated approach allows for human review at key transformation points while automating repetitive data transfer tasks.

Full custom migration provides the most comprehensive solution for large-scale projects. This involves building sophisticated migration pipelines that handle complex data transformations, relationship mapping, and error handling. Custom migrations can include data validation, content optimization, and even automated content improvements during transfer.

Third-party migration services exist for teams lacking internal technical resources or working with extremely tight timelines. These services specialize in CMS migrations and often have pre-built tools for common scenarios. While more expensive, they can significantly reduce migration time and risk.

Several factors influence your strategy choice. Content volume directly impacts the feasibility of manual approaches—sites with thousands of content pieces almost always require automation. Technical resources determine whether you can build custom migration tools or need external assistance. Timeline constraints may necessitate parallel processing or third-party involvement. Budget considerations balance the cost of custom development against the speed of professional migration services.

Migrating from WordPress

Export WordPress Content

WordPress offers several methods for content extraction, each with distinct advantages depending on your site's complexity and hosting environment. The right export method depends on your WordPress setup, the volume of content, and the types of custom fields and plugins you're using.

The WordPress REST API provides a standardized way to access all content types programmatically. This method works well for sites with standard WordPress structures and allows for granular control over what content you export. The REST API can handle posts, pages, custom post types, taxonomies, and media, making it a comprehensive solution for most WordPress sites.

For command-line access, WP-CLI export commands offer powerful scripting capabilities. WP-CLI can export content directly from the server, handling large sites more efficiently than web-based exports. This method is particularly useful for automated migration scripts and scheduled data transfers.

# Export posts and pages via WP-CLI
wp post list --post_type=post --format=json > posts.json
wp post list --post_type=page --format=json > pages.json

# Export custom post types
wp post list --post_type=portfolio --format=json > portfolio.json

# Export taxonomies
wp term list --taxonomy=category --format=json > categories.json
wp term list --taxonomy=post_tag --format=json > tags.json

# Export all meta fields
wp post meta list --all --format=json > meta.json

Plugin-based exports become necessary when using popular WordPress plugins that add custom functionality. For Advanced Custom Fields (ACF), you'll need specific export methods to preserve field configurations and data. Yoast SEO and other SEO plugins require special handling to migrate meta titles, descriptions, and structured data.

When working with multisite WordPress installations, you'll need to export each subsite individually or use specialized migration tools that can handle the multisite database structure. Consider the network structure during your planning phase to avoid overlooking subsite-specific content.

Data Transformation for WordPress

Transforming WordPress data into Sanity's structure requires understanding how WordPress concepts map to Sanity's document model. This transformation preserves content integrity while adapting to Sanity's more flexible schema design.

WordPress posts and pages become Sanity documents with type fields distinguishing between different content structures. The transformation process converts WordPress's post_title, post_content, and post_excerpt fields into Sanity's title, body, and excerpt fields. Post dates transform into publishedAt fields, while post_name becomes the slug.

interface WordPressPost {
  ID: number;
  post_title: string;
  post_content: string;
  post_excerpt: string;
  post_date: string;
  post_name: string;
  post_status: string;
  post_author: number;
  featured_media: number;
  meta: Record;
}

function transformWordPressToSanity(wpPost: WordPressPost): SanityPost {
  return {
    _type: 'post',
    title: wpPost.post_title,
    slug: {
      _type: 'slug',
      current: wpPost.post_name
    },
    publishedAt: wpPost.post_date,
    body: transformHTMLToPortableText(wpPost.post_content),
    excerpt: wpPost.post_excerpt,
    status: wpPost.post_status === 'publish' ? 'published' : 'draft',
    author: {
      _type: 'reference',
      _ref: `author-${wpPost.post_author}`
    },
    featuredImage: wpPost.featured_media ? {
      _type: 'reference',
      _ref: `image-${wpPost.featured_media}`
    } : undefined,
    seo: {
      _type: 'seo',
      metaTitle: wpPost.meta._yoast_wpseo_title || wpPost.post_title,
      metaDescription: wpPost.meta._yoast_wpseo_metadesc || generateExcerpt(wpPost.post_excerpt),
      focusKeyword: wpPost.meta._yoast_wpseo_focuskw,
      opengraph: {
        title: wpPost.meta._yoast_wpseo_opengraph_title,
        description: wpPost.meta._yoast_wpseo_opengraph_description,
        image: wpPost.meta._yoast_wpseo_opengraph_image
      }
    }
  };
}

WordPress categories and tags transform into Sanity reference documents that maintain hierarchical relationships while enabling more flexible content association. Rather than WordPress's rigid taxonomy system, Sanity allows any document to reference any other document, enabling richer content relationships.

Featured images become Sanity assets with enhanced metadata and optimization capabilities. The transformation process preserves alt text, captions, and copyright information while adding Sanity's built-in optimization features.

Custom fields from WordPress plugins or ACF require special attention during transformation. Map these fields to appropriate Sanity types—text fields become strings, repeater fields become arrays, and complex objects maintain their structure while adapting to Sanity's schema validation.

Important Note

WordPress HTML content must be converted to Sanity's Portable Text format for rich text editing. Use libraries like @portabletext/to-html or build custom parsers that handle your specific HTML patterns.

Handle WordPress-Specific Content

WordPress ecosystems often include specialized content types and plugins that require custom handling during migration. Addressing these WordPress-specific elements ensures complete content preservation.

Advanced Custom Fields (ACF) fields require careful mapping to Sanity schema types. ACF repeater fields transform into Sanity arrays, while flexible content fields become Sanity portable text with custom marks. Date picker fields should be converted to ISO format, and relationship fields become Sanity references with proper validation.

// Handle ACF repeater fields
function transformACFRepeater(repeaterData: any[]): any[] {
  return repeaterData.map(item => ({
    _type: 'repeaterItem',
    field1: item.field_1,
    field2: item.field_2,
    // Map additional fields as needed
  }));
}

// Handle ACF flexible content
function transformACFFlexibleContent(flexibleData: any[]): PortableTextBlock[] {
  return flexibleData.map(layout => {
    const block: PortableTextBlock = {
      _type: 'block',
      _key: generateKey(),
      style: 'normal',
      markDefs: [],
      children: [{ _type: 'span', text: layout.content || '' }]
    };

    // Add custom marks for specific layouts
    if (layout.acf_fc_layout === 'highlight') {
      block.markDefs.push({
        _key: generateKey(),
        _type: 'highlight',
        color: layout.highlight_color
      });
    }

    return block;
  });
}

Gutenberg blocks present unique challenges due to their structured nature. Convert block-based content to Portable Text while preserving block types, attributes, and nested structures. Some complex blocks may require custom Sanity components to maintain their functionality in the Studio.

WordPress taxonomy structures transform into Sanity's reference-based system. Categories become hierarchical reference documents with parent-child relationships, while tags become simple reference documents. Custom taxonomies follow similar patterns, allowing for more flexible content categorization than WordPress's built-in taxonomies.

Media library migration involves downloading WordPress media files and uploading them to Sanity's asset pipeline. Preserve metadata like alt text, captions, and copyright information during this transfer. WordPress's media organization can be maintained through Sanity's custom metadata fields.

User roles and permissions from WordPress need to be mapped to Sanity's team-based access control. WordPress user accounts can be imported as Sanity team members with appropriate role assignments. Custom WordPress capabilities should be translated to Sanity's permission model.

Migrating from Contentful

Contentful Data Export

Contentful provides several methods for exporting content, each suited to different migration scenarios. The right export method depends on your Contentful space size, API rate limits, and the complexity of your content model.

The Contentful CLI offers the most comprehensive export capabilities, including content entries, assets, and content model definitions. This command-line tool handles large exports more efficiently than web-based methods and provides detailed progress reporting.

# Install Contentful CLI
npm install -g contentful-cli

# Login to Contentful
contentful login

# Export entire space with all content and assets
contentful space export \
  --space-id YOUR_SPACE_ID \
  --management-token YOUR_MANAGEMENT_TOKEN \
  --export-dir ./contentful-export \
  --include-drafts \
  --include-archived \
  --skip-content-model \
  --max-concurrent-requests 10

# Export only content model
contentful space export \
  --space-id YOUR_SPACE_ID \
  --management-token YOUR_MANAGEMENT_TOKEN \
  --content-model-only \
  --export-dir ./content-model-export

The Content Management API provides programmatic access to all content types and allows for custom export logic. This approach gives you fine-grained control over what content to export and can handle complex filtering, transformation, and processing during export.

// Custom Contentful export script

const client = createClient({
  accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN
});

const space = await client.getSpace('your-space-id');
const environment = await space.getEnvironment('master');

async function exportAllContent() {
  const entries = {};
  const assets = [];

  // Export all content types
  const contentTypes = await environment.getContentTypes();

  for (const contentType of contentTypes.items) {
    const items = await environment.getEntries({
      content_type: contentType.sys.id,
      limit: 1000
    });

    entries[contentType.sys.id] = items.items;
  }

  // Export all assets
  const assetItems = await environment.getAssets({
    limit: 1000
  });

  assets.push(...assetItems.items);

  return { entries, assets, contentTypes: contentTypes.items };
}

Contentful Migration Apps provide user-friendly interfaces for content export but may have limitations for very large spaces. These apps often include features for filtering content by specific criteria and can be useful for targeted migrations of specific content types.

When working with Contentful environments, ensure you're exporting from the correct environment (master, development, staging, etc.). Contentful's environment system allows multiple versions of your content model and content, so verify you're exporting the intended version.

Contentful to Sanity Schema Mapping

Contentful and Sanity share similar concepts but use different terminology and implementation details. Understanding these mappings helps ensure accurate schema conversion and preserves content relationships.

Contentful Rich Text fields transform into Sanity's Portable Text format. Rich Text preserves text formatting, embedded assets, and references to other entries. The conversion process maintains marks for bold, italic, and other formatting while converting embedded entities to Sanity references.

function transformRichText(richText: any): PortableTextBlock[] {
  if (!richText || !richText.content) return [];

  return richText.content.map((node: any): PortableTextBlock => {
    const block: PortableTextBlock = {
      _type: 'block',
      _key: generateKey(),
      style: 'normal',
      markDefs: [],
      children: []
    };

    // Handle different node types
    if (node.nodeType === 'paragraph') {
      block.style = 'normal';
    } else if (node.nodeType === 'heading-1') {
      block.style = 'h1';
    } else if (node.nodeType === 'heading-2') {
      block.style = 'h2';
    }

    // Process text content with marks
    node.content.forEach((content: any) => {
      if (content.nodeType === 'text') {
        const span: PortableTextSpan = {
          _type: 'span',
          text: content.value
        };

        // Apply marks (bold, italic, etc.)
        if (content.marks) {
          content.marks.forEach((mark: any) => {
            if (mark.type === 'bold') {
              span.text = `**${span.text}**`;
            } else if (mark.type === 'italic') {
              span.text = `*${span.text}*`;
            }
          });
        }

        block.children.push(span);
      }
    });

    return block;
  });
}

Contentful References convert directly to Sanity references, maintaining the relationship between content pieces. One-to-many relationships in Contentful become arrays of references in Sanity. Many-to-many relationships require special handling to ensure all linked entries are properly preserved.

Contentful Assets transform into Sanity's image and file asset types. The migration process downloads original files from Contentful's CDN and uploads them to Sanity's asset management system. Asset metadata like titles, descriptions, and custom fields are preserved during transfer.

Contentful Entries become Sanity documents with type fields based on the original Contentful content type. Entry IDs should be preserved or mapped to maintain URL structure and SEO value. Published status transforms to Sanity's draft/published system.

Contentful Locales for internationalized content map to Sanity's internationalization features. Sanity's i18n plugin provides similar functionality with some differences in implementation. Language-specific content should be maintained during migration to preserve localization efforts.

Preserve Content Relationships

Maintaining content relationships during migration is crucial for preserving content integrity and functionality. Contentful's reference system allows complex content graphs, which must be carefully preserved in Sanity.

Export with references intact by using Contentful's include parameter to fetch linked entries in a single query. This approach ensures all related content is available during transformation and prevents broken references in the migrated content.

// Export content with full reference graph
async function exportContentWithReferences(contentTypeId: string) {
  const entries = await environment.getEntries({
    content_type: contentTypeId,
    include: 10, // Include up to 10 levels of references
    limit: 1000
  });

  return entries.items;
}

Create mapping tables that track Contentful entry IDs and their corresponding Sanity document IDs. These mappings are essential for updating references during transformation and for implementing URL redirects after migration.

Handle circular references carefully to prevent infinite loops during processing. Some content models may have circular reference patterns where entries reference each other. Implement safeguards to detect and properly handle these scenarios during transformation.

Validate link integrity after migration by running queries that check for broken references or orphaned content. Sanity's query language makes it easy to identify documents with invalid references.

// Validate references after migration
async function validateReferences() {
  const brokenReferences = await sanityClient.fetch(`
    *[_type == "blogPost" && defined(relatedPosts) && count(relatedPosts[!defined(^)]) > 0]{
      _id,
      title,
      "brokenRefs": relatedPosts[!defined(^)]
    }
  `);

  return brokenReferences;
}

Building Custom Migration Scripts

Sanity Client Setup

Proper client configuration ensures secure and efficient data transfer between your source system and Sanity. The right client setup provides the foundation for reliable migration operations.

Create separate clients for different migration phases. A read-only import client handles data ingestion, while a management client manages schema operations and dataset configuration. This separation provides better security and control over migration operations.

// migration-client.ts

// Read-only client for content import
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET || 'migration',
  token: process.env.SANITY_IMPORT_TOKEN,
  useCdn: false,
  apiVersion: '2024-03-20',
  perspective: 'published' // Only work with published content during migration
});

// Management client for schema operations
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET || 'migration',
  token: process.env.SANITY_MANAGEMENT_TOKEN
});

// Validation client for post-migration checks
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET || 'migration',
  token: process.env.SANITY_READ_ONLY_TOKEN,
  useCdn: false,
  apiVersion: '2024-03-20'
});

Configure API rate limiting to respect Sanity's usage quotas and prevent throttling during large migrations. Implement retry logic for failed requests and use batch operations to optimize performance.

// Rate-limited client configuration
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET,
  token: process.env.SANITY_IMPORT_TOKEN,
  useCdn: false,
  apiVersion: '2024-03-20',
  // Custom configuration for large migrations
  requestTagPrefix: 'migration',
  maxRetries: 5,
  retryDelay: 1000
});

// Batch operation helper
async function createBatch(documents: any[], batchSize = 50) {
  const transaction = rateLimitedClient.transaction();

  for (let i = 0; i ;
}

  id: number;
  source_url: string;
  alt_text: string;
  caption: {
    rendered: string;
  };
  media_type: 'image' | 'file';
  mime_type: string;
  media_details: {
    width: number;
    height: number;
    file: string;
    sizes: Record;
  };
}

Create transformation functions that handle specific data types and edge cases. These functions should be pure and testable, making them easier to maintain and debug.

// transformers/wordpress.ts

  // Implement HTML to Portable Text conversion
  // Use libraries like html-to-portable-text or build custom parser

  if (!html) return [];

  // Remove WordPress-specific shortcodes and filters
  const cleanHtml = html
    .replace(/\[caption[^\]]*\]([\s\S]*?)\[\/caption\]/g, '$1')
    .replace(/\[gallery[^\]]*\]/g, '')
    .replace(/\[embed[^\]]*\]([\s\S]*?)\[\/embed\]/g, '$1');

  // Convert HTML to Portable Text blocks
  // This is a simplified example - use a robust HTML parser
  const blocks: PortableTextBlock[] = [];

  // Split HTML into paragraphs and process each
  const paragraphs = cleanHtml.split(//).filter(p => p.trim());

  paragraphs.forEach((paragraph, index) => {
    if (paragraph.trim()) {
      blocks.push({
        _type: 'block',
        _key: generateKey(),
        style: 'normal',
        markDefs: [],
        children: [{
          _type: 'span',
          text: paragraph.replace(/]*>/g, '').trim()
        }]
      });
    }
  });

  return blocks;
}

  return {
    _type: 'blogPost',
    _id: `wp-post-${wpPost.id}`,
    title: wpPost.title.rendered,
    slug: {
      _type: 'slug',
      current: wpPost.slug
    },
    publishedAt: wpPost.date,
    updatedAt: wpPost.modified,
    status: wpPost.status === 'publish' ? 'published' : 'draft',
    body: transformHTMLToPortableText(wpPost.content.rendered),
    excerpt: wpPost.excerpt.rendered ? {
      _type: 'excerpt',
      text: transformHTMLToPortableText(wpPost.excerpt.rendered)
    } : undefined,
    author: {
      _type: 'reference',
      _ref: `wp-author-${wpPost.author}`
    },
    categories: wpPost.categories.map(catId => ({
      _type: 'reference',
      _ref: `wp-category-${catId}`
    })),
    tags: wpPost.tags.map(tagId => ({
      _type: 'reference',
      _ref: `wp-tag-${tagId}`
    })),
    featuredImage: wpPost.featured_media ? {
      _type: 'reference',
      _ref: `wp-media-${wpPost.featured_media}`
    } : undefined,
    seo: {
      _type: 'seo',
      metaTitle: wpPost.meta._yoast_wpseo_title || wpPost.title.rendered,
      metaDescription: wpPost.meta._yoast_wpseo_metadesc || generateDescription(wpPost.excerpt.rendered),
      focusKeyword: wpPost.meta._yoast_wpseo_focuskw,
      noindex: wpPost.meta._yoast_wpseo_meta_robots_noindex === '1',
      canonical: wpPost.meta._yoast_wpseo_canonical
    }
  };
}

Implement error handling and logging throughout the transformation pipeline. Log transformation details, warnings, and errors to help with debugging and quality assurance.

// utils/logger.ts
  private logs: Array = [];

  info(message: string, details?: any) {
    this.log('info', message, details);
  }

  warn(message: string, details?: any) {
    this.log('warn', message, details);
  }

  error(message: string, details?: any) {
    this.log('error', message, details);
  }

  private log(level: 'info' | 'warn' | 'error', message: string, details?: any) {
    const entry = {
      timestamp: new Date(),
      level,
      message,
      details
    };

    this.logs.push(entry);
    console.log(`[${level.toUpperCase()}] ${message}`, details || '');
  }

  exportLogs() {
    return this.logs;
  }

  clearLogs() {
    this.logs = [];
  }
}

Batch Processing & Error Handling

Large-scale migrations require careful batch processing and comprehensive error handling to ensure data integrity and system stability. Implement strategies for handling partial failures, rate limiting, and progress tracking.

Design a batch processing system that divides large migration tasks into manageable chunks. This approach prevents memory issues, allows for better error recovery, and provides visibility into migration progress.

// batch-processor.ts

interface BatchOptions {
  batchSize: number;
  maxRetries: number;
  retryDelay: number;
  continueOnError: boolean;
}

  private logger = new MigrationLogger();

  async processBatch(
    items: T[],
    processor: (item: T) => Promise,
    options: BatchOptions
  ): Promise {
    const { batchSize, maxRetries, retryDelay, continueOnError } = options;
    let processed = 0;
    let failed = 0;
    const errors: any[] = [];

    // Process items in batches
    for (let i = 0; i  {
          let retryCount = 0;

          while (retryCount  {
        if (result.status === 'fulfilled') {
          processed++;
        } else {
          failed++;
          errors.push({
            item: i + index + 1,
            error: result.reason,
            data: batch[index]
          });

          if (!continueOnError) {
            throw new Error(`Batch processing failed at item ${i + index + 1}: ${result.reason.message}`);
          }
        }
      });

      // Small delay between batches to respect rate limits
      await this.delay(100);
    }

    return { processed, failed, errors };
  }

  private delay(ms: number): Promise {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  getErrors() {
    return this.logger.exportLogs().filter(log => log.level === 'error');
  }

  getWarnings() {
    return this.logger.exportLogs().filter(log => log.level === 'warn');
  }
}

Implement transaction management for data integrity. Use Sanity's transaction API to group related operations and ensure atomic updates.

// transaction-manager.ts
  async executeWithTransaction(
    operations: (() => Promise)[],
    batchSize: number = 50
  ): Promise {
    const results: T[] = [];

    for (let i = 0; i  ({
    id: item.id,
    url: item.source_url,
    title: item.title.rendered,
    altText: item.alt_text,
    caption: item.caption.rendered,
    description: item.description.rendered,
    mimeType: item.mime_type,
    mediaType: item.media_type,
    fileSize: item.media_details.filesize,
    dimensions: {
      width: item.media_details.width,
      height: item.media_details.height
    },
    uploadedAt: item.date,
    modifiedAt: item.modified,
    author: item.author,
    meta: item.meta || {}
  }));
}

Download original files from WordPress using the source URLs. Implement robust error handling for network issues and implement retry logic for failed downloads. Consider bandwidth limitations when downloading large numbers of files.

// Download media files with error handling

async function downloadMediaFile(mediaItem: any, downloadDir: string): Promise {
  const filePath = path.join(downloadDir, `${mediaItem.id}-${mediaItem.title.rendered.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`);

  try {
    const response = await axios({
      method: 'GET',
      url: mediaItem.source_url,
      responseType: 'stream',
      timeout: 30000
    });

    const writer = fs.createWriteStream(filePath);
    response.data.pipe(writer);

    return new Promise((resolve, reject) => {
      writer.on('finish', () => resolve(filePath));
      writer.on('error', reject);
    });
  } catch (error) {
    console.error(`Failed to download media ${mediaItem.id}:`, error.message);
    throw error;
  }
}

Upload to Sanity assets using the Sanity client's asset upload functionality. Sanity handles image optimization, CDN distribution, and responsive image generation automatically.

// Upload media to Sanity with metadata preservation
async function uploadToSanity(filePath: string, metadata: any) {
  try {
    const fileBuffer = fs.readFileSync(filePath);

    const asset = await importClient.assets.upload('image', fileBuffer, {
      filename: path.basename(filePath),
      altText: metadata.altText,
      title: metadata.title.rendered,
      description: metadata.description.rendered,
      source: {
        id: metadata.id.toString(),
        name: 'wordpress-migration',
        url: metadata.source_url
      },
      metadata: {
        originalSize: metadata.fileSize,
        originalDimensions: metadata.dimensions,
        mimeType: metadata.mimeType,
        uploadedAt: metadata.uploadedAt,
        author: metadata.author
      }
    });

    return {
      _type: 'image',
      _id: asset._id,
      asset: {
        _type: 'reference',
        _ref: asset._id
      },
      altText: metadata.altText,
      caption: metadata.caption.rendered
    };
  } catch (error) {
    console.error(`Sanity upload failed for ${filePath}:`, error);
    throw error;
  }
}

Update content references after successful asset uploads. WordPress stores image references in HTML content, which must be updated to use Sanity's reference format.

// Update WordPress content with Sanity image references
function updateImageReferences(content: string, imageMappings: Map) {
  // Replace WordPress image references
  let updatedContent = content;

  // Replace wp-image-XXX classes with Sanity references
  updatedContent = updatedContent.replace(
    /class=".*?wp-image-(\d+).*?"/g,
    (match, imageId) => {
      const mapping = imageMappings.get(parseInt(imageId));
      if (mapping) {
        return `data-sanity-image-ref="${mapping._id}"`;
      }
      return match;
    }
  );

  // Replace WordPress image URLs with Sanity CDN URLs
  updatedContent = updatedContent.replace(
    /src="https?:\/\/[^"]*\/wp-content\/uploads\/[^"]*"/g,
    (match) => {
      const imageUrl = match.match(/src="([^"]*)"/)[1];
      const imageId = extractImageIdFromUrl(imageUrl);
      const mapping = imageMappings.get(imageId);

      if (mapping && mapping.asset) {
        return `src="${mapping.asset.url}"`;
      }
      return match;
    }
  );

  return updatedContent;
}

Contentful Asset Migration

Contentful assets include rich metadata and relationships that must be preserved during migration. The migration process should maintain asset descriptions, titles, and any custom metadata fields.

Use the Contentful Asset API to download assets with their original metadata preserved. Contentful provides different asset URLs for various transformations, so access the original, untransformed version for migration.

// Download Contentful assets
async function downloadContentfulAsset(asset: any, downloadDir: string): Promise {
  const response = await fetch(asset.fields.file[process.env.CONTENTFUL_LOCALE || 'en-US'].url);
  const buffer = Buffer.from(await response.arrayBuffer());

  const fileName = asset.fields.title[process.env.CONTENTFUL_LOCALE || 'en-US']
    .replace(/[^a-z0-9]/gi, '_')
    .toLowerCase();

  const filePath = path.join(downloadDir, `${asset.sys.id}-${fileName}`);
  fs.writeFileSync(filePath, buffer);

  return filePath;
}

Preserve alt text and titles from Contentful asset fields. Contentful stores this information in localized fields, so ensure you're accessing the correct locale during migration.

// Extract Contentful asset metadata
function extractContentfulAssetMetadata(asset: any) {
  const locale = process.env.CONTENTFUL_LOCALE || 'en-US';

  return {
    id: asset.sys.id,
    title: asset.fields.title?.[locale] || '',
    description: asset.fields.description?.[locale] || '',
    fileName: asset.fields.file?.[locale]?.fileName || '',
    contentType: asset.fields.file?.[locale]?.contentType || '',
    size: asset.fields.file?.[locale]?.details?.size || 0,
    dimensions: asset.fields.file?.[locale]?.details?.image || {},
    uploadedAt: asset.sys.createdAt,
    updatedAt: asset.sys.updatedAt,
    tags: asset.metadata?.tags || []
  };
}

Handle different asset types appropriately. Contentful supports images, videos, documents, and other file types, each requiring specific handling during migration to Sanity.

// Handle different asset types
async function migrateContentfulAsset(asset: any, downloadDir: string) {
  const metadata = extractContentfulAssetMetadata(asset);
  const filePath = await downloadContentfulAsset(asset, downloadDir);

  const fileBuffer = fs.readFileSync(filePath);

  let sanityAsset;

  if (metadata.contentType.startsWith('image/')) {
    sanityAsset = await importClient.assets.upload('image', fileBuffer, {
      filename: metadata.fileName,
      title: metadata.title,
      description: metadata.description,
      metadata: {
        originalId: metadata.id,
        originalSize: metadata.size,
        originalDimensions: metadata.dimensions,
        tags: metadata.tags
      }
    });
  } else {
    sanityAsset = await importClient.assets.upload('file', fileBuffer, {
      filename: metadata.fileName,
      title: metadata.title,
      description: metadata.description,
      metadata: {
        originalId: metadata.id,
        originalSize: metadata.size,
        contentType: metadata.contentType,
        tags: metadata.tags
      }
    });
  }

  return {
    _type: metadata.contentType.startsWith('image/') ? 'image' : 'file',
    _id: sanityAsset._id,
    asset: {
      _type: 'reference',
      _ref: sanityAsset._id
    },
    title: metadata.title,
    description: metadata.description,
    originalId: metadata.id
  };
}

Optimize Images During Migration

Leverage Sanity's powerful image pipeline during migration to improve site performance and reduce bandwidth usage. Sanity automatically handles image format conversion, responsive generation, and CDN optimization.

Sanity's automatic format conversion creates multiple image formats (WebP, AVIF, JPEG) from uploaded images, allowing browsers to select the most efficient format for each device and network condition.

// Configure image optimization during upload
async function uploadOptimizedImage(filePath: string, metadata: any) {
  const fileBuffer = fs.readFileSync(filePath);

  const asset = await importClient.assets.upload('image', fileBuffer, {
    filename: metadata.fileName,
    altText: metadata.altText,
    metadata: {
      optimize: true,
      // Sanity handles these automatically
      generateWebp: true,
      generateAvif: true,
      responsiveBreakpoints: [
        { width: 640, height: 360 },
        { width: 768, height: 432 },
        { width: 1024, height: 576 },
        { width: 1280, height: 720 },
        { width: 1536, height: 864 }
      ]
    },
    // Custom metadata for optimization tracking
    customMetadata: {
      source: 'migration',
      originalSize: metadata.fileSize,
      optimizedAt: new Date().toISOString()
    }
  });

  return asset;
}

Responsive image generation ensures optimal display across all devices. Sanity automatically creates multiple image sizes that can be used in responsive designs, reducing page load times and improving user experience.

// Use optimized images in components
interface OptimizedImage {
  asset: {
    _ref: string;
  };
  altText?: string;
  hotspot?: {
    x: number;
    y: number;
    height: number;
    width: number;
  };
  crop?: {
    top: number;
    bottom: number;
    left: number;
    right: number;
  };
}

CDN optimization through Sanity's global CDN ensures fast image delivery worldwide. Images are cached at edge locations, reducing latency for users regardless of their geographic location.

Lazy loading support can be implemented using Sanity's image URL parameters. Add loading="lazy" attributes to image elements and use Sanity's built-in lazy loading capabilities to improve page load performance.

Data Validation & Quality Assurance

Content Integrity Checks

Thorough validation ensures that migrated content maintains its quality, relationships, and functionality. Implement comprehensive checks to verify migration success before going live.

Verify complete content migration by comparing counts and performing detailed content audits. Ensure all content types, fields, and relationships have been successfully transferred.

// Comprehensive content validation
async function validateContentMigration() {
  const validationResults = {
    totalIssues: 0,
    issues: [],
    recommendations: []
  };

  // Check for missing required fields
  const missingRequiredFields = await validationClient.fetch(`
    *[_type == "blogPost" && (!defined(title) || title == "")]{
      _id,
      _type,
      "missingFields": ["title"]
    }
  `);

  if (missingRequiredFields.length > 0) {
    validationResults.totalIssues += missingRequiredFields.length;
    validationResults.issues.push({
      type: 'missing_required_fields',
      count: missingRequiredFields.length,
      examples: missingRequiredFields.slice(0, 5)
    });
  }

  // Check for empty content bodies
  const emptyBodies = await validationClient.fetch(`
    *[_type == "blogPost" && (!defined(body) || count(body) == 0)]{
      _id,
      title,
      _type
    }
  `);

  if (emptyBodies.length > 0) {
    validationResults.totalIssues += emptyBodies.length;
    validationResults.issues.push({
      type: 'empty_bodies',
      count: emptyBodies.length,
      examples: emptyBodies.slice(0, 5)
    });
  }

  // Check for broken references
  const brokenReferences = await validationClient.fetch(`
    *[_type == "blogPost" && (defined(author) && !defined(author->name)) || (defined(categories) && count(categories[!defined(^)]) > 0)]{
      _id,
      title,
      _type,
      "brokenRefs": select(
        defined(author) && !defined(author->name) => ["author"],
        defined(categories) && count(categories[!defined(^)]) > 0 => categories[!defined(^)],
        []
      )
    }
  `);

  if (brokenReferences.length > 0) {
    validationResults.totalIssues += brokenReferences.length;
    validationResults.issues.push({
      type: 'broken_references',
      count: brokenReferences.length,
      examples: brokenReferences.slice(0, 5)
    });
  }

  // Check SEO metadata completeness
  const missingSeo = await validationClient.fetch(`
    *[_type == "blogPost" && (!defined(seo) || !defined(seo.metaTitle) || !defined(seo.metaDescription))]{
      _id,
      title,
      _type,
      "seoIssues": select(
        !defined(seo) => ["missing seo object"],
        !defined(seo.metaTitle) => ["missing metaTitle"],
        !defined(seo.metaDescription) => ["missing metaDescription"],
        []
      )
    }
  `);

  if (missingSeo.length > 0) {
    validationResults.issues.push({
      type: 'missing_seo',
      count: missingSeo.length,
      examples: missingSeo.slice(0, 5)
    });
  }

  return validationResults;
}

Validate that links and references are preserved correctly during migration. Check both internal links within content and reference fields between documents.

// Validate link integrity
async function validateLinkIntegrity() {
  const issues = [];

  // Check internal links in portable text
  const documentsWithLinks = await validationClient.fetch(`
    *[_type == "blogPost" && defined(body)][_id, title, body]
  `);

  for (const doc of documentsWithLinks) {
    const links = extractLinksFromPortableText(doc.body);

    for (const link of links) {
      if (link.href?.startsWith('/') && !isValidInternalLink(link.href)) {
        issues.push({
          documentId: doc._id,
          documentTitle: doc.title,
          brokenLink: link.href,
          linkText: link.text
        });
      }
    }
  }

  // Check reference fields
  const brokenRefs = await validationClient.fetch(`
    *[_type == "blogPost" && defined(relatedPosts) && count(relatedPosts[!defined(^)]) > 0]{
      _id,
      title,
      "brokenReferences": relatedPosts[!defined(^)]._ref
    }
  `);

  brokenRefs.forEach(doc => {
    doc.brokenReferences.forEach(ref => {
      issues.push({
        documentId: doc._id,
        documentTitle: doc.title,
        brokenReference: ref
      });
    });
  });

  return issues;
}

function extractLinksFromPortableText(portableText: PortableTextBlock[]): Array {
  const links = [];

  portableText.forEach(block => {
    if (block.markDefs) {
      block.markDefs.forEach(mark => {
        if (mark._type === 'link') {
          const linkText = block.children
            .filter(child => child.marks?.includes(mark._key))
            .map(child => child.text)
            .join('');

          links.push({
            href: mark.href,
            text: linkText
          });
        }
      });
    }
  });

  return links;
}

Verify that images and media are displaying correctly with proper metadata. Check for missing alt text, broken image references, and optimization settings.

// Validate media assets
async function validateMediaAssets() {
  const issues = [];

  // Check for images without alt text
  const imagesWithoutAlt = await validationClient.fetch(`
    *[_type == "image" && (!defined(altText) || altText == "")]{
      _id,
      asset->{_ref, _type}
    }
  `);

  if (imagesWithoutAlt.length > 0) {
    issues.push({
      type: 'missing_alt_text',
      count: imagesWithoutAlt.length,
      message: 'Images missing alt text for accessibility'
    });
  }

  // Check for broken image references
  const brokenImageRefs = await validationClient.fetch(`
    *[_type == "blogPost" && defined(featuredImage) && !defined(featuredImage.asset)]{
      _id,
      title,
      "brokenImageRef": featuredImage._ref
    }
  `);

  if (brokenImageRefs.length > 0) {
    issues.push({
      type: 'broken_image_references',
      count: brokenImageRefs.length,
      examples: brokenImageRefs.slice(0, 5)
    });
  }

  // Check image optimization
  const unoptimizedImages = await validationClient.fetch(`
    *[_type == "image" && !defined(metadata.optimize)]{
      _id,
      asset->{_ref}
    }
  `);

  if (unoptimizedImages.length > 0) {
    issues.push({
      type: 'unoptimized_images',
      count: unoptimizedImages.length,
      message: 'Images not optimized for performance'
    });
  }

  return issues;
}

Performance Testing

Performance validation ensures that the migrated content loads efficiently and provides a good user experience. Test various aspects of content delivery and query performance.

Measure content loading speed for different types of content and page sizes. Compare performance against benchmarks and identify optimization opportunities.

// Performance testing suite
  async testQueryPerformance(): Promise {
    const tests = [
      {
        name: 'Single blog post fetch',
        query: '*[_type == "blogPost" && slug.current == $slug][0]',
        params: { slug: 'test-post' }
      },
      {
        name: 'Latest posts list',
        query: '*[_type == "blogPost" && status == "published"] | order(publishedAt desc)[0...10]',
        params: {}
      },
      {
        name: 'Category posts',
        query: '*[_type == "blogPost" && $categoryId in categories[]._ref] | order(publishedAt desc)[0...20]',
        params: { categoryId: 'test-category' }
      },
      {
        name: 'Search query',
        query: '*[_type == "blogPost" && title match $searchTerm]',
        params: { searchTerm: 'test' }
      }
    ];

    const results = [];

    for (const test of tests) {
      const times = [];

      // Run each test multiple times for average
      for (let i = 0; i  a + b, 0) / times.length;
      const minTime = Math.min(...times);
      const maxTime = Math.max(...times);

      results.push({
        testName: test.name,
        query: test.query,
        averageTime: avgTime,
        minTime,
        maxTime,
        times: times.length
      });
    }

    return results;
  }

  async testImageLoadingSpeed(): Promise {
    const images = await validationClient.fetch(`
      *[_type == "image"][0...20]{
        _id,
        asset->{url},
        metadata
      }
    `);

    const results = [];

    for (const image of images) {
      const sizes = [
        { width: 400, height: 300 },
        { width: 800, height: 600 },
        { width: 1200, height: 900 }
      ];

      const loadTimes = [];

      for (const size of sizes) {
        const url = `${image.asset.url}?w=${size.width}&h=${size.height}&fit=crop&auto=format`;

        const start = performance.now();
        const response = await fetch(url);
        const end = performance.now();

        if (response.ok) {
          loadTimes.push({
            size: `${size.width}x${size.height}`,
            loadTime: end - start,
            fileSize: response.headers.get('content-length')
          });
        }
      }

      results.push({
        imageId: image._id,
        loadTimes,
        optimized: !!image.metadata.optimize
      });
    }

    return results;
  }
}

interface QueryPerformanceResult {
  testName: string;
  query: string;
  averageTime: number;
  minTime: number;
  maxTime: number;
  times: number;
}

interface ImagePerformanceResult {
  imageId: string;
  loadTimes: Array;
  optimized: boolean;
}

GROQ query performance testing helps identify slow queries that might impact site performance. Profile complex queries and optimize them for better execution times.

Image optimization verification ensures that Sanity's image pipeline is working correctly and delivering optimized formats. Test different image sizes, formats, and CDN performance.

CDN effectiveness testing measures how quickly content is delivered to users in different geographic regions. Verify that Sanity's CDN is providing the expected performance improvements.

SEO Preservation

Maintaining SEO value during migration is crucial for preserving search rankings and organic traffic. Implement comprehensive SEO validation to ensure migration doesn't impact search visibility.

Verify URL structure preservation to maintain existing search engine rankings. Implement proper redirects if URL structures change during migration.

// SEO validation
async function validateSEOPreservation() {
  const issues = [];

  // Check for missing slugs
  const missingSlugs = await validationClient.fetch(`
    *[_type == "blogPost" && (!defined(slug) || !defined(slug.current))]{
      _id,
      title
    }
  `);

  if (missingSlugs.length > 0) {
    issues.push({
      type: 'missing_slugs',
      count: missingSlugs.length,
      message: 'Blog posts missing slugs for URL generation'
    });
  }

  // Check SEO metadata completeness
  const incompleteSEO = await validationClient.fetch(`
    *[_type == "blogPost" && status == "published" && (
      !defined(seo) ||
      !defined(seo.metaTitle) ||
      !defined(seo.metaDescription) ||
      seo.metaTitle == "" ||
      seo.metaDescription == ""
    )]{
      _id,
      title,
      slug,
      "seoIssues": select(
        !defined(seo) => ["missing seo object"],
        !defined(seo.metaTitle) || seo.metaTitle == "" => ["missing metaTitle"],
        !defined(seo.metaDescription) || seo.metaDescription == "" => ["missing metaDescription"],
        []
      )
    }
  `);

  if (incompleteSEO.length > 0) {
    issues.push({
      type: 'incomplete_seo',
      count: incompleteSEO.length,
      examples: incompleteSEO.slice(0, 5)
    });
  }

  // Check title tag lengths
  const titleLengths = await validationClient.fetch(`
    *[_type == "blogPost" && status == "published" && length(seo.metaTitle) > 60]{
      _id,
      title,
      "titleLength": length(seo.metaTitle),
      "metaTitle": seo.metaTitle
    }
  `);

  if (titleLengths.length > 0) {
    issues.push({
      type: 'title_too_long',
      count: titleLengths.length,
      message: 'Meta titles exceeding 60 characters'
    });
  }

  // Check description lengths
  const descriptionLengths = await validationClient.fetch(`
    *[_type == "blogPost" && status == "published" && length(seo.metaDescription) > 160]{
      _id,
      title,
      "descriptionLength": length(seo.metaDescription),
      "metaDescription": seo.metaDescription
    }
  `);

  if (descriptionLengths.length > 0) {
    issues.push({
      type: 'description_too_long',
      count: descriptionLengths.length,
      message: 'Meta descriptions exceeding 160 characters'
    });
  }

  return issues;
}

Validate that meta tags migration preserved all SEO metadata correctly. Check title tags, meta descriptions, Open Graph tags, and structured data.

Redirect implementation verification ensures proper URL redirection if the migration changes URL patterns. Test that old URLs correctly redirect to new locations with appropriate status codes.

Canonical URL validation confirms that proper canonical tags are in place to prevent duplicate content issues during and after migration.

Post-Migration Optimization

Sanity Schema Refinement

After completing the initial migration, refine your Sanity schemas to take full advantage of Sanity's capabilities and improve the content editing experience.

Implement better validation rules to ensure data quality and prevent content errors. Sanity's validation system can enforce field requirements, format constraints, and business logic.

// Enhanced schema validation
  name: 'blogPost',
  type: 'document',
  title: 'Blog Post',
  fields: [
    {
      name: 'title',
      type: 'string',
      title: 'Title',
      validation: Rule => [
        Rule.required().error('Title is required'),
        Rule.min(10).warning('Titles should be at least 10 characters for SEO'),
        Rule.max(60).warning('Titles should not exceed 60 characters')
      ]
    },
    {
      name: 'slug',
      type: 'slug',
      title: 'Slug',
      options: {
        source: 'title',
        maxLength: 200
      },
      validation: Rule => Rule.required()
    },
    {
      name: 'body',
      type: 'portableText',
      title: 'Body Content',
      validation: Rule => [
        Rule.required().error('Body content is required'),
        Rule.min(50).warning('Posts should have at least 50 characters for SEO')
      ]
    },
    {
      name: 'excerpt',
      type: 'excerpt',
      title: 'Excerpt',
      validation: Rule => [
        Rule.max(300).warning('Excerpts should not exceed 300 characters')
      ]
    }
  ],
  preview: {
    select: {
      title: 'title',
      media: 'featuredImage'
    }
  }
};

Create custom input components that improve the content editing experience. Custom components can provide specialized interfaces for specific data types or integrate with third-party services.

// Custom SEO component

  const { type, onChange, value } = props;

  const handleChange = (field: string, fieldValue: string) => {
    onChange({
      ...value,
      [field]: fieldValue
    });
  };

  return (
    
      
         handleChange('metaTitle', e.target.value)}
          ref={ref}
        />
      

      
         handleChange('metaDescription', e.target.value)}
        />
      

      {value?.metaTitle && value?.metaTitle.length > 60 && (
        
          Title exceeds recommended 60 characters
        
      )}

      {value?.metaDescription && value?.metaDescription.length > 160 && (
        
          Description exceeds recommended 160 characters
        
      )}
    
  );
});

Configure preview configurations that show rich previews in the Sanity Studio. Well-designed previews help content editors quickly identify and navigate content.

// Enhanced preview configuration
  name: 'blogPost',
  type: 'document',
  title: 'Blog Post',
  preview: {
    select: {
      title: 'title',
      author: 'author.name',
      media: 'featuredImage',
      publishedAt: 'publishedAt',
      status: 'status'
    },
    prepare(selection: any) {
      const { title, author, media, publishedAt, status } = selection;
      return {
        title: title || 'Untitled Post',
        subtitle: author ? `by ${author} • ${new Date(publishedAt).toLocaleDateString()}` : 'No author',
        media,
        status: status === 'published' ? '✅ Published' : '📝 Draft'
      };
    }
  }
};

Content Team Training

Comprehensive training ensures your content team can leverage Sanity's full capabilities and work efficiently in the new environment.

Train team members on Sanity Studio navigation and interface basics. Cover document creation, editing, publishing workflows, and version management.

Content editing workflows training should cover how to create, edit, and publish content using Sanity's collaborative features. Demonstrate real-time collaboration, comment threads, and review processes.

Real-time collaboration features like simultaneous editing, presence indicators, and live comments can significantly improve content team productivity. Train team members on these collaborative capabilities.

Preview functionality training shows how content editors can preview changes across different devices and contexts before publishing. Sanity's preview system integrates with your frontend to provide accurate previews.

Training Resources

Create a content style guide and workflow documentation specific to your Sanity implementation. Include examples of best practices and common editing scenarios.

Monitor & Iterate

Post-migration monitoring helps identify issues quickly and continuously improve the content management system.

Monitor content usage patterns to understand how content is being created and consumed. Use analytics to identify popular content types, frequently used fields, and potential optimizations.

Track query performance to identify slow queries or inefficient data fetching patterns. Sanity's query analytics can help optimize content delivery performance.

Collect user feedback from content editors to identify pain points and improvement opportunities. Regular surveys and feedback sessions help prioritize system improvements.

Implement bug tracking and resolution processes to quickly address issues that arise during daily use. Create a clear process for reporting, prioritizing, and fixing bugs.

Common Migration Challenges

Handling Complex Content

Complex content structures present unique challenges during migration. Understanding these challenges helps prepare effective solutions.

Nested repeater fields from systems like WordPress or Contentful require careful transformation into Sanity's array structures. Maintain data relationships while adapting to Sanity's schema design.

// Handle complex repeater fields
function transformRepeaterField(repeaterData: any[]): SanityArrayField[] {
  return repeaterData.map((item, index) => ({
    _key: `item-${index}`,
    _type: 'repeaterItem',
    title: item.title,
    description: item.description,
    // Handle nested repeaters
    nestedItems: item.nested_items ? transformRepeaterField(item.nested_items) : [],
    // Handle conditional fields
    conditionalField: item.conditional_condition ? {
      _type: 'conditionalBlock',
      value: item.conditional_value
    } : undefined
  }));
}

Conditional logic in source systems must be adapted to Sanity's approach. While Sanity doesn't have built-in conditional fields like some CMS platforms, you can achieve similar functionality through custom components or schema design.

Multi-language content migration requires careful handling of Sanity's internationalization features. Ensure language-specific content is properly separated and relationships between language versions are maintained.

Version history preservation during migration maintains content audit trails. While Sanity has its own versioning system, you may need to migrate historical versions for compliance or editorial purposes.

Performance at Scale

Large-scale migrations introduce performance challenges that require specialized solutions and optimization strategies.

Batch processing prevents memory issues and rate limiting problems during large migrations. Process content in manageable chunks with proper error handling and progress tracking.

// Optimized batch processing for large datasets
  private readonly MAX_BATCH_SIZE = 100;
  private readonly RATE_LIMIT_DELAY = 100; // ms between batches

  async migrateLargeDataset(
    items: T[],
    transform: (item: T) => Promise,
    onProgress?: (processed: number, total: number) => void
  ) {
    const totalItems = items.length;
    let processedItems = 0;

    // Process in batches
    for (let i = 0; i  {
        try {
          const result = await transform(item);
          return { success: true, data: result };
        } catch (error) {
          console.error(`Item ${i + batchIndex} failed:`, error);
          return { success: false, error, originalItem: item };
        }
      });

      const batchResults = await Promise.allSettled(batchPromises);

      // Report batch progress
      processedItems += batch.length;
      onProgress?.(processedItems, totalItems);

      // Rate limiting delay
      if (i + this.MAX_BATCH_SIZE  {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Parallel imports can significantly speed up migration time but require careful management of API rate limits and resource usage. Use worker threads or parallel processing to maximize throughput.

Rate limiting respect prevents API throttling and ensures stable migration performance. Implement exponential backoff and retry logic for failed requests.

Progress tracking provides visibility into migration status and helps identify performance bottlenecks. Real-time progress reporting keeps stakeholders informed and helps with troubleshooting.

Maintaining Availability

Minimizing downtime during migration ensures continuous content delivery and prevents disruption to user experience.

Blue-green deployment strategies allow you to maintain content availability during migration. Run the old and new systems in parallel and switch over when migration is complete.

Feature flags enable gradual rollout of new functionality while maintaining the ability to quickly rollback if issues arise. Implement flags for content delivery and management features.

// Feature flag system for gradual migration
  private flags: Map = new Map();

  constructor(config: Record) {
    this.flags = new Map(Object.entries(config));
  }

  isEnabled(feature: string): boolean {
    return this.flags.get(feature) || false;
  }

  enable(feature: string): void {
    this.flags.set(feature, true);
  }

  disable(feature: string): void {
    this.flags.set(feature, false);
  }

  // Gradual rollout based on percentage
  isEnabledForPercentage(feature: string, percentage: number): boolean {
    const hash = this.hashString(feature);
    return (hash % 100) ;
  }
  return ;
}

Gradual rollout strategies reduce risk by slowly migrating content and functionality. Start with non-critical content types and gradually migrate more important content.

Rollback procedures prepare for unexpected issues and ensure you can quickly revert changes if necessary. Document rollback steps and test them before going live.

Migration Tools & Services

Sanity CLI Tools

Sanity provides comprehensive command-line tools that streamline development, deployment, and content management operations. Mastering these tools significantly improves migration efficiency.

The Sanity CLI offers powerful commands for project management, content operations, and deployment. These tools are essential for both development and production migration workflows.

# Install Sanity CLI globally
npm install -g @sanity/cli

# Initialize new project
npm create sanity@latest

# Deploy Studio to production
npx sanity deploy

# Manage datasets
sanity dataset list
sanity dataset create production
sanity dataset import ./export.tar.gz production
sanity dataset export production ./backup.tar.gz

# Content management commands
sanity documents list --dataset production
sanity documents create --dataset production ./new-document.json
sanity documents query '*[_type == "blogPost"]' --dataset production

# Schema management
sanity schema validate
sanity schema diff

# Development commands
npx sanity dev --host 0.0.0.0 --port 3333
npx sanity start

Dataset management commands handle multiple environments and backups. Create separate datasets for development, staging, and production to isolate migration testing from live content.

Content import/export utilities facilitate bulk content operations and provide backup capabilities. These tools are essential for large-scale migrations and content recovery.

Schema validation commands help ensure your Sanity schemas are properly defined and follow best practices. Use validation during development to catch issues early.

Third-Party Migration Services

When internal resources are limited or timelines are aggressive, third-party migration services can provide specialized expertise and tools for complex migrations.

Cloud-based migration platforms offer automated tools for common migration scenarios. These platforms often provide pre-built connectors for popular CMS systems and handle technical complexity automatically.

Research platforms that specialize in headless CMS migrations and have experience with your specific source system. Look for services that offer data validation, rollback capabilities, and post-migration support.

Specialist migration agencies provide comprehensive migration services including planning, execution, and post-launch optimization. These agencies bring extensive experience and can handle complex scenarios that automated tools might not address.

When selecting an agency, evaluate their track record with similar migrations, technical expertise with both your source system and Sanity, and approach to quality assurance and testing.

Open-source migration tools provide cost-effective solutions for teams with technical resources. These tools often offer flexibility for custom requirements but require development effort to implement and maintain.

Popular open-source options include content transformation libraries, CMS connectors, and migration frameworks that can be customized for specific requirements.

When to Get Professional Help

Recognizing when to seek expert assistance can prevent costly mistakes and ensure migration success. Several indicators suggest the need for professional migration services.

Large content volumes (<10k items) typically require specialized tools and expertise to migrate efficiently. Large migrations introduce performance, reliability, and data integrity challenges that benefit from professional experience.

Complex custom integrations with third-party systems, custom workflows, or specialized content types often require expert knowledge to preserve functionality during migration.

Tight timeline requirements may necessitate parallel processing, specialized tools, or additional resources that professional services can provide.

Limited technical resources within your organization can make migration planning and execution challenging. Professional services provide the expertise needed while allowing your team to focus on core business activities.

Consider the migration's strategic importance and impact on business operations when deciding between internal and external resources. High-stakes migrations often benefit from professional involvement to minimize risk.

Migration Cost Considerations

Hidden Migration Costs

Understanding the full cost of migration helps with budget planning and resource allocation. Migration costs extend beyond obvious development expenses.

Development time represents the most significant cost component. Factor in time for schema design, migration script development, testing, and troubleshooting. Migration projects often require more development time than initially estimated.

Testing and quality assurance require substantial resources to ensure data integrity and system reliability. Budget for comprehensive testing environments, validation tools, and QA personnel.

Content team training costs include time for training sessions, documentation creation, and learning curve adjustments during the transition period. Factor in temporary productivity decreases as the team adapts to the new system.

Downtime impact can result in lost revenue or user engagement during migration windows. Plan for maintenance windows, communication strategies, and contingency plans.

Post-launch optimization costs include performance tuning, bug fixes, and feature enhancements that become apparent after launch. Budget for ongoing support and optimization work.

ROI of Migration

While migration costs can be substantial, the long-term benefits often provide significant return on investment through improved efficiency and capabilities.

Improved content velocity results from Sanity's superior editing experience and real-time collaboration. Teams typically publish content faster and with fewer coordination challenges.

Better developer experience translates to faster development cycles and reduced maintenance overhead. Sanity's modern architecture and tooling improve developer productivity and satisfaction.

Enhanced content flexibility enables new content types, delivery channels, and user experiences that were impossible with legacy systems. This flexibility drives business innovation and competitive advantage.

Reduced technical debt through modern architecture and improved maintainability lowers long-term development costs and improves system reliability.

Future-proof architecture ensures your content management system can evolve with changing business needs and technology trends, protecting your investment over time.

Consider both quantitative benefits (time savings, reduced hosting costs, improved performance) and qualitative benefits (user experience, team satisfaction, business agility) when calculating migration ROI.

Migration Checklist

Pre-Migration

Complete these preparation tasks before starting the actual migration process:

  • Content inventory complete - Document all content types, fields, and relationships
  • Sanity project set up - Create project, configure datasets, set up team members
  • Schema designed - Define all Sanity schemas and validation rules
  • Migration scripts tested - Validate scripts with small sample datasets
  • Backup strategy in place - Create backups of source system and test restore procedures
  • Team trained on basics - Provide initial training on Sanity Studio and concepts
  • Performance benchmarks established - Measure current system performance for comparison
  • URL mapping documented - Plan how URLs will change and required redirects
  • SEO metadata audited - Ensure all SEO data is identified and preserved
  • Media asset plan created - Document media file organization and optimization strategy

During Migration

Monitor and manage the migration process with these checkpoints:

  • Content exported successfully - Verify all content types and relationships exported
  • Data transformation complete - Confirm all content converted to Sanity format
  • Assets uploaded - All media files transferred with metadata preserved
  • References verified - Content relationships and links maintained
  • Testing performed - Validate content accuracy, functionality, and performance
  • Issues documented - Record all problems and resolutions for future reference
  • Progress monitored - Track migration status against timeline and milestones
  • Quality gates passed - Ensure data quality and integrity standards met
  • Stakeholder communication - Keep team informed of progress and issues
  • Rollback plan tested - Verify ability to revert changes if needed

Post-Migration

Complete these tasks after content migration to ensure successful transition:

  • DNS updates made - Update domain configuration if required
  • Redirects configured - Implement URL redirects to maintain SEO value
  • SEO validated - Verify meta tags, structured data, and search visibility
  • Performance optimized - Tune queries, implement caching, and optimize delivery
  • Team fully trained - Complete training on all Sanity features and workflows
  • Documentation updated - Create user guides and technical documentation
  • Monitoring implemented - Set up performance and error tracking
  • Backup procedures established - Create regular backup schedules for Sanity content
  • Support process defined - Document how to handle issues and requests
  • Success metrics tracked - Monitor performance improvements and ROI

Conclusion

Migrating to Sanity represents a strategic investment in your content management capabilities that pays dividends through improved efficiency, flexibility, and developer experience. The migration journey, while complex, follows predictable patterns that can be managed with proper planning, tools, and expertise.

Planning prevents most issues by identifying challenges early and developing solutions before they become problems. Thorough content inventory, schema design, and testing phases significantly reduce migration risks.

Start small, scale gradually by beginning with non-critical content types and expanding migration scope as processes stabilize. This approach minimizes risk and allows learning and optimization throughout the migration.

Test thoroughly before launch to ensure data integrity, functionality, and performance meet expectations. Comprehensive validation catches issues before they impact users or business operations.

Invest in team training to maximize the benefits of Sanity's advanced features. Proper training ensures your content team can leverage Sanity's collaboration capabilities and improved editing experience.

Monitor post-launch performance to identify optimization opportunities and ensure the migration delivers expected benefits. Ongoing monitoring and optimization ensure continued success and ROI.

The journey to Sanity transforms not just your technology stack but how your teams create, manage, and deliver content. The combination of real-time collaboration, powerful querying, and flexible content modeling positions your organization for future digital content challenges and opportunities.

Need Expert Help?

Contact Digital Thrive to discuss your Sanity migration project. Our team has extensive experience with complex content migrations and can help ensure a smooth transition to Sanity's modern headless CMS. We also specialize in [web development](/services/web-development/) services that complement your migration project.

Sources

  1. Sanity Documentation - Migrating Content
  2. Sanity Documentation - Content Lake API
  3. Sanity Documentation - Portable Text
  4. Sanity Documentation - Image Pipeline
  5. WordPress REST API Handbook
  6. Contentful Management API Documentation
  7. [Digital Thrive Knowledge Base - Internal Sanity Implementation Experience]
  8. [Headless CMS Migration Best Practices - Industry Research]