Skip to content

Drupal Migrate API — Migrating Content from WordPress and Drupal

DodaTech Updated 2026-06-27 9 min read

In this tutorial, you'll learn how Drupal's Migrate API works: migrating content from WordPress to Drupal, performing Drupal-to-Drupal upgrades, configuring source plugins for different data formats, using Process plugins for data transformation, and writing Migration YAML files.

What You'll Learn

  • What the Migrate API is and how it powers Drupal migrations
  • The core migrate modules: migrate, migrate_drupal, migrate_drupal_ui
  • Migrating from WordPress using the WordPress Migrate module
  • Migration structure: source, process, and destination
  • Source plugins: EmbeddedSource, CSV, Database, File, URL, Xml
  • Process plugins: get, concat, explode, extract, dedupe_entity, skip_on_empty, callback, default_value
  • Destination plugins: entity:node, entity:user, entity:taxonomy_term, config:language
  • Migration YAML structure: id, label, source, process, destination
  • Running migrations with Drush: import, status, rollback
  • Migration groups and dependencies
  • Content transformation and field mapping

Why It Matters

Most Drupal projects involve migrating data from another platform. A client might have 10,000 blog posts in WordPress, 5,000 users in a custom CRM, or a legacy Drupal 7 site with years of content. Manually recreating this content is impractical. The Migrate API handles the transformation, deduplication, and validation automatically. It is the difference between a two-week migration project and a six-month manual copy-paste nightmare.

Real-World Use

A university with 15 years of content on WordPress decides to migrate to Drupal for better content modeling and security. They have 8,000 posts, 2,000 media files, 500 users, and 50 categories. Using the WordPress Migrate module, they write custom migrations that map WordPress posts to Drupal nodes, WordPress categories to Drupal taxonomy terms, and WordPress users to Drupal users. The migration runs in 30 minutes. Automated rollback lets them test repeatedly until the mapping is perfect.

Learning Path

flowchart LR
  A[Configuration Management] --> B[Migrate API]
  B --> C[Migrate Concepts]
  C --> D[Source Plugins]
  D --> E[Process Plugins]
  E --> F[Destination Plugins]
  F --> G[Migration YAML]
  G --> H[Running Migrations]
  H --> I[Performance and Go-Live]

What is the Migrate API?

The Migrate API is a framework for importing data into Drupal. It reads data from a source, transforms it through a series of process steps, and writes it to a destination.

Source (CSV, database, XML, JSON)
        |
        v
Process (transform, map, filter, deduplicate)
        |
        v
Destination (Drupal entities, configuration)

The API is in Drupal core, so no additional modules are required for basic migrations.

Migrate Modules

# Core migrate modules
drush pm:enable migrate              # Core migration framework
drush pm:enable migrate_drupal       # Drupal-to-Drupal migration
drush pm:enable migrate_drupal_ui    # Web UI for Drupal migrations

# Contributed modules
composer require drupal/migrate_plus        # Additional source/process plugins
composer require drupal/migrate_tools       # Drush commands for migrations
composer require drupal/migrate_source_csv   # CSV source plugin
composer require drupal/wordpress_migrate   # WordPress import

Migration YAML Structure

Every migration is defined by a YAML file. Here is a typical structure:

# migrations/migrate_plus.migration.import_articles.yml
id: import_articles
label: 'Import articles from WordPress'
migration_group: wordpress
migration_tags:
  - wordpress
  - content

source:
  plugin: wordpress_post
  constants:
    uid: 1
    status: 1

process:
  type:
    plugin: default_value
    default_value: article
  title: title
  body/value: body
  body/format:
    plugin: default_value
    default_value: basic_html
  field_tags:
    plugin: migration_lookup
    migration: import_tags
    source: tags
  uid: constants/uid
  status: constants/status
  created: post_date
  changed: post_modified

destination:
  plugin: entity:node

dependencies:
  enforced:
    module:
      - migrate_plus
      - wordpress_migrate

Migration Anatomy

  • id: Unique machine name for the migration
  • label: Human-readable description
  • source: Defines where data comes from and how to read it
  • process: Maps source fields to destination fields with optional transformations
  • destination: Defines what to create (node, user, taxonomy term)
  • dependencies: Other migrations that must run first

Source Plugins

Source plugins read data from various formats:

