This is a preview of the Storyblok Website with Draft Content

Migrating Drupal articles to Storyblok

Migrating from a legacy CMS like Drupal to a modern headless CMS such as Storyblok can seem challenging, but with the right tools, it becomes a manageable and rewarding process. In this article, we’ll guide you through migrating a simple blog from Drupal to Storyblok using a custom Drush command and the brand-new PHP client for Storyblok’s Management API .

You can find the complete code for the Drush command here .

Why migrate to Storyblok?

Drupal has been a reliable CMS for years, but Storyblok offers modern advantages :

  • Improved Performance: Decoupling the frontend and backend leads to faster page loads and better scalability.
  • Flexibility: Use any frontend framework (React, Vue, Next.js, etc.) to build your site or app.
  • Developer Experience: Storyblok’s intuitive API and visual editor streamline collaboration between developers and content creators.
  • Future-Proofing: A headless CMS ensures your content is ready for emerging technologies like IoT and AR/VR.

Objective of the article

This guide will walk you through migrating a simple blog from Drupal to Storyblok. You’ll learn how to:

  1. Export content from Drupal using Drush.
  2. Transform and map Drupal fields to Storyblok’s structure.
  3. Import content and assets into Storyblok using the Management API.
  4. Customize the migration process to fit your needs.

Understanding the migration process

Migrating content from Drupal to Storyblok involves several key steps. In this section, we’ll break down the process and explain how the script handles each step. We’ll also highlight relevant snippets from the script to give you a clear understanding of how it works.

What is being migrated?

For this example, we’re migrating a simple blog with a single content type: article. Each article in Drupal has three fields:

  • Title: The title of the blog post.
  • Body: The main content of the post (markup).
  • Image: An optional image associated with the post.

The goal is to export these articles from Drupal, transform the data into a format that Storyblok understands, and then import them into Storyblok as stories.

Key steps in the migration

Exporting Content from Drupal Using the Drupal Entity Query

The first step is to query and export the content from Drupal. The script uses Drupal’s Entity Query  to retrieve all published nodes of the article content type. Here’s the relevant snippet:

        
      $query = \\Drupal::entityQuery('node')
  ->accessCheck(FALSE)
  ->condition('status', 1)
  ->condition('type', self::COMPONENT_TYPE);

if ($options['limit']) {
  $query->range(0, $options['limit']);
}

$nids = $query->execute();
    

This code queries all published nodes of type "article" (defined as self::COMPONENT_TYPE). If a limit option is provided to the command, it restricts the number of nodes exported.

Transforming content to fit Storyblok’s structure

Once the content is retrieved, it needs to be transformed into a format that Storyblok can understand. The script maps Drupal fields to Storyblok fields and prepares the data for import. Here’s how the transformation happens:

        
      $toBeExported[] = [
  'title' => $node->label(),
  'body' => $node->get('body')->value,
  'created_date' => $this->dateFormatter->format($node->getCreatedTime(), 'custom', 'Y-m-d H:i:s'),
  'author' => $node->getOwner()->label(),
  'image' => $this->getImage($node),
  'tags' => $this->getTags($node),
];
    

This code creates an array for each node, mapping Drupal fields (like titlebody, and image) to keys that will be used in Storyblok.

hint:

Storyblok requires content to be structured in a specific way, so this step ensures compatibility.

Importing content into Storyblok using the Management API Client

The final step is to import the transformed content into Storyblok. The script uses the new Storyblok Management API PHP Client to create stories and upload assets.

Here’s a snippet showing how a story is created:

        
      $storyContent = new StoryblokData();
$storyContent->set('component', self::COMPONENT_TYPE);
$storyContent->set('title', $item['title']);
$storyContent->set('body', $item['body']);
$storyContent->set('image.id', $image->id());
$storyContent->set('image.fieldtype', 'asset');
$storyContent->set('image.filename', $image->filename());

