Upgrade Guide#

This upgrade guide lists all breaking changes in Volto and explains the steps that are necessary to upgrade to the latest version. Volto uses Semantic Versioning. For more information see Versioning policy.


There are times when updating the Volto boilerplate (the one generated by @plone/generator-volto) is enough to fulfill all the changes. If you haven't heavily modified it, moving things around and copying over your dependencies might do when dealing with upgrades. We keep the generator up to date and in sync with current Volto release. Please notice that the generator is able to tell you when it runs if it's outdated. The generator is also able to "update" your project with the latest changes, and propose to you to merge the changes, so you can run it on top of your project by answering the prompt.

Upgrading to Volto 15.x.x#

Update your Rich Text Editor configuration#

DraftJS libraries are now lazy-loaded, and some changes have been introduced in the way that the rich text editor is bootstrapped. In case you have extended the rich text editor configuration in your projects you have to update your src/config.js:

The old way:

  export default function applyConfig(config) {
    config.settings = {
      listBlockTypes = [
    return config;

The new way:

  export default function applyConfig(config) {
    const { richtextEditorSettings } = config.settings;
    config.settings.richtextEditorSettings = (props) => {
      const result = richtextEditorSettings(props);
      result.listBlockTypes = [...result.listBlockTypes, 'my-list-item']
      return result;
    return config;

Language Switcher no longer takes care of the sync of the language#

This responsibility has been transferred in full to the API Redux middleware, if you have shadowed either LanguageSwitcher or MultilingualRedirector (during the alpha phase) components, please update them. Not doing so won't break your project, but they won't get the latest features and bug fixes, and probably will update the language cookie twice.

LinkView component markup change#

The LinkView component has the literal The link address is: <the link> is now wrapped in a <p> block instead of a <span> block. Please check if you have a CSS bound to that node and adjust accordingly.

Rename core-sandbox fixture to coresandbox#

Only applying to Volto core development, for the sake of consistency with the other fixtures, core-sandbox fixture it's been renamed to coresandbox in all scripts and related file paths and filenames.

Extend the original intent and rename RAZZLE_TESTING_ADDONS to ADDONS#

Originally the RAZZLE_TESTING_ADDONS environment variable was an escape hatch to load some components and configurations not present in vanilla Volto. One could enable them at will. Initially thought as fixtures for acceptance tests, the original intent has been repurposed and extended to just ADDONS. One could extend the ADDONS list configuration via this environment variable.

It works for published packages, such as those add-ons that live in the packages folder locally to your project. This is similar to the testing add-ons from vanilla Volto.

Use @root alias instead of ~#

A new @root alias has been set up to replace the ~ alias. Support for the ~ alias is still in place, but we now mark it as deprecated. The use of ~ will be removed in Volto 16.

Deprecated since version 15.0.

Upgrading to Volto 14.x.x#

Revisited, rethought and refactored seamless mode#

Seamless mode was released as experimental in Volto 13. However, after a period of testing some issues were detected so the feature has been rethought and refactored.

If you want to take full advantage of seamless mode you should upgrade your backend to the latest plone.restapi (8.12.1 or greater) and plone.rest (2.0.0a1 or greater) versions.

If you were already using Seamless mode in your deployments, you should update them as explained in the deployment documentation (link just below).


The proxy in development mode will only work using the new traversal ++api++. You need to upgrade your development environment to include the requirements explained above.

Read the full documentation about Seamless mode: Seamless mode.

Update i18n configuration for projects and add-ons#

The i18n script and infrastructure have been moved to their own package since we needed them to be independent of Volto itself. This was necessary for being able to use them from the add-ons without having to install the whole Volto package (which is not possible).

@plone/scripts package is the placeholder of the script, which has also been improved alongside the infrastructure (Babel config) for it to run.

Steps for migration:


In a project's package.json replace the scripts i18n line with this one:

   "scripts": {
-    "i18n": "NODE_ENV=production node src/i18n.js"
+    "i18n": "rm -rf build/messages && NODE_ENV=production i18n"
+  },


If you are an add-on maintainer, remove the src/i18n.js script, since it's useless. Within the scripts section of package.json apply the following change:

   "scripts": {
-    "i18n": "NODE_ENV=production node node_modules/@plone/volto/src/i18n.js",
+    "i18n": "rm -rf build/messages && NODE_ENV=production i18n --addon",

afterwards add this to the dependencies list:

+  "dependencies": {
+    "@plone/scripts": "*"

Apply the following diff to your add-on's babel.config.js:

-module.exports = require('@plone/volto/babel');
+module.exports = function (api) {
+  api.cache(true);
+  const presets = ['razzle/babel'];
+  const plugins = [
+    [
+      'react-intl', // React Intl extractor, required for the whole i18n infrastructure to work
+      {
+        messagesDir: './build/messages/',
+      },
+    ],
+  ];
+  return {
+    plugins,
+    presets,
+  };


For convenience the i18n script is now an executable in the node environment.

Removal of the old configuration system based on imports#

As announced in the deprecation notice in Volto 12 release, from Volto 14 onwards, the old configuration system based on imports will stop working. Migrate your Volto configuration for your projects before upgrading to Volto 14.

More information: https://6.dev-docs.plone.org/volto/upgrade-guide/index.html#volto-configuration-registry

Content locking#

Not really a breaking change, but it's worth noting it. By default, Volto 14 comes with Locking support enabled, if the backend supports it. Thus:

  • Upgrade Plone RestAPI

    • plone.restapi>=8.9.0 (Plone 5+)

    • plone.restapi>=7.4.0 (Plone 4)

  • Update plone:CORSPolicy to include Lock-Token within allow_headers:


Blocks chooser now uses the title instead of the id of the block as translation source#

The BlockChooser component now uses the title of the block as source for translating the block title. Before, it took the id of the block, which is utterly wrong and missleading. There is a chance that this change will trigger untranslated blocks titles in your projects and add-ons.

Variation field now uses the title instead of the id of the variation as translation source#

Following the same convention as the above change, Variation field coming from the block enhancers now uses the title of the block as source for translating the variation title. Before, it took the id of the block, which as stated before, is wrong and missleading. There is a chance that this change will trigger untranslated variation titles in your projects and add-ons.

Listing block no longer retrieve fullobjects by default#

The query used by the listing block always used the fullobjects flag, which fully serialized (and thus, wake from the db) the resultant response items. This was causing performance issues. From Volto 14, the results will get the normal catalog query metadata results. You'll need to adapt your code to get the appropiate data if required and/or use the metadata counterparts. If your custom code depends on this behavior and you don't have time to adapt now, there's a scape hatch: set an additional fullobjects key to true per variation in the variation of the listing block config object:

    variations: [
        id: 'default',
        isDefault: true,
        title: 'Default',
        template: DefaultListingBlockTemplate,
        fullobjects: true


This feature needs at least plone.restapi >= 8.13.0 and plone.volto 3.1a4. You need to update the catalog from your existing objects in your database, if you have already a production site in place, in order to have the new metadata filled in the catalog.

New mobile navigation menu#

The mobile navigation menu has been improved using a customizable CSSTransition group animation. It is a breaking change since this change introduces new classes and HTML to accomplish it. A new NavItems helper (presentational) component has been introduced as well. However, the API of the component hasn't changed so your customizations/shadowed components (if any) are safe. If you want to use the new stock menu and interaction, and you have customizations/shadowed components, you need to update them using the stock one.

Adjusted main Logo component styling#

In order to match the Plone logo and in lieu to use a better generic icon starting point, the Logo.jsx component and .logo-nav-wrapper styling have been adjusted. The logo is not constrained by default to 64px and the wrapper now centers vertically. Please check that your project logo placeholder is still in good shape after upgrade.

Move theme.js import to top of the client code#

This is not a strict breaking change, but it's worth mentioning it as it might be important to keep in mind, especially if you are using inline CSS imports in your code, it might change your CSS cascade apply order. However, if you use the theme approach adding custom.overrides/custom.less files, you are good to go since they are applied in the same batch.

getVocabulary action changed its signature#

The getVocabulary action has changed API. Before, it used separate positional arguments, but now it uses named arguments by passing a single object as the argument. You'll have to adjust any call you do if you are using this action in custom code to the new API.

Upgrading to Volto 13.x.x#

Deprecating NodeJS 10#

Since April 30th, 2021 NodeJS 10 is out of Long Term Support by the NodeJS community, so we are deprecating it in Volto 13. Please update your projects to a NodeJS LTS version (12 or 14 at the moment of this writing).

Seamless mode is the default in development mode#

Not really a breaking change, but it's worth noting it. By default, Volto 13 in development mode uses the internal proxy in seamless mode otherwise configured differently. To learn more about the seamless mode read: Seamless mode and Zero configuration builds.

Refactored Listing block using schemas and ObjectWidget#

The Listing block has been heavily refactored using schema forms and BlockDataForm as well as the other new internal artifacts to leverage blocks variations and extensions at the same time simplifying it.

Furthermore, the "More..." link it's now opt-in, instead of always-in. If your projects rely on it, you should set the block setting config.blocks.blocksConfig.listing.showLinkMore to true.

The advantage of this is that now you can use the QuerystringWidget with schema based data forms in a reusable way in your custom blocks. See the Listing block code for further references.

Migrate your existing listing blocks#

(Updated: 2021/06/12) If you have an existing Volto installation and you are using listing blocks, you must run an upgrade step in order to match the new listing internals. You can find this upgrade step in the plone.volto package. You can run the step from there if you have installed plone.volto in your project, it's named Migrate listing blocks from Volto 12 to Volto 13. You can find it in the Add-ons control panel. Alternatively, you can transfer it to your own integration packages and run it from there.


def from12to13_migrate_listings(context):
    def migrate_listing(originBlocks):
        blocks = deepcopy(originBlocks)
        for blockid in blocks:
            block = blocks[blockid]
            if block["@type"] == "listing":
                if block.get("template", False) and not block.get("variation", False):
                    block["variation"] = block["template"]
                    del block["template"]
                if block.get("template", False) and block.get("variation", False):
                    del block["template"]

                # Migrate to internal structure
                if not block.get("querystring", False):
                    # Creates if it is not created
                    block["querystring"] = {}
                if block.get("query", False) or block.get("query") == []:
                    block["querystring"]["query"] = block["query"]
                    del block["query"]
                if block.get("sort_on", False):
                    block["querystring"]["sort_on"] = block["sort_on"]
                    del block["sort_on"]
                if block.get("sort_order", False):
                    block["querystring"]["sort_order"] = block["sort_order"]
                    if isinstance(block["sort_order"], bool):
                        block["querystring"]["sort_order"] = (
                            "descending" if block["sort_order"] else "ascending"
                        block["querystring"]["sort_order"] = block["sort_order"]
                    block["querystring"]["sort_order_boolean"] = (
                        if block["sort_order"] == "descending" or block["sort_order"]
                        else False
                    del block["sort_order"]
                if block.get("limit", False):
                    block["querystring"]["limit"] = block["limit"]
                    del block["limit"]
                if block.get("batch_size", False):
                    block["querystring"]["batch_size"] = block["batch_size"]
                    del block["batch_size"]
                if block.get("depth", False):
                    block["querystring"]["depth"] = block["depth"]
                    del block["depth"]

                # batch_size to b_size, idempotent
                if block["querystring"].get("batch_size", False):
                    block["querystring"]["b_size"] = block["querystring"]["batch_size"]
                    del block["querystring"]["batch_size"]

                print(f"Migrated listing in {obj.absolute_url()}")

        return blocks

    pc = api.portal.get_tool("portal_catalog")
    for brain in pc.unrestrictedSearchResults(object_provides=IBlocks.__identifier__):
        obj = brain.getObject()
        obj.blocks = migrate_listing(obj.blocks)

If you have trouble configuring the upgrade step in your own package, you can take a look and configure it as in plone.volto as shown in this PR: https://github.com/kitconcept/kitconcept.volto/pull/29


When an official integration package exists, these upgrade steps in the backend will be provided in there.

Update your custom variations (templates) in your project listing blocks#

In the case that you have custom templates for your listing blocks in your projects, it's required that you update the definitions to match the new core variations syntax.

Going from this:

config.blocks.blocksConfig.listing.templates = {
  mycustomvariationid: {
    label: 'My custom listing variation',
    template: MyCustomListingBlockTemplate,

To this:

  config.blocks.blocksConfig.listing.variations = [
      id: 'mycustomvariationid',
      isDefault: false,
      title: 'My custom listing variation',
      template: MyCustomListingBlockTemplate,

Control panel icons are now SVG based instead of font based#

It was long due, the control panel overview route /controlpanel is now using SVG icons from the Pastanaga icon set, instead of the deprecated font ones. If you have customized or created a control panel and you are using it in Volto, you should update it and use the config registry setting: controlPanelsIcons and add the name of your control panel and the related SVG like:

import myfancyiconSVG from '@plone/volto/icons/myfancyicon.svg';
import config from '@plone/volto/registry'

config.settings.controlPanelsIcons.mynewcontrolpanelid = myfancyiconSVG;

Login form UI and accessibility updated#

Not really a breaking change, but it's worth to note that we changed the look and feel of the login form and improved its usability and accessibility. Another move towards the new Quanta look and feel.

Changes in the Table block feature set and messages#

The "inverted" option in Table Block was removed since it was useless with the current CSS set. Better naming of options and labels in table block (English). Updating the i18n messages for the used translations is advisable, but not required.

Upgrading to Volto 12.x.x#

Volto Configuration Registry#

The configuration object in Volto is located in the ~/config module and uses it as container of Volto's config taking advantage of the ES6 module system. So we "import" the config every time we need it, then the exported config data in that module "magically" is there whenever we want to access to it.

It's been a while since we were experiencing undesired side effects from "circular import dependency" problems in Volto, due to the very nature of the solution (importing the ~/config). Although they aren't very noticeable, they are there, waiting to bite us. In fact, circular dependencies are common in NodeJS world, and the very nature of how it works make them "workable" thanks to the NodeJS own import resolution algorithm. So the "build" always works, although we have the circular dependencies, but that leads to weird problems like (just to mention one of them) the HMR (Hot Module Reloader) not working properly.

That's why in this version we are introducing the new Volto's Configuration Registry. It's a centralized singleton that is populated from the core config module and can be modified by the add-ons and the project itself. The code, instead of using imports to get it, imports the singleton and then access the proper registry key inside it (settings, blocks, views, etc...)

Changes in your code (and local customizations)#

This was the old way:

import { settings } from '~/config'


and this is the new way:

import config from '@plone/volto/registry'

config.settings.isMultilingual = true


The old way of using the import to get Volto's configuration will still be working as long as you support it in your project src/config but it will be deprecated and will stop working from Volto 14 onwards.

It is highly advisable that you use the new configuration registry right away. Your custom code (and Volto customizations using the shadowing engine) has to adapt to the new way of reading the config from the new Volto's Configuration. However, it won't be mandatory until Volto 14, leaving the community time to adapt their code and projects.


If you are an add-on maintainer, and you migrate your add-on to be Volto 12 compatible, it's recommended that you add it as peerDependencies for Volto 12.

  "peerDependencies": {
    "@plone/volto": ">=12.0.0"

Changes in your project's routes module#

--- a/src/routes.js
+++ b/src/routes.js
@@ -5,7 +5,7 @@

 import { App } from '@plone/volto/components';
 import { defaultRoutes } from '@plone/volto/routes';
-import { addonRoutes } from '~/config';
+import config from '@plone/volto/registry';

  * Routes array.
@@ -18,7 +18,7 @@ const routes = [
     component: App, // Change this if you want a different component
     routes: [
       // Add your routes here
-      ...(addonRoutes || []),
+      ...(config.addonRoutes || []),

Changes in your project's config module#

Remove the imports and the exports in your src/config.js:

--- a/src/config.js
+++ b/src/config.js
@@ -12,30 +12,11 @@
  * }

-import {
-  settings as defaultSettings,
-  views as defaultViews,
-  widgets as defaultWidgets,
-  blocks as defaultBlocks,
-  addonReducers as defaultAddonReducers,
-  addonRoutes as defaultAddonRoutes,
-} from '@plone/volto/config';
+import '@plone/volto/config';

-export const settings = {
-  ...defaultSettings,
-export const views = {
-  ...defaultViews,
-export const widgets = {
-  ...defaultWidgets,
-export const blocks = {
-  ...defaultBlocks,
-export const addonRoutes = [...defaultAddonRoutes];
-export const addonReducers = { ...defaultAddonReducers };

notice from the diff, that you must add this import AFTER all your imports:

// All your imports required for the config here BEFORE this line
import '@plone/volto/config';

Then add this function as default export at the end of your src/config.js module:

export default function applyConfig(config) {
  // Add here your project config
  return config;

It has the same signature, and it's used like the applyConfig() function in index.js module add-ons. You should place your project's configuration here and mutate the config like you would do it in add-ons.

Let's show it in an example. Let's say you have this config in your project's src/config module:

export const settings = {
  isMultilingual: true,
  supportedLanguages: ['en', 'de'],
  defaultLanguage: 'de',
  navDepth: 3,

then you'll add the applyConfig() function as default export and copy that settings key in it:

export default function applyConfig(config) {
  config.settings = {
    isMultilingual: true,
    supportedLanguages: ['en', 'de'],
    defaultLanguage: 'de',
    navDepth: 3,
  return config;


The add-ons you might be using might need to migrate to use the new configuration registry too. Make sure all of them are already migrated to Volto 12.


Although this might be daunting, the migration is quite straightforward, and the refactoring of the required code can be undergone through a series of "search and replace" in your IDE of choice.

Changes in your project's package.json#

You need to update the setupFiles key of your jest configuration:

     "setupFiles": [
-      "@plone/volto/test-setup.js"
+      "@plone/volto/test-setup-globals.js",
+      "@plone/volto/test-setup-config.js"

Changes in snapshots tests in your project#

Please note that your tests' snapshots will also change because of the new testing mocks (in widgets, blocks and in views). You should review them, make sure that the mocks are there instead of the real mocked components and accept them.

Alternative - both configurations coexisting#


This configuration is not recommended and might lead to inconsistencies and has been tested only partially and can you can find unseen problems. This method is only a workaround in case the add-ons you are using are not yet migrated, or you can't migrate your code. And in any case, as stated above, it will be deprecated and will stop working in Volto 14.

Make the following changes to your src/config.js, first remove the main imports, and add the import to the new config registry:

-import {
-  settings as defaultSettings,
-  views as defaultViews,
-  widgets as defaultWidgets,
-  blocks as defaultBlocks,
-  addonReducers as defaultAddonReducers,
-  addonRoutes as defaultAddonRoutes,
-} from '@plone/volto/config';
+import ConfigRegistry from '@plone/volto/registry';

then after the last existing import in your existing src/config.js add:

import * as voltoDefaultConfig from '@plone/volto/config';

Remove all the default exports, but save your config for later use:

-export const settings = {
-  ...defaultSettings,
-export const views = {
-  ...defaultViews,
-export const widgets = {
-  ...defaultWidgets,
-export const blocks = {
-  ...defaultBlocks,
-export const addonRoutes = [...defaultAddonRoutes];
-export const addonReducers = { ...defaultAddonReducers };

showing in the diff the default ones, you should have your configuration inside those, like:

export const settings = {
  isMultilingual: true,
  supportedLanguages: ['en', 'de'],
  defaultLanguage: 'de',
  navDepth: 3,

At the end of your src/config.js:

+const localconfig = {
+  ...voltoDefaultConfig,
+const applyLocalConfig = applyConfig(localconfig);
+ConfigRegistry.settings = applyLocalConfig.settings;
+ConfigRegistry.blocks = applyLocalConfig.blocks;
+ConfigRegistry.views = applyLocalConfig.views;
+ConfigRegistry.widgets = applyLocalConfig.widgets;
+ConfigRegistry.addonRoutes = applyLocalConfig.addonRoutes;
+ConfigRegistry.addonReducers = applyLocalConfig.addonReducers;
+export const settings = applyLocalConfig.settings;
+export const blocks = applyLocalConfig.blocks;
+export const views = applyLocalConfig.views;
+export const widgets = applyLocalConfig.widgets;
+export const addonRoutes = applyLocalConfig.addonRoutes;
+export const addonReducers = applyLocalConfig.addonReducers;

Migrate your config to the new config registry style, but not as a default export, at the end of src/config.js:

+function applyConfig(config) {
+  config.settings = {
+    ...config.settings,
+    isMultilingual: true,
+    supportedLanguages: ['en', 'de'],
+    defaultLanguage: 'de',
+    navDepth: 3,
+  };


Although you can keep both ways of using Volto's config it is recommended you use the new registry based configuration as soon as possible, as we discourage you to continue using the old way.

Upgrading to Volto 11.x.x#

AlignBlock component new placement and import path#

Due to problems with circular dependencies, the AlignBlock was moved to helpers and used from there. Unfortunately, it was proven to be worse overall. We moved it (with a known workaround) to its rightful place again.

If your code is importing it from helpers, you should update it to the new path:

- import { AlignBlock } from '@plone/volto/helpers';
+ import AlignBlock from '@plone/volto/components/manage/Sidebar/AlignBlock';

id is removed from FormFieldWrapper#

We have removed the id from the FormFieldWrapper because it coincides with the label id if we don't provide the fieldset.

If you have cypress tests which depends on this id then just remove the id from the test and if the test fails then just add .react-select-container instead of your id. See https://github.com/plone/volto/pull/2102 for more details.

New Default Listing Template#


If you have customized the default listing template this change possibly does not have an effect on your project.

The default Template for the Listing Block now no longer contains an image. The old default Template has been renamed to "Summary". This will lead to every Listing Block in your Project that uses the default Template to now use the new default Template, and thus, no longer showing the image.

To resolve this you can change the template of the affected Listing Blocks either manually or by writing a backend script for that.

Upgrading to Volto 10.x.x#

Remove the Razzle plugins patch#


If you haven't upgraded your project to Volto 9.x.x and followed the upgrade guide instructions, you are set, and you do not need to do anything.

In order to have support for Razzle plugins as local modules we introduced a patch in 9.0.0 that addressed the lack of support in Razzle 3.3.7 . Unfortunately, not only did that introduced more headaches than benefits, but inadvertently we introduced a bug on the patch. We've found a workaround to still support plugins as local modules without patching Razzle, however that forces you to delete the patch introduced in your projects if you followed the 9.x.x upgrade guide steps.

getContent changes#

The content is no longer fetched from Volto with the fullobjects flag in the request. If your code relied on children being fully serialized with their parent, you should refactor it. Alternatively, you can set settings.bbb_getContentFetchesFullobjects to true to get the old behavior.

@testing-library/react upgrade notice#

@testing-library/react has been upgraded too, and it comes with some internal API changes too, so if you make heavy use of it in your tests, you might need to update your testing code to adapt to the changes. Please refer to the @testing-library/react documentation for further information if needed.

Upgrading to Volto 9.x.x#

Internal upgrade to use Razzle 3.3.7#


If you haven't customized your razzle.config.js in your project, or have any custom plugin in place, you don't have to do anything.

Razzle is the isometric build system for both the server and the client parts on top of which Volto is built. Recently, it has been under heavy development and some new exciting features have been added to it. The Razzle configuration is now more flexible and extensible than ever.

This change might be breaking for you if you customized the razzle.config.js heavily in your projects. Since the new version adds a new way to extend Razzle configuration, you should adapt your extensions to the new way of doing it. See the documentation for more information: https://razzlejs.org/docs/customization#extending-webpack

It also unifies the way things are extended in Razzle plugins as well, so if you are using any official or third party Razzle plugins you should upgrade them to the latest version. If you have developed your own Razzle plugin, you should adapt its signature as well. See the documentation for more information: https://razzlejs.org/docs/customization#plugins

Razzle 3.3 also has some new experimental features, that will be default in the upcoming Razzle 4, such as the new React Fast Refresh feature, which fixes the annoying breaking of the router after any live refresh.

See the documentation of Razzle for more information: https://razzlejs.org/

Changes involved#

We need to patch an internal Razzle utility in order to allow the use of non-released Razzle plugins. This feature will be in Razzle 4, unfortunatelly at this point the development of the Razzle 3 branch is freezed already, so we need to amend the original using the patch. The patch will be obsolete and no longer required once we move to Razzle 4 (see https://github.com/jaredpalmer/razzle/pull/1467).


Since Volto 9.2.0 the next step IS NOT required anymore.

~~Copy (and overwrite) the patches folder into your local project https://github.com/plone/volto/tree/master/patches or, if you want to be more accurate, just copy patches/razzle-plugins.patch file and overwrite patches/patchit.sh file.~~

Babel config housekeeping#

Historically, Volto was using "stage-0" TC-39 proposals. The configuration was starting to show its age, since Babel 7 decided to stop maintaining the presets for stages, we moved to use a static configuration instead of a managed one. That lead to a "living on the edge" situation since we supported proposals that didn't make the cut. For more information about the TC39 approval process read (https://tc39.es/process-document/)

We decided to put a bit of order to the chaos and declare that Volto will support only stage-4 approved proposals. They are supported by @babel/preset-env out of the box and provide a good sensible default baseline for Volto.

Proposal deprecations:

  • @babel/plugin-proposal-decorators

  • @babel/plugin-proposal-function-bind

  • @babel/plugin-proposal-do-expressions

  • @babel/plugin-proposal-logical-assignment-operators

  • @babel/plugin-proposal-pipeline-operator

  • @babel/plugin-proposal-function-sent

In fact, Volto core only used the first one (decorators) and we made the move to not use them anymore a long time ago. However, if you were using some of the other presets, your code will stop compiling. Migrate your code or if you want to use the proposal anyways, you'll need to provide the configuration to your own project (babel.config.js) in your project root folder.

You might still be using the old-style connecting of your components to the Redux store using @connect decorator, in that case, take a look at any connected component in Volto to have a glimpse on how to migrate the code.

If you were not using any of the deprecated proposals (the most common use case), then you are good to go, and you don't have to do anything.

Hoisting problems on some setups#

Some people were experimenting weird hoisting issues when installing dependencies. This was caused by Babel deprecated proposals packages and its peer dependencies that sometimes conflicted with other installed packages.

Volto's new Babel configuration uses the configuration provided by babel-razzle-preset package (Razzle dependency) and delegates the dependencies management to it, except a few Babel plugins that Volto still needs to work.

In order for your projects not have any problem with the new configuration and comply with the new model, you need to remove any local dependency on @babel/core and let Volto handle them.

diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -183,7 +183,6 @@
     "node": "^10 || ^12 "
   "dependencies": {
-    "@babel/core": "7.11.1",
     "@plone/volto": "8.9.2",
     "mrs-developer": "1.2.0",

Recomended browserslist in package.json#

Not a breaking change, but you might want to narrow the targets your Volto project is targeting. This might improve your build times, as well as your bundle size. This is the recommended browserlist you should include in your local package.json.

  "browserslist": [
    "last 4 versions",
    "Firefox ESR",
    "not ie 11",
    "not dead"


Please notice that it does not target dead and deprecated browsers by its vendors.

New webpack resolver plugin#

A new webpack resolver plugin has been integrated with Volto, it reroutes 'local' resolve requests (for example import Something from './Something') to 'absolute' resolve requests (like import Something from '@plone/myaddon/Something). This allows the shadow-based customization mechanisms to work consistently with add-ons and Volto.

This is not a breaking change, and it shouldn't affect any existing code, but by its very nature, a resolver plugin has the potential to introduce unexpected behavior. Just be aware of its existence and take it into consideration if you notice anything strange.

Content Types icons#

Helper method getIcon from Url has been removed in favor of getContentIcon from Content which is now configurable.

See contentIcons.

Upgrading to Volto 8.x.x#

Upgrade package.json testing configuration#

The dummy-addons-loader.js file has been renamed to jest-addons-loader.js, to be more in line with the rest of the existing files. You should add the following value to the moduleNameMapper property of the jest key in your project's package.json:

"load-volto-addons": "<rootDir>/node_modules/@plone/volto/jest-addons-loader.js",

Upgrading to Volto 7.x.x#

A misspelled file has been renamed. If you import strickthrough.svg in your project, you'll now find that file at @plone/volto/icons/strikethrough.svg.

New webpack resolve alias for Volto themes#

As a "nice to have", a new resolve alias is provided that points to Volto's theme folder. So, in your project's theme.config file, you can replace:

@themesFolder: '../../node_modules/@plone/volto/theme/themes';
@siteFolder: "../../theme";
@fontPath : "../../@{theme}/assets/fonts";


@themesFolder: '~volto-themes';
@siteFolder: '~@package/../theme';
@fontPath: "~volto-themes/@{theme}/assets/fonts";

You might consider moving your theme files to a subfolder called site, to prepare for the arrival of add-ons theming and their overrides. In that case, you would set your @siteFolder to:

@siteFolder: '~@package/../theme/site';

Upgrading to Volto 6.x.x#

First, update the package.json of your Volto project to Volto 6.x.x.

  "dependencies": {
    "@plone/volto": "6.0.0",


This release includes a number of changes to the internal dependencies. If you have problems building your project, you might need to remove your node_modules and, ultimately, also remove your yarn.lock file. Then run again yarn for rebuilding the dependencies.

Upgrade to Node 12#

We have now dependencies that requires node >=10.19.0. Although Node 10 has still LTS "maintenance" treatment (see https://nodejs.org/en/about/releases/) the recommended path is that you use from now on node 12 which is LTS since last October.

Upgrade local dependencies versions#

You need to update devDependencies in package.json in your local environment:

  "devDependencies": {
    "eslint-plugin-prettier": "3.1.3",
    "prettier": "2.0.5",
    "stylelint-config-idiomatic-order": "8.1.0",
    "stylelint-config-prettier": "8.0.1",
    "stylelint-prettier": "1.1.2",

and remove entirely the resolutions key:

  "resolutions": {
    "@plone/volto/razzle/webpack-dev-server": "3.2.0"

Update package.json config#

Add this key to the jest.moduleNameMapper:

  "moduleNameMapper": {
    "@plone/volto/babel": "<rootDir>/node_modules/@plone/volto/babel",

because the new version of Jest is a bit more picky when importing externals. Attention, this mapping needs to be the first, it needs to come before the @plone/volto/(.*)$ key.


Prettier has been updated, introducing some breaking formatting changes. It's recommended that you upgrade your local version of prettier and reformat your code with it using:

yarn prettier:fix


stylelint has been upgraded too, and it introduces some changes in the declaration of the styles order. It's recommended that you upgrade your local version of prettier and reformat your code with it using:

yarn stylelint:fix

CSS modules are not supported anymore#

Razzle does not support them anymore, so neither do we. If you need them, you could add a Webpack config in your local razzle.config.js.

Update your eslint config#

Introduced in the Volto 5 series, it's recommended that you update your local ESLint config. In the past, we used .eslintrc file to do so. In order to support automatically Volto add-ons, you should remove it and use a JS based config named .eslintrc.js with this content:

const path = require('path');
const projectRootPath = path.resolve('.');
const packageJson = require(path.join(projectRootPath, 'package.json'));

// Extends ESlint configuration for adding the aliases to `src` directories in Volto add-ons
const addonsAliases = [];
if (packageJson.addons) {
  const addons = packageJson.addons;
  addons.forEach(addon => {
    const addonPath = `${addon}/src`;
    addonsAliases.push([addon, addonPath]);

module.exports = {
  extends: './node_modules/@plone/volto/.eslintrc',
  settings: {
    'import/resolver': {
      alias: {
        map: [
          ['@plone/volto', '@plone/volto/src'],
          ['@package', './src'],
        extensions: ['.js', '.jsx', '.json'],
      'babel-plugin-root-import': {
        rootPathSuffix: 'src',

New wrappers in block editor#

We have improved the overall UX of the block drag and drop feature by using the library react-beautiful-dnd in the block editor. It introduces new wrappers (belonging to the lib machinery) in the structure. The original structure and class names are still in there (as children of these wrappers) to maintain maximum backwards compatibility. Those might be cleaned up in next major versions, so if for some reason you have customized the styling of your blocks in edit mode relying on the old structure, you might want to review and adapt them.

Update config.js#


This is required since Volto version 6.1.0 1541

Add these lines of code to the config.js of your project:

import {
  addonRoutes as defaultAddonRoutes,
  addonReducers as defaultAddonReducers,
} from '@plone/volto/config';

export const addonRoutes = [...defaultAddonRoutes];
export const addonReducers = { ...defaultAddonReducers };

Update the routes.js of your project:

import { addonRoutes } from '~/config';

const routes = [
    path: '/',
    component: App, // Change this if you want a different component
    routes: [
      // Add your routes here
      ...(addonRoutes || []),

Upgrading to Volto 5.x.x#

First, update the package.json of your Volto project to Volto 5.x.x.

  "dependencies": {
    "@plone/volto": "5.0.0",

New lazy loading boilerplate#

Volto is now capable of splitting and lazy loading components. This allows for better performance and reduced bundle sizes, the client also has to parse and load less code, improving the user experience, especially on mobile devices.

The boilerplate includes changes in the structural foundation of Volto itself. So if you have updated in your projects any of these components:

  • src/helpers/Html/Html.jsx

  • src/components/theme/App/App.jsx

  • src/server.jsx

  • src/client.jsx

you should adapt them to the newest changes in Volto source code. You can do that by diffing the new ones with your versions.

Testing lazy loaded components#

The whole process has been designed to have a minimal impact in existing projects. However, only one thing should be changed in your components tests, especially if your components are composed of original Volto components (not SemanticUI ones, though).

You should adapt them by mocking the Volto component or resolve (await) in an async construction before the test is fired. See this Codepen example:


import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
import { Component1, Component2 } from "./components";

describe("CustomComponent", () => {
  it("rendered lazily", async () => {
    const { container, getByText } = render(<App />);

    await Component1;
    await Component2;


There is also another pattern used in Volto core for testing you can transform your test to be async aware like this:

--- a/src/components/manage/Preferences/PersonalPreferences.test.jsx
+++ b/src/components/manage/Preferences/PersonalPreferences.test.jsx
@@ -3,6 +3,7 @@ import renderer from 'react-test-renderer';
 import { Provider } from 'react-intl-redux';
 import configureStore from 'redux-mock-store';
 import { MemoryRouter } from 'react-router-dom';
+import { wait } from '@testing-library/react';

 import PersonalPreferences from './PersonalPreferences';

@@ -13,7 +14,7 @@ jest.mock('react-portal', () => ({

 describe('PersonalPreferences', () => {
-  it('renders a personal preferences component', () => {
+  it('renders a personal preferences component', async () => {
     const store = mockStore({
       intl: {
         locale: 'en',
@@ -36,7 +37,8 @@ describe('PersonalPreferences', () => {
-    const json = component.toJSON();
-    expect(json).toMatchSnapshot();
+    await wait(() => {
+      expect(component.toJSON()).toMatchSnapshot();
+    });

Helmet title it's now centralized in View.jsx#

All the calls for updating the title in the document performed by Helmet are now centralized in the View.jsx components. It's recommended to remove all the Helmet calls for updating the title from your components especially if you are using some SEO add-ons for Volto, since not doing that could interfere with them.

Upgrading to Volto 4.x.x#

First, update your package.json to Volto 4.x.x.

  "dependencies": {
    "@plone/volto": "4.0.0",

New initial blocks per content type setting in Alpha 37#

Not a breaking change, but now there's a new setting in Blocks, initialBlocks where you can define the initial blocks for all content types. You can override the default ('title' and a 'text' block) and provide your own by modifying the configuration object:

const initialBlocks = {
    Document: ['leadimage', 'title', 'text', 'listing' ]

provide an empty object if you don't want to define any additional initial blocks and keep the default.

const initialBlocks = {};

ImageSidebar moved to Image Block directory in Alpha 29#

For better resource grouping, the ImageSidebar component has been moved to the Image block component directory: components/manage/Blocks/Image

Copy yarn.lock from volto-starter-kit in Alpha 17#

Due to changes in the dependency tree, it's required to use a specific yarn.lock file by deleting it and copy the one here: https://github.com/plone/volto-starter-kit/blob/master/yarn.lock before upgrading to Volto alpha 17.

Forked Helmet into Volto core#

Due to the inactivity of the Helmet project, we decided to fork it to the core. It's part of the Volto helpers now. You have to update your imports accordingly. Please notice that now it's a named import:

--- a/src/components/Views/ReportView.jsx
+++ b/src/components/Views/ReportView.jsx
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
+import { Helmet } from '@plone/volto/helpers';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { format, parse } from 'date-fns';
 import { filter, map } from 'lodash';

Alpha 16 is a brownbag release#

There was a problem with the projects using Volto eslint config when upgrading to the latest versions related to typescript, we will take of that in the near future. So skip this version.

Stylelint and prettier config in Alpha 14#

In your project's boilerplate, you need to update the stylelint and prettier configuration accordingly to the changes made in Alpha 14 in package.json like this:

diff --git a/package.json b/package.json
index 7c8194c..5c63469 100644
--- a/package.json
+++ b/package.json
@@ -46,26 +46,51 @@
   "prettier": {
     "trailingComma": "all",
-    "singleQuote": true
+    "singleQuote": true,
+    "overrides": [
+      {
+        "files": "*.overrides",
+        "options": {
+          "parser": "less"
+        }
+      }
+    ]
   "stylelint": {
     "extends": [
-      "stylelint-config-standard",
-      "stylelint-config-idiomatic-order",
-      "./node_modules/prettier-stylelint/config.js"
-    ]
+      "stylelint-config-idiomatic-order"
+    ],
+    "plugins": [
+      "stylelint-prettier"
+    ],
+    "rules": {
+      "prettier/prettier": true,
+      "rule-empty-line-before": [
+        "always-multi-line",
+        {
+          "except": [
+            "first-nested"
+          ],
+          "ignore": [
+            "after-comment"
+          ]
+        }
+      ]
+    },
+    "ignoreFiles": "theme/themes/default/**/*.overrides"
   "engines": {
     "node": "^10 || ^12"
   "dependencies": {
-    "@plone/volto": "4.0.0-alpha.10"
+    "@plone/volto": "4.0.0-alpha.14"
   "devDependencies": {
     "eslint-plugin-prettier": "3.0.1",
-    "postcss-overrides": "3.1.4",
-    "prettier": "1.17.0",
-    "prettier-stylelint": "0.4.2"
+    "prettier": "1.19.1",
+    "stylelint-config-idiomatic-order": "6.2.0",
+    "stylelint-config-prettier": "6.0.0",
+    "stylelint-prettier": "1.1.1"
   "resolutions": {
     "@plone/volto/razzle/webpack-dev-server": "3.2.0"


If you are linting actively your project, the build might be broken after this update. You should run:

$ yarn prettier:fix
$ yarn stylelint:fix

then commit the changes.

openObjectBrowser API change in Alpha 11#

The API of the ObjectBrowser component changed in alpha 11 to make it more flexible. In case you had custom blocks using it, you have to update the call in case you were using a link mode:

@@ -42,7 +42,7 @@ const OtherComp = ({
                     href: '',
-              : () => openObjectBrowser('link')
+              : () => openObjectBrowser({ mode: 'link' })
           onChange={(name, value) => {
             onChangeBlock(block, {

See openObjectBrowser handler API for more details.

Renaming Tiles into Blocks#

An internal renaming to use the term Blocks everywhere was done to unify naming through the code and the documentation.

Plone RESTAPI was updated for that purpose too, and running an upgrade step (do so in Plone's Addons control panel) is required in order to migrate the data. No step is required if you are using a brand-new ZODB.

This is the version compatibility table across all the packages involved:

Volto 4 - plone.restapi >= 5.0.0 - kitconcept.voltodemo >= 2.0


The renaming happened in Volto 4 alpha.10 and plone.restapi 5.0.0. Volto 4 alpha versions under that release use older versions of plone.restapi and kitconcept.voltodemo, however if you are using alpha releases it's recommended to upgrade to the latest alpha or the final release of Volto 4.

The project configuration should also be updated, in your src/config.js:

diff --git a/src/config.js b/src/config.js
index f1fe9c2..9517c38 100644
--- a/src/config.js
+++ b/src/config.js
@@ -16,7 +16,7 @@ import {
   settings as defaultSettings,
   views as defaultViews,
   widgets as defaultWidgets,
-  tiles as defaultTiles,
+  blocks as defaultBlocks,
 } from '@plone/volto/config';

 export const settings = {
@@ -31,6 +31,6 @@ export const widgets = {

-export const tiles = {
-  ...defaultTiles,
+export const blocks = {
+  ...defaultBlocks,

Add theme customization to your project#

Volto 4 now also expects a file named src/theme.js with this content by default:

import 'semantic-ui-less/semantic.less';
import '@plone/volto/../theme/themes/pastanaga/extras/extras.less';

Remove enzyme configuration#

Enzyme has been removed, in favor of @testing-library/react, and the configuration should be removed in package.json:

diff --git a/package.json b/package.json
index 27c7f8d..8f5f088 100644
--- a/package.json
+++ b/package.json
@@ -44,9 +44,6 @@
-    "snapshotSerializers": [
-      "enzyme-to-json/serializer"
-    ],
     "transform": {
       "^.+\\.js(x)?$": "babel-jest",
       "^.+\\.css$": "jest-css-modules",

Blocks engine - Blocks configuration object#

The blocks engine was updated and there are some important breaking changes, in case that you've developed custom blocks. The configuration object is now unified and expresses all the properties to model a block. This is how a block in the defaultBlocks object looks like:

const defaultBlocks = {
  title: {
    id: 'title', // The name (id) of the block
    title: 'Title', // The display name of the block
    icon: titleSVG, // The icon used in the block chooser
    group: 'text', // The group (blocks can be grouped, displayed in the chooser)
    view: ViewTitleBlock, // The view mode component
    edit: EditTitleBlock, // The edit mode component
    restricted: false, // If the block is restricted, it won't show in in the chooser
    mostUsed: false, // A meta group `most used`, appearing at the top of the chooser
    blockHasOwnFocusManagement: false, // Set this to true if the block manages its own focus
    security: {
      addPermission: [], // Future proof (not implemented yet) add user permission role(s)
      view: [], // Future proof (not implemented yet) view user role(s)

There is an additional object groupBlocksOrder that contains an array with the order that the blocks group should appear:

const groupBlocksOrder = [
  { id: 'mostUsed', title: 'Most used' },
  { id: 'text', title: 'Text' },
  { id: 'media', title: 'Media' },
  { id: 'common', title: 'Common' },

You should adapt and merge the configuration of your own custom blocks to match the defaultBlocks and groupBlocksOrder one. You can modify the order of the groups and create your own as well.

Blocks engine - Simplification of the edit blocks wrapper#

The edit block wrapper boilerplate was quite big, and for bootstraping an edit block you had to copy it from an existing block. Now all this boilerplate has been transferred to the Blocks Engine, so bootstrapping the edit component of a block is easier and does not require any pre-existing code.

In order to upgrade your blocks you should simplify the outer <div> (took as an example the Title block):

--- a/src/components/manage/Blocks/Title/Edit.jsx
+++ b/src/components/manage/Blocks/Title/Edit.jsx
@@ -138,11 +138,7 @@ class Edit extends Component {
       return <div />;
     return (
-      <div
-        role="presentation"
-        onClick={() => this.props.onSelectBlock(this.props.block)}
-        className={cx('block title', { selected: this.props.selected })}
-      >
+      <>
@@ -185,7 +181,7 @@ class Edit extends Component {
             this.node = node;
-      </div>
+      </>

The blocks engine now takes care of the keyboard navigation of the blocks, so you need to remove the outer <div> from your custom block, then your block doesn't have to react to the change on this.props.selected either, because it's also something that the blocks engine already does for you.

The focus management is also transferred to the engine, so it's not needed for your block to manage the focus. However, if your block does indeed require to manage its own focus, then you should mark it with the blockHasOwnFocusManagement property in the blocks configuration object:

    text: {
      id: 'text',
      title: 'Text',
      icon: textSVG,
      group: 'text',
      view: ViewTextBlock,
      edit: EditTextBlock,
      restricted: false,
      mostUsed: false,
      blockHasOwnFocusManagement: true,
      security: {
        addPermission: [],
        view: [],

Default view renaming#

The default view for content types DocumentView.jsx has been renamed to a more appropriate DefaultView.jsx. This view contains the code for rendering blocks in case the content type has been Blocks enabled. Enable Blocks on your content types by composing the view of your content type using DefaultView component.


  • The old messages container has been removed since it's not used any more by Volto. We changed it to use the Toast library.

  • Improved the Pastanaga Editor block wrapper container layout, deprecating the hack .ui.wrapper > *.

Upgrading to Volto 3.x#

Volto was upgraded to use Razzle 3.0.0 which is not a breaking change itself, but it forces some changes in the boilerplate on your Volto projects. You should change the babel config by deleting .babelrc file and creating a new file babel.config.js with these contents:

module.exports = require('@plone/volto/babel');

Then update your package.json to Volto 3.x.

  "dependencies": {
    "@plone/volto": "3.0.0",

Volto 3.x is compatible with the new changes introduced in the vocabularies endpoint in plone.restapi 4.0.0. If you custom-build a widget based in the Volto ones, you should update them as well. Volto updated its own widget set to support them:

  • components/manage/Widgets/ArrayWidget

  • components/manage/Widgets/SelectWidget

  • components/manage/Widgets/TokenWidget

They all use react-select third party library for render it.

Upgrading to Volto 2.x#

Improved Blocks HOC#

The Blocks HOC (High Order Component) was changed to lift off some features from the blocks themselves, and now it takes care of them by itself.

  • The delete block feature was moved to it

  • The keylisteners for navigating through blocks was moved to it

  • The properties passed down to the blocks are improved and documented

This change only applies to your existing blocks, you have to update them accordingly by deleting the trash icon and action from the end of your blocks

{this.props.selected && (
    onClick={() => this.props.onDeleteBlock(this.props.block)}
    <Icon name={trashSVG} size="18px" />

Modify the parent element of your block making these changes:

  onClick={() => this.props.onSelectBlock(this.props.block)}
  className={cx('block hero', {
    selected: this.props.selected,
  onKeyDown={e =>
  ref={node => {
    this.node = node;
  • Add the keylisteners to the parent element of your block

  onKeyDown={e =>
  • Add a ref to it and assign it to this.node.

  ref={node => {
    this.node = node;
  • Add a proper role for it


Take a look into the implementation of the default Volto blocks to get a grasp on all the edge cases related to keyboard navigation and how to deal with them.

Reordering of the internal CSS, added extra files#

The internal Volto CSS has been tidied up and reordered, for that reason, some other extras have been introduced and the theme.config in your project needs to be updated by making sure you have these two extras in the theme.config file:

/* Extras */
@main        : 'pastanaga';
@custom      : 'pastanaga';