# CSV source
source:
  plugin: csv
  path: /var/www/migrations/users.csv
  header_row_count: 1
  keys:
    - id
  fields:
    id: 'User ID'
    name: 'Username'
    email: 'Email address'

# Database source
source:
  plugin: d7_node
  node_type: article

# Embedded source (hardcoded data)
source:
  plugin: embedded_data
  data_rows:
    -
      id: 1
      name: 'Static Page'
    -
      id: 2
      name: 'About Us'
  ids:
    id:
      type: integer

# URL/JSON source
source:
  plugin: url
  data_fetcher_plugin: http
  data_parser_plugin: json
  urls: 'https://api.example.com/posts'
  item_selector: /posts
  fields:
    -
      name: title
      label: Title
      selector: title
    -
      name: body
      label: Body
      selector: content
  ids:
    title:
      type: string

Process Plugins

Process plugins transform source data before saving to the destination:

process:
  # Direct mapping (source to destination as-is)
  title: source_title

  # Default value if source is empty
  type:
    plugin: default_value
    default_value: article

  # Concat multiple fields
  field_full_name:
    plugin: concat
    source:
      - first_name
      - last_name
    delimiter: ' '

  # Skip empty values
  field_summary:
    plugin: skip_on_empty
    method: process
    source: excerpt

  # Callback (PHP function)
  created:
    plugin: callback
    callable: strtotime
    source: post_date

  # Explode a string into an array
  field_tags:
    plugin: explode
    source: tags_string
    delimiter: ','

  # Extract a value from an array
  field_coordinates:
    plugin: extract
    source: location
    index:
      - lat
      - lng

  # Reference another migration
  field_author:
    plugin: migration_lookup
    migration: import_users
    source: author_id

  # Deduplicate entities
  field_category:
    plugin: dedupe_entity
    entity_type: taxonomy_term
    field: name
    source: category_name

  # Static map (replace values)
  field_region:
    plugin: static_map
    source: region_code
    map:
      NE: 'Northeast'
      SE: 'Southeast'
      MW: 'Midwest'
      W: 'West'
    bypass: true

Destination Plugins

Destination plugins define where the processed data is saved:

# Create nodes
destination:
  plugin: entity:node

# Create users
destination:
  plugin: entity:user

# Create taxonomy terms
destination:
  plugin: entity:taxonomy_term

# Create configuration
destination:
  plugin: config:language

# Entity:node with bundle specified in process
process:
  type:
    plugin: default_value
    default_value: article

Running Migrations with Drush

# List all migrations
drush migrate:status

# Show status of a specific migration
drush migrate:status import_articles

# Run a specific migration
drush migrate:import import_articles

# Run all migrations in a group
drush migrate:import wordpress

# Import with a limit (for testing)
drush migrate:import import_articles --limit=10

# Run migrations in parallel
drush migrate:import import_articles import_users --execute-dependencies

# Rollback a migration (removes imported data)
drush migrate:rollback import_articles

# Rollback all migrations in a group
drush migrate:rollback wordpress

# Show messages from a migration
drush migrate:messages import_articles

# Reset a stalled migration
drush migrate:reset import_articles

Migration Groups and Dependencies

Migrations can depend on each other:

# migrations/migrate_plus.migration.import_tags.yml
id: import_tags
label: 'Import WordPress categories as taxonomy terms'
migration_group: wordpress
source:
  plugin: wordpress_category
destination:
  plugin: entity:taxonomy_term
  default_bundle: tags

# migrations/migrate_plus.migration.import_articles.yml
id: import_articles
label: 'Import WordPress posts as articles'
migration_group: wordpress
source:
  plugin: wordpress_post
process:
  field_tags:
    plugin: migration_lookup
    migration: import_tags  # This migration must run first
    source: categories
destination:
  plugin: entity:node
  default_bundle: article

# Dependencies ensure correct order
# 1. import_tags runs first (creates taxonomy terms)
# 2. import_articles runs second (references the terms)

Custom Process Plugin

Create custom process logic:

<?php
namespace Drupal\my_module\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\migrate\MigrateSkipProcessException;

/**
 * Generate a URL-safe slug from a string.
 *
 * @MigrateProcessPlugin(
 *   id = "my_slugify"
 * )
 */
class Slugify extends ProcessPluginBase {

  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    if (empty($value)) {
      throw new MigrateSkipProcessException();
    }

