This year at DrupalCon North America Redfin Solutions’ CTO Chris Wells had the honor to speak for the first time at a DrupalCon. His presentation Migrating into Layout Builder had the privilege of being one of the most well-attended sessions of the conference.
Redfin Solutions has a longstanding relationship with the University of New England (UNE)--Maine's largest private university--and they were at a turning point where their previously cutting-edge website felt dated, especially the content editor experience. With Drupal 7's end-of-life on the horizon, we worked with them to come up with an upgrade plan to Drupal 8, so that we would have better access to a modern interface.
Previously, their Drupal website had been implementing responsive component-based design principles using WYSIWYG Templates. With more modern tools like Gutenberg and Layout Builder in core, we knew we had great options and opportunities to provide a better content editor experience.
We knew that we would have to find a strategy for migrating the older paradigm to the new paradigm, and for this we chose layout builder. With its core support and logical application of blocks as components, it was a natural choice. But, how would we get larger blocks of HTML into a place where all new pages were using the new paradigm of pages where each page is a Layout Builder override?
Luckily, Drupal has just such a way to transform data on input, which is the Migrate API. The Migrate API follows a common pattern in Computer Science called Extract, Transform, Load. In the parlance of our times (that is, Drupal), we use the phrases "source" (extract), "process" (transform), and "destination" (load). Each of these phases are represented by Plugins to the Migrate API.
In the case of UNE, we were migrating from (source) Drupal 7 nodes (the page body field) into (destination) "basic text" blocks. For the source, we used the live Drupal 7 database on Pantheon. The "basic text" block is the one that comes out of the box in Drupal 8 as a custom block type, and has a title and a body.
We did NOT go down the rabbit hole of regex'ing out each of the components, but rather we migrated the old body into the new paradigm, so that every page uses the same paradigm from the start, and content editors can expand into using layout builder overrides over time. We simply migrated in some legacy styles, which eventually we will discard. We had the staff and resources to clean up any egregious inaccuracy in the translation as needed, so this ended up being the most time-and-cost-efficient solution.
However, the real magic of this migration is really the process part, where we change the data into the format it needed for layout builder.
Layout Builder Storage
So first, we need to understand how Layout Builder actually stores things behind the scenes. Much like an entity reference field, layout builder is really storing a list of sections. When you build a page with Layout Builder, you are adding sections to it (a one-col, followed by a two-col, followed by another one-col, for example). Much like with regular field tables, it stores the entity ID, revision ID, delta (so it knows the right order!), and then some data value. For taxonomy term references, for example, it would store the "tid" for the term being referenced.
With Layout Builder, there's additional complication. Since each section may contain multiple components, there's an extra layer where we need to then store the components for a section each in their proper order.
For this, Drupal's Layout Builder is not nesting another set of entity references. Instead, it's actually storing a serialized Section object. One of the main tenets of a Section object is an array of SectionComponent objects, which each store their own location and position within the section.
The actual table where this information is stored is the [entity]__layout_builder__layout table in the database. Depending on which entity you've enabled Layout Builder overrides for, this may be the node__layout_builder__layout table, or the user__layout_builder__layout table.
Most layout builder SectionComponents are just "blocks" in the traditional Drupal sense of that entity. With that said, there is one new concept that should be introduced, which is whether or not blocks are to be considered "re-usable." Re-usable blocks are the ones you normally create from Structure > Blocks > Custom Block Library, and you then place to be "re-used" across the website, for example on a sidebar on every page.
Non-re-usable blocks are those which are created when you insert block content into a Layout Builder layout. The difference between these two is really just a boolean (and hidden) field on the block, which helps filter blocks using the UI.
And, the very last piece of the storage puzzle to be aware of is the "inline_block_usage" table. This simply stores the block_content_id, the layout_entity_type (ex.g. "node"), and the layout_entity_id (ex.g. "node id"). It's a record of where the non-re-usable blocks are, in fact, used.
OK, so let's do this!
We need to transform Drupal 7 node bodies into blocks, and then migrate the pages into pages, where the "body" of the node is now the Layout Builder overrides.
To do this, we are going to:
migrate bodies into non-re-usable blocks
migrate the nodes into nodes
be sure and link up the previously migrated blocks as Layout Builder Sections/SectionComponents
To help demonstrate these concepts, I've created a fully-functional website repo on Drupal 9 using some CSVs as a source. I'm going to dissect some of the main parts of that for you.
Step 1: Import the Blocks
In many ways, this is a very standard block migration, but the special thing to call your attention to is the "reusable" field in the "process" section:
# whether or not it's reusable
This specifies that the blocks coming in are inline blocks. You may or may not want to use this, but we certainly did, and this is how you set it.
Step 2: Import the Nodes
In many ways, you are just migrating node fields in the way you normally would, mapping fields like title, uid, etc.
Where this one gets special is that we migrate into a field called
layout_builder__layout which is the field that stores the overrides. With that, fields expects a Section object (or an array of Sections).
# This is the layout_builder__layout field, which stores everything!
# Where do we get them from? This `components` field comes from us. We use prepareRow to set it.
# We need a custom plugin to correctly map this.
The source for where to get the "body" (blocks / SectionComponents for our Section) is this "components" field. That's not a field in my CSV, it's one where I do a lookup to get all the blocks that were migrated in relative to this node. To do this, I use the prepareRow() method provided by migrate_tools to add a new source property.
# Basics about the source plugin - where to get the data,
# what kind it is, describe the columns in the csv.
In this new prepareRow method, we can look up the migrated blocks and return them in the correct order; each will become a section component:
Now, the components source field is an array of (non-re-usable) block IDs.
Now, we can use that with our custom plugin which is a Migrate API Process Plugin.
Where the Magic Happens
The process plugin has a main entry point of transform(). This method is responsible for returning a value formatted in the way that the destination plugin expects it. In our case, we need to return a Section (or perhaps an array of Sections if you're feeling adventurous). Remember that SectionsComponents primarily make up Sections, we need to first build up the SectionComponents themselves.
To do this, we need access to the UUID generator service in Drupal, and to create a configuration array for the SectionComponent. The following array details the configuration.
id: the plugin and derivative you're using, specifically for us "inline_block" and then the bundle, yielding "inline_block:basic" (the type of block).
label: what the label of this block is (the block title). This is a required field, so set it to something.
provider: layout_builder - always the same in our case.
label_display: whether or not to show the label (boolean)
view_mode: which view mode to use when displaying this block
block_revision_id: the revision ID of the block to display
block_serialized: the serialized version of the block (you can probably leave this null and it will be serialized for you later)
context_mapping: to be perfectly honest I don't know what this is and maybe someone out there can explain it to me, but it works when it's an empty array :)
After creating your SectionComponents array, you can return a new Section object by specifying the layout you're using for that section, any settings for the Section, and the array of SectionComponents to put into it.
Try it for Yourself!
If you download the example repo, you can restore the included DDEV database snapshot (if using DDEV) or use the .sql file to import the database. You may need to change the paths in your migrations depending on your setup.
As always feel free to be in touch if you would like to learn more!