Demo-Ready Drupal: Streamlining Content Creation with Migrate

A list of blog post titles created with the tutorial in the blog post

In my previous blog post, I introduced a convenient composer script for effortless project reinstallation. However, a crucial element missing from this setup is the swift availability of content to test new features or assess the site structure.

If you're following along, you're likely already familiar with the command: composer project:reinstall

If not, check out this post for a quick guide:

Importing content in Drupal can be achieved through various methods, but my preferred approach involves leveraging the Migrate module and its numerous extensions. This method provides flexibility, scalability, and easy extension through custom plugins.

The Goal:

I want a quick way to create a lot of blog posts without using a database as source with text and images. I should be able to easily modify and share this demo setup.

The Setup:

This blog post's setup mirrors the structure of my personal blog, featuring a content type "Blog post." This content type includes an introduction text, a field for the blog post, a main image, and a reference to a channel where the post will be listed and organized.

Several modules are employed in this demo setup:

Creating the Demo Module:

You can explore and download the module from here:

Please note: Note that the module follows my structure and requires modification for your specific purpose.

The main concept revolves around migration definitions for the desired content and providing JSON files containing the demo content.

Example structure of the module's directories:

The image shows subdirectories for artifacts and migration definition files


Migration Definitions:

In this example I have 3 migrations: The demo_blog_posts migration requires the demo_channel and demo_media_images migration to run first since we use migrate_lookup to reference the created entities in the blog posts. The json files inside the artifacts folder contains the demo content where manually created the structure and used ChatGPT to create the actual content.

The content of the json files always follow a similar structure. Inside the json there is a data array which will act as the data source for the migration. 

The source part of the blog posts migration

The data_parser_plugin json allows looping through the array, where each entry acts as a row for migration. With the help of the system_stream_wrapper module, the JSON file is packaged with the module.

 plugin: url
 data_fetcher_plugin: file
 data_parser_plugin: json
   - 'module://cyb_demo/artifacts/blog_posts.json'
 track_changes: true
 item_selector: data
   - name: name
     label: 'The name of the blog post'
     selector: name
   - name: field_introduction_text
     label: 'The field_introduction_text of the blog post'
     selector: field_introduction_text
   - name: field_blog_post_content
     label: 'The field_blog_post_content of the blog post'
     selector: field_blog_post_content
   - name: field_channel
     label: 'The field_channel of the blog post'
     selector: field_channel
   - name: field_main_image
     label: 'The id of the media image'
     selector: field_main_image

Looking Up the Channel by Name and Image by ID:

Examples of using migration_lookup for referencing created entities in blog posts.

Excerpt from demo_blog_posts.yml:

   - plugin: skip_on_empty
     source: field_channel
     method: process
   - plugin: sub_process
     source: field_channel
         - plugin: migration_lookup
             - demo_channel
           source: name
   - plugin: skip_on_empty
     source: field_main_image
     method: process
   - plugin: migration_lookup
       - demo_media_images
       - field_main_image

Excerpt of the blog_posts.json file:

 "data": [
     "name": "The Adventures of Lorem Ipsum",
     "field_introduction_text": "Join Lorem Ipsum on a whimsical journey through the world of placeholder text.",
     "field_blog_post_content": "<p>\n This is a hilarious tale of <strong>Lorem Ipsum</strong>, the unsung hero of the text world, and his escapades in the land of placeholder content. Strap in for a <u>rollercoaster</u> of laughs!</p><p>\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.</p>",
     "field_channel": [
         "name": "Demo Channel A"
     "field_main_image": "1"
     "name": "The Secret Life of Placeholder Text",
     "field_introduction_text": "Unveiling the hidden secrets of Lorem Ipsum and its clandestine activities.",
     "field_blog_post_content": "<p>\n Did you know that <strong>Lorem Ipsum</strong> has a secret life? Dive into the mysterious world of placeholder text and discover the surprising adventures that unfold behind the scenes!</p>",
     "field_channel": [
         "name": "Demo Channel A"
     "field_main_image": "2"

Using Migrate to Import External Images:

With the Migrate File module, importing placeholder images is streamlined. The image_import plugin handles downloading the image file into the defined destination folder.

   plugin: image_import
   source: image_url
   destination: 'constants/file_destination'
   title: title
   alt: alt_text

Excerpt of the media_images.json:

 "data": [
     "id": 1,
     "file_url": "",
     "alt_text": "Lorem Ipsum 1",
     "title": "Lorem Ipsum Image 1"
     "id": 2,
     "file_url": "",
     "alt_text": "Lorem Ipsum 2",
     "title": "Lorem Ipsum Image 2"

Importing the content

With the module in place I can simply execute the following command to have my content imported:

drush mim --group=demo 

[notice] Processed 2 items (2 created, 0 updated, 0 failed, 0 ignored) - done with 'demo_channel'
[notice] Processed 20 items (20 created, 0 updated, 0 failed, 0 ignored) - done with 'demo_media_images'
[notice] Processed 20 items (20 created, 0 updated, 0 failed, 0 ignored) - done with 'demo_blog_posts'

This command imports all migrations in the "demo" group, resolving dependencies to ensure that dependent migrations run before demo_blog_posts.

My newly created blog posts all have different content, are assigned to a channel and also have an image imported.

And with this one-liner I can reinstall and create demo content very quickly:

ddev composer project:reinstall && ddev drush mim --group=demo



Leveraging Migrate for importing demo content proves to be quick, easy, and extendable. Adding new fields later poses no problem, and the knowledge gained about Migrate benefits developers for future tasks involving project migrations to Drupal.

I'd love to hear your thoughts and learn about alternative ways you import demo content!



Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.