    // Convert to lowercase, replace spaces with hyphens,
    // remove special characters
    $slug = mb_strtolower($value);
    $slug = preg_replace('/[^a-z0-9-]+/', '-', $slug);
    $slug = trim($slug, '-');

    return $slug;
  }
}
# Using the custom plugin
process:
  field_slug:
    plugin: my_slugify
    source: title

Content Transformation Strategies

Mapping Field Values

# WordPress post type to Drupal content type
process:
  type:
    plugin: static_map
    source: post_type
    map:
      post: article
      page: page
      news: news
    bypass: true  # Skip if no match

Combining Fields

# Combine first/last name into full name
process:
  field_full_name:
    plugin: concat
    source:
      - first_name
      - last_name
    delimiter: ' '

# Combine address fields
process:
  field_address:
    plugin: concat
    source:
      - street
      - city
      - zip
    delimiter: ', '

Date Conversion

# WordPress stores dates differently from Drupal
process:
  created:
    plugin: callback
    callable: strtotime
    source: post_date_gmt

Common Mistakes

  1. Running migrations without testing on a copy first: Always test migrations on a staging environment. Running directly on production can corrupt data or create duplicates.

  2. Not setting migration dependencies: If import_articles depends on import_tags but you do not declare the dependency, the articles migration might fail trying to reference tags that do not exist yet.

  3. Using wrong source field names: Migration YAML source fields must match the actual data columns. A typo like titlle instead of title silently produces empty fields.

  4. Forgetting to map required fields: If a destination field is required (like title or type) and you do not map a source field to it, the migration fails.

  5. Not using skip_on_empty for optional fields: If a source field is empty for some rows, the migration might fail. Use skip_on_empty to skip the field when the source value is missing.

Practice Questions

  1. What is the purpose of migration_lookup in a process plugin, and when would you use it?
  2. A WordPress post has a comma-separated tags field like "news, updates, announcements". How do you map this to a Drupal entity reference field?
  3. What happens when you run drush migrate:rollback on a migration that other migrations depend on?
  4. Challenge: Create a complete migration YAML file that reads from a CSV file containing: title, body, author_name, category, post_date, image_url. Map these to: a node of type "article", a user (create if not exists), a taxonomy term "category", and a media image (download from URL). Include at least three process plugins (migration_lookup, callback, skip_on_empty, default_value, concat).

FAQ

What is the difference between migrate and migrate_drupal modules?

The migrate module provides the core framework (source, process, destination plugins). The migrate_drupal module provides pre-written migrations for upgrading from Drupal 6 and 7. The migrate_drupal_ui module provides the web interface for these upgrades.

Can I migrate from WordPress to Drupal without paid modules?

Yes. The WordPress Migrate module (drupal.org/project/wordpress_migrate) is free and open source. It provides source plugins for WordPress XML exports and supports posts, pages, comments, users, categories, tags, media, and menus.

What happens if a migration fails mid-way?

Drupal tracks which items were successfully imported. When you fix the issue and re-run, it skips already-imported items (by default). Use --update to re-import existing items, or drush migrate:rollback to start fresh.

How do I migrate media files?

Use the download or file source plugins. For WordPress, the WordPress Migrate module handles media attachments. For custom migrations, use the plugin: file_copy or plugin: download process plugins to download files and create file entities.

Can I run a migration on a production site?

Yes, but with caution. Always test on staging first. Run the migration during low-traffic periods. Use --limit to test small batches. Have a rollback plan (drush migrate:rollback). Monitor server resources during large migrations.

Mini Project

Goal: Migrate a WordPress site to Drupal using the Migrate API.

  1. Export your WordPress content as XML (Tools > Export)
  2. Set up a fresh Drupal installation
  3. Install the WordPress Migrate module and its dependencies
  4. Create a custom migration module that:
    • Migrates WordPress posts to Drupal articles
    • Maps WordPress categories to Drupal taxonomy terms
    • Maps WordPress tags to Drupal tags
    • Maps WordPress authors to Drupal users (create new if missing)
    • Downloads featured images and attaches them to articles
  5. Run the migration and verify: article count, taxonomy terms, user accounts, media files
  6. Write a YAML migration that imports additional data from a CSV file containing user ratings for each post

What's Next

Now that you understand migrations, proceed to performance optimization to learn Caching strategies, CDN integration, and server tuning. Then explore backups and maintenance to keep your Drupal site healthy.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro