Migrating Drupal articles to Storyblok
Storyblok is the first headless CMS that works for developers & marketers alike.
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:
- Export content from Drupal using Drush.
- Transform and map Drupal fields to Storyblok’s structure.
- Import content and assets into Storyblok using the Management API.
- 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.
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 title
, body
, and image
) to keys that will be used in Storyblok.
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.
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 API, Asset 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.
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.
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.
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.
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.
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:
- 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
- Use the
- Follow the prompts:
- The generator will ask you a few questions. Here’s an example of how to answer them:
- Module machine name:
storyblok_exporter
- Module name:
Storyblok Exporter
- Class name:
StoryblokExporterCommands
- Module machine name:
- This will create a new Drush command file in the correct location:
./web/modules/storyblok_exporter/
- The generator will ask you a few questions. Here’s an example of how to answer them:
- Replace the generated code:
- Open the generated
StoryblokExporterCommands.php
file and replace its content with the script we’ve been discussing.
- Open the generated
- Enable the module:
- Enable the module using Drush:
drush en storyblok_exporter
- Enable the module using Drush:
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
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
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!