$story = new StoryData();
$story->setName($item['title']);
$story->setSlug($this->generateSlug($item['title']));
$story->setContentType($this::COMPONENT_TYPE);
$story->setCreatedAt($item['created_date']);
$story->setContent($storyContent->toArray());
$story->set('tag_list', $item['tags']);

$response = $storyApi->create($story);

    

This code creates a new Storyblok story using the transformed data. It sets the story’s title, body, image, and other metadata, then sends it to Storyblok via the Management API.

As you can see, we are also setting the image field of our Story using a $image that we defined previously. This is an asset that we actually uploaded to Storyblok, so that we can now link it to the Story. We’ll see in detail how we can do that.

Exploring the script

Let's take a closer look at some other relevant part of the script, to let you have a deeper understanding of the process and also a break down of the new Management API Client.

Setting up the Storyblok Client

Before the migration process begins, the script initializes the Storyblok Management API client using the user’s OAuth token and space ID. Here’s how the initialization happens:

        
      $this->storyblokClient = ManagementApiClient::init(Settings::get('STORYBLOK_OAUTH_TOKEN'));
$this->spaceId = Settings::get('STORYBLOK_SPACE_ID');
    

This code initializes the Storyblok client using the OAuth token and space ID provided in the Drupal settings. These credentials are required to authenticate and interact with Storyblok’s Management API.

Once the client is initialized, we need to initialize the specific classes that interact with API endpoints:

        
      $storyApi = $this->storyblokClient->storyApi($this->spaceId);
$assetApi = $this->storyblokClient->assetApi($this->spaceId);
$tagApi = $this->storyblokClient->tagApi($this->spaceId);
    

These lines initialize the Story APIAsset API, and Tag API instances, which are used to create stories, upload assets, and manage tags, respectively.

Uploading assets

If an article includes an image, the script extracts and uploads it to Storyblok’s asset management system and links it to the mapped field on the story. To get the image from the node, we wrapped the Drupal API in a custom method getImage($node), which is a helper built upon $node->get('field_image') that you can customize to match your entity.

The asset uploading process happens in a function that returns an $image object. Here's how it works, with error handling stripped out:

        
      private function uploadImageToStoryblok(AssetApi $assetApi, array $image): ?AssetData {
  $filePath = $this->fileSystem->realpath($image['uri']);
  $response = $assetApi->upload($filePath);

  if ($response->isOk()) {
    return $response->data();
  }
}
    

This function takes an image from Drupal, uploads it to Storyblok using the Asset API, and returns the uploaded asset’s data (e.g., id and filename).

Assets like images need to be stored in Storyblok’s system so they can be linked to stories. This function ensures that media files are properly uploaded.

Creating tags

The script also includes functionality for handling tags. Here’s a glimpse of how tags are retrieved from Drupal:

        
      private function getTags(NodeInterface $node): array {
  $tags = [];
  foreach ($node->get('field_tags') as $tag) {
    if ($tag->entity) {
      $tags[] = $tag->entity->label();
    }
  }
  return $tags;
}
    

This function retrieves tags associated with a Drupal node and prepares them for migration. Ultimately, a tag is a string, so we need to create that tag in Storyblok and then link it to the Story.

The tag creation process happens in a dedicated function, here’s how it works without logging and error handling:

        
      private function createTagsInStoryblok(TagApi $tagApi, array $tags): void {
  foreach ($tags as $tag) {
    $response = $tagApi->create($tag);
  }
}
    

This process ensures that all tags are migrated to Storyblok, even the ones associated with unpublished or deleted articles.

To associate a list of tags to a Story, you can simply set the value:

        
      $story->set('tag_list', $item['tags']);
    

The argument is an array of strings, and if an item of this list is not present on Storyblok, it will be created. So the previous steps of creating tags on Storyblok first are optional, but they ensure consistency in the migration.

warn:

You will see errors when the client tries to create a tag that already exists, but the process will continue.

Error handling

