When migrating content to Drupal from another source (CSV, Drupal 7, external systems, etc.), it’s very common to deal with images and files. Since Drupal 8 and especially in Drupal 9–11, Media entities are the recommended way to handle images and files instead of using image/file fields directly.
This article explains how to migrate images into Media entities using Migrate, step by step, and then reference those Media entities from content. The examples are based on a CSV source, but the same concepts apply to other sources.
Why use Media instead of file/image fields?
The Media module has been in Drupal core for years now and is mature and stable in Drupal 10 and Drupal 11. Using Media entities gives you:
- Reusable images and files across content
- A centralized Media Library
- Extra fields on media (copyright, author, alt defaults, etc.)
- Better editorial experience
At Omitsis, using Media is now, and for years, a default standard for Drupal projects.
The downside is that migrating to Media is slightly more complex than migrating directly to file fields. This post focuses on solving that.
Prerequisites
This article assumes basic knowledge of the Migrate API.
If you’re new to migrations, it’s recommended to first read an introduction on migrating from CSV using the Migrate module.
We will not use contrib helpers like migrate_files_to_media. Instead, we’ll rely only on core/contrib migrate plugins so you fully understand what’s happening.
Required modules (Drupal 10 / 11)
Install the required modules via Composer:
composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csv drupal/config_devel
Enable them:
drush en -y migrate migrate_plus migrate_tools migrate_source_csv config_devel
config_develis optional but very convenient during development to auto-install migration YAML files.
Migration strategy overview
We’ll split the process into three migrations:
- Files migration – copy image files into Drupal and create
fileentities - Media migration – create
media:imageentities referencing those files - Content migration – create nodes referencing the Media entities
This separation makes migrations easier to debug, rerun and reuse.
1. Migrating image files
First, we migrate the physical image files and create file entities.
Migration file: migrate_plus.migration.migrate_files.yml
id: migrate_files
label: Migrate Files
source:
plugin: csv
path: modules/custom/custom_migrate/data/products.csv
delimiter: ','
enclosure: '"'
header_offset: 0
ids:
- code
fields:
0:
name: cat
label: 'Category'
1:
name: code
label: 'Product code'
2:
name: title
label: 'Title'
3:
name: image
label: 'Image'
constants:
file_source_uri: 'public://import/source/images'
file_dest_uri: 'public://imports/dest/images'
process:
file_source:
- plugin: concat
delimiter: '/'
source:
- constants/file_source_uri
- image
- plugin: urlencode
file_dest:
- plugin: concat
delimiter: '/'
source:
- constants/file_dest_uri
- image
- plugin: urlencode
uri:
plugin: file_copy
source:
- '@file_source'
- '@file_dest'
uid:
plugin: default_value
default_value: 1
destination:
plugin: entity:file
Key points
- Constants define source and destination directories
file_copycopies the image and creates a file entity- The CSV only contains the image filename, not full paths
file_copysupports overwrite and reuse options if needed
At this point, Drupal knows about the files, but not yet as Media.
2. Creating Media entities from files
Now we convert the files into media:image entities.
Migration file: migrate_plus.migration.media_images.yml
id: media_images
label: Media images
source:
plugin: csv
path: modules/custom/custom_migrate/data/products.csv
delimiter: ','
enclosure: '"'
header_offset: 0
ids:
- code
fields:
0:
name: cat
label: 'Category'
1:
name: code
label: 'Product code'
2:
name: title
label: 'Title'
3:
name: image
label: 'Image'
process:
# Avoid creating duplicate media entities
skip:
- plugin: entity_lookup
entity_type: media
bundle: image
value_key: name
source: image
ignore_case: true
access_check: false
- plugin: skip_on_not_empty
method: row
message: 'Skipping already existing media'
field_media_image/target_id:
plugin: migration_lookup
migration: migrate_files
source: code
no_stub: true
uid:
plugin: default_value
default_value: 1
destination:
plugin: entity:media
default_bundle: image
migration_dependencies:
optional:
- migrate_files
Key points
- Media reuse is critical: one image = one Media entity
entity_lookupchecks if a Media with the same name already existsmigration_lookupretrieves thefidfrom the previous migrationdefault_bundleis set toimage
This approach prevents duplicate media entities when the same image appears multiple times in the CSV.
3. Migrating content and referencing Media
Finally, we migrate the content (nodes) and reference the Media entities.
Migration file: migrate_plus.migration.products.yml
id: products
label: Import products
source:
plugin: csv
path: modules/custom/custom_migrate/data/products.csv
delimiter: ','
enclosure: '"'
header_offset: 0
ids:
- code
fields:
0:
name: cat
label: 'Category'
1:
name: code
label: 'Product code'
2:
name: title
label: 'Title'
3:
name: image
label: 'Image'
process:
field_cat:
plugin: entity_lookup
entity_type: taxonomy_term
bundle: cat_producto
value_key: name
source: cat
ignore_case: true
field_producto_codigo: code
title:
- plugin: callback
callable: mb_strtolower
source: title
- plugin: callback
callable: ucfirst
type:
plugin: default_value
default_value: producto
field_imagen/target_id:
plugin: entity_lookup
entity_type: media
bundle: image
value_key: name
source: image
ignore_case: true
access_check: false
destination:
plugin: entity:node
migration_dependencies:
optional:
- migrate_files
- media_images
Key points
- Media entities are referenced using
entity_lookup - Lookup is done by the media name (image filename)
- This keeps content migrations clean and independent
Registering migrations automatically
Using config_devel, we can auto-install migrations by declaring them in the module .info.yml:
config_devel:
install:
- migrate_plus.migration.migrate_files
- migrate_plus.migration.media_images
- migrate_plus.migration.products
Running the migrations
Reload migration configuration:
drush cdi custom_migrate
Run migrations in order:
drush mim migrate_files
drush mim media_images
drush mim products
Final result
- Files are copied and registered
- Media entities are created and reused
- Nodes reference Media correctly
This approach is stable, reusable and future-proof for Drupal 10 and Drupal 11 projects, and works equally well with CSV, SQL or Drupal-to-Drupal migrations.