Content and Component Migrations with storyblok-migrate

Contents

First and foremost Storyblok is known for its great UI. The wonderful Visual Editor is one of the features which sets Storyblok apart from its competitors. But also the quick and easy way how you can make updates to the schema of your content models via the UI directly while editing the content of your site can make the life of content editors a lot easier.

Editing the schema directly in the content editor UI.

Although it is nice to have the option to manage the structure of your site via the UI, especially with bigger projects with a lot of people working on the same site, it can become tedious to keep track of all the changes to the schema of the site when relying solely on the UI for managing your content types. In such cases it’s often times favorable to store the schema of you content types in code to be able to keep track of changes via version control.

Additionally, sometimes you might want to refactor the structure of your site and its components. In a lot of traditional content management systems and frameworks there is the concept of migrations, either directly built in from the start or available via third party plugins. For example the PHP framework Laravel has migrations built in. The CMS Drupal has update hooks to serve a similar purpose.

With the storyblok-migrate npm package it is now possible to define content migrations which are automatically applied to your stories for Storyblok. This makes it straight forward to refactor certain content types and to rename fields or even changing the type of a field.

Installing storyblok-migrate

storyblok-migrate is available as a npm package. You can install it as a dependency of your project, by running the following command in your CLI of choice.

npm install --save-dev storyblok-migrate

After installing the dependency you have to create a new directory named storyblok at the root level of your project (but you can change the name and the location of this directory via an optional configuration file if you want).

Configuration

In the following code snippet you can see an example storyblok.config.js file with all its options set to its default values.

// storyblok.config.js
module.exports = {
  componentDirectory: 'storyblok',
  backupDirectory: 'backup',
  dryRun: process.argv.includes('--dry-run'),
  oauthToken: process.env.STORYBLOK_OAUTH_TOKEN,
  spaceId: process.env.STORYBLOK_SPACE_ID,
};

All the configuration options of storyblok-migrate are optional. But you have to make sure that at least the STORYBLOK_OAUTH_TOKEN and your STORYBLOK_SPACE_ID environment variables are set correctly. It is recommended to do this via an .env file which you create at the root of your project.

STORYBLOK_OAUTH_TOKEN=AbCd3FghiJklmnOpqRstuvWxyz-4444-12345678901234567890
STORYBLOK_SPACE_ID=16512

At least if your source code is public, you must keep your OAuth token private! I highly recommend you to add the .env file to your .gitignore list if you’re using Git.

The dryRun option makes it possible to see what would happen when running a certain command without actually changing anything in Storyblok. You can either append --dry-run when running a command or setting this option to true to run your commands in dry run mode.

Defining components in code

One of the core principles of storyblok-migrate is that every component is defined in code. This means you don’t use the Storyblok UI for creating new components or updating existing ones. Ideally, you only make changes to your components in one place: the component definitions in your code.

// storyblok/article.js
const metaImage = require('./meta_image');

module.exports = {
  display_name: 'Article',
  is_nestable: false,
  is_root: true,
  name: 'article',
  schema: {
    title: {
      pos: 0,
      type: 'text',
    },
    image: {
      component_whitelist: [metaImage.name],
      maximum: 1,
      pos: 10,
      restrict_components: true,
      type: 'bloks',
    },
  },
};

Above you can see an example Storyblok component as it looks when defined as a JavaScript object. You may wonder why you should even consider abandoning the convenience of the great Storyblok interface for a seemingly complicated approach like this. But as we will see, this method has a number of advantages.

For example: if you change the name of a field or if you add a new field, you most likely also have to update the code of your website. If you have specified the schema of your components in your code base as well, all of your changes are made in one place. This makes it a lot easier to track and sometimes even to revert changes if something unforeseen happens.

Additionally you get all the other benefits of version control. It’s much easier for multiple developers to work on the same site. All changes to the structure of the site are reflected in a commit to the code base. That way it is much less likely that a change to the schema of the site breaks the code of another developer. It happened more than once in my career that somebody accidentally deleted some field or even a whole content type. When having them all stored in code and even disallow changing the schema in the UI this problem is a problem from the past.

Running component migrations

After you have defined all your content types in code you’re ready to run the migration script in order to push them to Storyblok.

npx storyblok-migrate

If you run npx storyblok-migrate without any parameters you start the guided UI mode which asks you some questions about what you want to do.

Running storyblok-migrate in UI mode.

But you can also use parameters to immediately run a certain task.

# Run component migrations only for specific components.
npx storyblok-migrate --component-migrations --components article,meta_image

Content migrations

The second vital part of storyblok-migrate is the possibility to run content migrations. What this means is that you can update the content structure of your website whenever you change the schema of one of your components. Imagine you change the name of a textfield – content migrations make it possible to update all of your content, move the data from the old field to the new one and delete the old one.

// storyblok/article.js
const metaImage = require('./meta_image');

module.exports = {
  display_name: 'Article',
  is_nestable: false,
  is_root: true,
  migrations: [
    // Replace `headline` field with new `title` field.
    ({ content }) => {
      // If the `headline` field was already delted
      // earlier, don't run this migration again.
      if (!content.headline) return;

      content.title = content.headline;
      delete content.headline;
    },
  ],
  name: 'article',
  schema: {
    title: {
      pos: 0,
      type: 'text',
    },
    image: {
      component_whitelist: [metaImage.name],
      maximum: 1,
      pos: 10,
      restrict_components: true,
      type: 'bloks',
    },
  },
};

Above you can see how you can define a content migration function inside of the component definition file. All the functions specified in the migrations array are run one after another. There is no restriction what those migration functions are allowed to do. They get passed in an object with the content of the current component and you’re free to modify it in whichever way you want. It is recommended to add a check at the beginning of your migration function to make sure to only run it when its needed.

Helper scripts

storyblok-migrate also has some built in helper scripts for backing up your content and exporting components from Storyblok.

# Create a backup of all components and stories.
npx storyblok-backup --components --stories

By default backups are stored in a directory named backup but you can change the location by changing the backupDirectory configuration option.

# Export all components.
npx storyblok-component-export

You can use the command above to initially get started with using storyblok-migrate. This will automatically generate your component definitions in storyblok based on the schema of the components you’ve created in Storyblok. But be careful: this command replaces existing component definitions you’ve already created earlier. But you can specify which components you want to export: npx storyblok-component-export article,product.

Roadmap

Currently storyblok-migrate, although 100% functional, is still in a testing phase. Some of its features might not be very well optimized for very large scale sites with hundreds or even thousands of stories. The next steps on the roadmap to v1.0.0 are optimizations and new features like versioning of content migrations, rolling back after a faulty migration and importing content from third party sources like other content management systems or even feeds.

Wrapping it up

Being able to automatically migrate your content and field types makes it easier to work on large scale websites where multiple developers work on the same code base and multiple persons are responsible to work on the structure of the site.

But it also comes with a price: it makes it much harder, or, depending on the skills of the person in charge, maybe even impossible, for a content editor to quickly make a change to the schema of the site. Oftentimes this is a trade-off worth taking in order to guarantee that this person doesn’t accidentally break the site. But in other situations it might cripple the content editors in their work.

If you decide to use storyblok-migrate please let me know if it works for your use case and which features you think are still missing.


About the author

Markus Oberlehner

Markus Oberlehner

Markus Oberlehner is a Open Source Contributor and Blogger living in Austria and a Storyblok Ambassador. He is the creator of avalanche, node-sass-magic-importer, storyblok-migrate.


More to read...