The script includes error handling to ensure that any issues during the migration process are logged and addressed. Just an example:

        
      try {
  $response = $storyApi->create($story);
  if ($response->isOk()) {
    $migratedCount++;
    $this->logger()->success("Successfully migrated: " . $item['title']);
  } else {
    $this->logger()->error("Failed to migrate: " . $item['title'] . ". Error: " . $response->getErrorMessage());
  }
} catch (\\Exception $e) {
  $this->logger()->error("Error migrating: " . $item['title'] . ". Error: " . $e->getMessage());
}
    

This code attempts to create a story in Storyblok and logs the result. If an error occurs, it logs the error message for debugging.

hint:

Error handling ensures that the migration process can continue even if some content fails to migrate, and it provides visibility into what went wrong.

Customizing the script for your needs

The script we’ve explored so far is designed to migrate a simple blog with a single content type (article) and a few basic fields (title, body, image, and tags). However, every Drupal site is unique, and you may need to customize the script to handle more complex scenarios. In this section, we’ll discuss how you can adapt the script to fit your specific needs.

Modifying the Content Type

If your Drupal site has multiple content types or additional fields, you’ll need to modify the script to handle them. Here’s how you can do that.

Adding New Fields

To migrate additional fields, you’ll need to update the transformation logic in the script. For example, if your article content type has a new field called subtitle, you can add it to the transformation array like this:

        
      $toBeExported[] = [
  'title' => $node->label(),
  'subtitle' => $node->get('field_subtitle')->value, // New field
  'body' => $node->get('body')->value,
  ...
];
    

This code adds a new field (subtitle) to the array that will be used to create stories in Storyblok. Now we can add this to our Story:

        
      $storyContent = new StoryblokData();
$storyContent->set('component', self::COMPONENT_TYPE);
$storyContent->set('title', $item['title']);
$storyContent->set('subtitle', $item['subtitle']); // Add the subtitle field
...
    

Handling multiple content types

If your site has multiple content types (e.g., article, news, event), you can modify the script to handle each type separately. For example, you can add a condition to check the content type and apply different transformations:

        
      if ($node->bundle() === 'article') {
  $toBeExported[] = [
    'content_type' => $node->bundle(), // Add the content type
    'title' => $node->label(),
    'body' => $node->get('body')->value,
    // Article-specific fields
  ];
} elseif ($node->bundle() === 'news') {
  $toBeExported[] = [
    'content_type' => $node->bundle(),
    'title' => $node->label(),
    'summary' => $node->get('field_summary')->value,
    // News-specific fields
  ];
}
    

This code checks the content type of each node and applies the appropriate transformation. Furthermore, this adds the content_type field to the exported data, indicating whether the node is an article, news, or another content type. This allows the migrateToStoryblok method to identify the content type and apply the appropriate transformations.

In the migrateToStoryblok method, you can then use the content_type field to determine which fields to include in the Storyblok story and call the specific ->set() method to add your fields.

Batch processing and optimizations

For large sites with thousands of nodes, you may need to optimize the script to handle the migration in batches. Here’s how you can implement batch processing:

Example: batch processing

You can modify the script to process nodes in batches, reducing memory usage and improving performance:

        
      $batchSize = 50; // Process 50 nodes at a time
$nids = $query->execute();
$batches = array_chunk($nids, $batchSize);

foreach ($batches as $batch) {
  $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($batch);
  // Process nodes in this batch
  // ...
  $migrated = $this->migrateToStoryblok($toBeExported);
  
  // Optional: Add a small delay between batches to avoid rate limiting
  if ($index < count($chunks) - 1) {
    sleep(1);
  }
}
    

This code processes nodes in batches of 50, reducing memory usage and improving performance.

hint:

Batch processing is essential for migrating large sites without overwhelming server resources.

Following the progress of the new Management API Client

As the new Management API PHP Client evolves, you can expect enhancements like improved batch processing, better error handling, and more. Keep an eye on the official Storyblok documentation and GitHub repository for updates.

Running the Script with Drush

This Drush command was created and tested with Drupal 11.x on php 8.3, so it may not work on older versions. If you have a Drupal version between 8.x and 10.x, we encourage you to test it and report any problems you may find.

The only other requirement is that you have Drush installed, which you can do by running composer require drush/drush from the root of your Drupal installation.

Before executing the migration command, you need to create a custom Drush command file using the drush generate command. Here’s how to do that:

Generate the Drush command file

To create a custom Drush command file, follow these steps:

  1. Run the Drush generator:
    • Use the drush generate drush:command-file command to create the necessary folder structure and files:
              
            drush generate drush:command-file
          
  2. Follow the prompts:
    • The generator will ask you a few questions. Here’s an example of how to answer them:
      • Module machine namestoryblok_exporter
      • Module nameStoryblok Exporter
      • Class nameStoryblokExporterCommands
    • This will create a new Drush command file in the correct location:
              
            ./web/modules/storyblok_exporter/
          
  3. Replace the generated code:
    • Open the generated StoryblokExporterCommands.php file and replace its content with the script we’ve been discussing.
  4. Enable the module:
    • Enable the module using Drush:
              
            drush en storyblok_exporter
          

Set environment variables

The script relies on two environment variables to authenticate with Storyblok’s Management API:

  • STORYBLOK_OAUTH_TOKEN: Your Storyblok OAuth token.
  • STORYBLOK_SPACE_ID: The ID of your Storyblok space.

Set these variables in your Drupal settings.php file:

        
      $settings['STORYBLOK_OAUTH_TOKEN'] = 'your-oauth-token';
$settings['STORYBLOK_SPACE_ID'] = 'your-space-id';

    

Execute the command

Once the module is set up and the environment variables are configured, you can run the migration script using Drush:

        
      drush storyblok_exporter:export
    

Optional parameters:

  • Use the -limit option to restrict the number of nodes exported. For example:
            
          drush storyblok_exporter:export --limit=10
        
hint:

The command storyblok_exporter is also shortened in sbe.

Interpreting the output

When you run the script, it will provide output in the terminal, including success messages, error logs, and progress updates. Here’s what to expect:

  • Success messages:
    Successfully migrated: Example Article Title
    Exported 5 articles to Storyblok.
    Content successfully exported 🎉
    
  • Error logs:
    Error migrating: Example Article Title. Error: Failed to upload image.
    Failed to migrate: Example Article Title. Error: Invalid API response.
    
  • Progress updates:
    Uploading image: example-image.jpg
    Creating tags: tag1, tag2
    

Verifying the migration

After running the script, verify that the content was successfully migrated to Storyblok:

  1. Log in to Storyblok and navigate to the Content section.
  2. Check the stories, assets, and tags to ensure they match the content exported from Drupal.

Conclusion

In this article, we’ve walked you through the steps to migrate a simple blog from Drupal to Storyblok using a custom Drush command and a new PHP client for Storyblok’s Management API. Migrating from Drupal to Storyblok is not just about moving content: it’s about embracing a modern, flexible, and future-proof approach to content management. With the tools and knowledge provided in this article, you’re well-equipped to make the transition smoothly and efficiently.

Please find the complete code for the migration script used in this article in our dedicated GitHub repo .

If you’re considering migrating from Drupal to Storyblok, now is the perfect time to give it a try! The script and steps outlined in this article provide a solid foundation for your migration journey. Whether you’re migrating a small blog or a large-scale website, Storyblok’s flexibility and powerful API make it an excellent choice for your next project.

We hope this guide has been helpful, and we encourage you to try the migration process for yourself. If you have any questions or feedback, feel free to reach out to the Storyblok community, and if you find any issue with the new Management API PHP Client, please open an issue on its GitHub project .

Happy migrating, and welcome to the world of Joyful Headless with Storyblok!

Author

Edoardo Dusi

Edoardo Dusi

Edoardo is a Senior Developer Relations Engineer at Storyblok who has a strong technical background, is passionate about creating and sharing content that educates and inspires others, and enjoys connecting with the developer community and promoting the benefits of open source software.