Migrating from WordPress to Backdrop CMS

Drupal Camp Asheville 2024 - July 12th-14th

By Justin Keiser

I work for a non-profit organization called the Academy of Model Aeronautics that supports the hobby of model aviation. We have a collection of websites that use WordPress, Drupal, and Backdrop. We also have a foundation that raises money to support the organization and the hobby.

Below are screenshots of the old website and the new website.

The old website built in WordPress.

Old WordPress homepage 

new ama foundation website

New Homepage with a Hero image and branded colors.

The director of the foundation wanted to refresh the website. It used WordPress and being fonder of Drupal and Backdrop CMS, I wanted to move it to one of those platforms. I wanted content types, and views, and did not want to pay for plugins. So, I chose Backdrop. Migrating content from WordPress can be quick and easy, and this article is meant to help with that.

About Backdrop CMS

Backdrop CMS is a fork of Drupal and retains many Drupal 7 traits while incorporating features found in Modern Drupal like Configuration Management. There is an upgrade path from Drupal 7, which I did not need in this case, but would be very helpful for anyone upgrading from Drupal 7. Many modules are incorporated into Backdrop core including, Pathauto, Admin Menu, Token, and Entity Reference, as well as over 1000 contributed projects. There is perhaps a misconception that Backdrop is only for simple projects. Backdrop is not just Drupal 7, it is Drupal 7 with ten years of development and improvements. Personally, I spend more time contributing to Backdrop than Drupal. I have multiple contributed modules ported from Drupal that I am listed as the maintainer on.

Steps for Migrating from Wordpress

  1. I knew from experience that modules existed to migrate from WordPress to Drupal. I found that the WordPress Import module was available for use in Backdrop. I installed the module in a vanilla installation of Backdrop using the Project Installer included in Backdrop core. I went to the WordPress site and exported the site contents.
Wordpress dashboard for exporting content
 WordPress Dashboard for exporting content 
  1. Then there is a UI inside Backdrop for importing from WordPress.

WordPress Import UI

Ability to rollback imports: another great part of the UI
  1. Select where to send the content.

WordPress Import UI

Importing as filtered HTML helps with filtering out some of the random tags added by WordPress.
  1. There is a caveat with the images that I will explain, but the images do get imported. A hint here might be that the /wp_images directory should just be removed so the files go directly into the files folder.

WordPress Import 

WordPress import for the files 
  1. There is a need to create a Category taxonomy if wanting to retain the categories from WordPress. This field must also be added to the Post content type. After doing that there will be an option to import the categories and tags.

WordPress Import UI 

It is strange the text under both vocabularies says "all categories" 
  1. After configuring the users and clicking Finish, then poof - you've got content! There's nothing to it.

Backdrop CMS content page. 

About those images

In the imported posts the images work, but when going to the manage files tab under content there is nothing there.

Backdrop CMS UI

There are images but Backdrop doesn't know about them

The old images work but are not reusable. I found that I could move the imported images to another folder and use the Bulk Media Upload Module to reupload them back to the files folder. The paths to the images in all the body fields are retained. The module can be a bit confusing because there is a need to upload the files to a content type. To accomplish this, I created a content type for images and one for documents.

One downside to using the WordPress Import module and then finagling the image files is that the URLs to the files are often very long and not what Backdrop or Drupal would create naturally.

Moving Content

The AMA Foundation awards scholarships yearly. On the WordPress site, every scholarship winner normally had a blog post. Then – because Views doesn’t really exist– a page was manually created listing all the winners and linking to the blogs.

This called for consolidating the scholarship winners into a content type and assembling a view for listing the winners by year. The Node Convert module was used to move the scholarship blogs into another content type.

Also, many blog posts did not have tags or were in the “Uncategorized” Category. I used a combination of manual editing, along with the Autotag module and Views Bulk Operations to add tags to blog posts. This made the content much more useful.

Theming

Drupal has a new Starterkit theme to easily create a new theme from an existing theme. Backdrop has something similar. I chose the Bootstrap 5 Lite theme and I created a subtheme with the Devel Subthemer module. Eventually, the subtheme became a completely custom theme, as show below:

Devel Subthemer

User interface for creating a subtheme

Bricks

After migrating content, altering the structure, and building a theme there was a change in requirements. In front of the International Aeromodeling Center there is a sidewalk containing personalized bricks. People can purchase a brick and add a personalized message.

Walk of Fame Bricks

'Walk of Fame' Bricks 

For the Walk of Fame, AMA sells three different kinds of bricks which vary by color and size. We wanted website users to be able to search for and view bricks, as displayed above. I learned there was a spreadsheet containing information for the nearly 2000 personalized bricks. Obviously, entering those in the website by hand would be time consuming. Thankfully Backdrop and Drupal have a ready-made solution for turning CSV files into Nodes with the Feeds module. I used this simple tutorial on YouTube to learn how to do that.

  1. I created three different content types along with fields matching the spreadsheet to make the imports clean.

 content types for bricks

  1. After the imports and some CSS wrangling I got a close approximation to the actual bricks without needing individual brick photos. I printed the field contents on a background image of the brick.

$100 brick 

Brick 100 content type

The bricks were assembled with a view that had an exposed filter to search by the contents of the brick. Everything seemed fine until I learned that we wanted to have a kiosk where members could look up bricks before finding them on the walk.

Decoupled Backdrop

The solution for the kiosk could’ve been just using the website in a locked down form so only the brick search was usable. However, I knew I could create an app that only contained the brick search. After attending numerous DrupalCamps and hearing about decoupled applications and blocks I thought this was the way to proceed. I viewed a tutorial on Drupalize.me which got things started. Then I found a wonderful live demo from Drupal South on React Query. I pretty much ended up following that completely.

The problem was that each tutorial was for Drupal and I was using Backdrop. Drupal has the JSON:API module in Core, but Backdrop does not. Luckily, there is a contributed solution with the Headless module. The output of the view is slightly different than Drupal but it still works.

View created for exporting content as JSON
View created for exporting content as JSON.

Example of one brick 

{
            "nid": "30723",
            "node_title": "Brick Number: 1",
            "vns_node_content": "Brick Number: 011.0 ",
            "field_data_field_first_line_of_brick_100_node_entity_type": "node",
            "field_data_field_second_line_of_brick_100_node_entity_type": "node",
            "field_data_field_brick_background_100_node_entity_type": "node",
            "field_data_field_first_line_of_brick_500_node_entity_type": "node",
            "field_data_field_second_line_of_brick_500_node_entity_type": "node",
            "field_data_field_brick_background_1000_node_entity_type": "node",
            "field_data_field_brick_background_500_node_entity_type": "node",
            "field_data_field_second_line_of_brick_1000_node_entity_type": "node",
            "field_data_field_third_line_of_brick_1000_node_entity_type": "node",
            "field_data_field_fourth_line_of_brick_1000_node_entity_type": "node",
            "field_data_field_fifth_line_of_brick_1000_node_entity_type": "node",
            "field_data_field_first_line_of_brick_1000_node_entity_type": "node",
            "field_data_field_third_line_of_brick_100_node_entity_type": "node",
            "field_data_field_third_line_of_brick_500_node_entity_type": "node",
            "_field_data": {
                "nid": {
                    "entity_type": "node",
                    "entity": {
                        "is_active_revision": "1",
                        "nid": "30723",
                        "vid": "30777",
                        "type": "brick_100",
                        "langcode": "und",
                        "title": "Brick Number: 1",
                        "uid": "17",
                        "status": "1",
                        "created": "1674852110",
                        "changed": "1674852110",
                        "scheduled": "0",
                        "comment": "1",
                        "promote": "0",
                        "sticky": "0",
                        "tnid": "0",
                        "translate": "0",
                        "revision_timestamp": "0",
                        "revision_uid": "0",
                        "in_preview": null,
                        "log": "Created by FeedsNodeProcessor",
                        "comment_close_override": "0",
                        "body": [],
                        "field_brick_background_100": [],
                        "field_first_line_of_brick_100": {
                            "und": [
                                {
                                    "value": "HORRACE CAIN",
                                    "format": "plain_text",
                                    "safe_value": "HORRACE CAIN"
                                }
                            ]
                        },
                        "field_first_name_brick_purchase": {
                            "und": [
                                {
                                    "value": "Horrace",
                                    "format": "plain_text",
                                    "safe_value": "Horrace"
                                }
                            ]
                        },
                        "field_last_name_brick_purchase": {
                            "und": [
                                {
                                    "value": "Cain",
                                    "format": "plain_text",
                                    "safe_value": "Cain"
                                }
                            ]
                        },
                        "field_second_line_of_brick_100": {
                            "und": [
                                {
                                    "value": "539",
                                    "format": "plain_text",
                                    "safe_value": "539"
                                }
                            ]
                        },
                        "field_third_line_of_brick_100": [],
                        "path": {
                            "pid": "30854",
                            "source": "node/30723",
                            "alias": "brick-100/brick-number-1-1",
                            "langcode": "und",
                            "auto": true
                        },
                        "cid": "0",
                        "last_comment_timestamp": "1674852110",
                        "last_comment_name": null,
                        "last_comment_uid": "17",
                        "comment_count": "0",
                        "name": "admin",
                        "picture": "0",
                        "data": "b:0;"
                    }
                }
            },
            "field_field_first_line_of_brick_100": [
                {
                    "rendered": {
                        "#markup": "HORRACE CAIN",
                        "#access": true
                    },
                    "raw": {
                        "value": "HORRACE CAIN",
                        "format": "plain_text",
                        "safe_value": "HORRACE CAIN"
                    }
                }
            ],
            "field_field_second_line_of_brick_100": [
                {
                    "rendered": {
                        "#markup": "539",
                        "#access": true
                    },
                    "raw": {
                        "value": "539",
                        "format": "plain_text",
                        "safe_value": "539"
                    }
                }
            ],
            "field_field_brick_background_100": [
                {
                    "rendered": {
                        "#theme": "image_formatter",
                        "#item": {
                            "uri": "public://default_images/brick-100_0.png",
                            "is_default": true,
                            "alt": "",
                            "title": ""
                        },
                        "#image_style": "thumbnail",
                        "#path": "",
                        "#access": true
                    },
                    "raw": {
                        "uri": "public://default_images/brick-100_0.png",
                        "is_default": true,
                        "alt": "",
                        "title": ""
                    }
                }
            ],
            "field_field_first_line_of_brick_500": [],
            "field_field_second_line_of_brick_500": [],
            "field_field_brick_background_1000": [],
            "field_field_brick_background_500": [],
            "field_field_second_line_of_brick_1000": [],
            "field_field_third_line_of_brick_1000": [],
            "field_field_fourth_line_of_brick_1000": [],
            "field_field_fifth_line_of_brick_1000": [],
            "field_field_first_line_of_brick_1000": [],
            "field_field_third_line_of_brick_100": [],
            "field_field_third_line_of_brick_500": []
        },

Once the output of the Headless module was combined with the techniques in both the Drupalize.me and Drupal South tutorials, I had a decoupled block that could be embedded in any Backdrop site. With some minor modifications, the module became both a Drupal module and a standalone React App. Including the normalize function because it turned out to be the hardest part.


const normalizeBricks = (results, total_items, items_per_page, total_pages, current_page) => {
  const bricks = []
  results.forEach(item => {
    bricks.push({
      total_items: total_items,
      total_pages: total_pages,
      items_per_page: items_per_page,
      current_page: current_page,
      id: item.nid,
      title: item.node_title,
      path: item._field_data.nid.entity.path.alias,
      firstLine100: item.field_field_first_line_of_brick_100[0]?.raw.value,
      secondLine100: item.field_field_second_line_of_brick_100[0]?.raw.value,
      thirdLine100: item.field_field_third_line_of_brick_100[0]?.raw.value,
      background100: item.field_field_brick_background_100[0]?.raw.uri,
      firstLine500: item.field_field_first_line_of_brick_500[0]?.raw.value,
      secondLine500: item.field_field_second_line_of_brick_500[0]?.raw.value,
      thirdLine500: item.field_field_third_line_of_brick_500[0]?.raw.value,
      background500: item.field_field_brick_background_500[0]?.raw.uri,
      firstLine1000: item.field_field_first_line_of_brick_1000[0]?.raw.value,
      secondLine1000: item.field_field_second_line_of_brick_1000[0]?.raw.value,
      thirdLine1000: item.field_field_third_line_of_brick_1000[0]?.raw.value,
      fourthLine1000: item.field_field_fourth_line_of_brick_1000[0]?.raw.value,
      fifthLine1000: item.field_field_fifth_line_of_brick_1000[0]?.raw.value,
      background1000: item.field_field_brick_background_1000[0]?.raw.uri,
    })
  })
  return bricks
}

As a result, we have an app at https://bricks.modelaircraft.org powered by Backdrop and React running on a kiosk in our lobby.

Our new app powered by Backdrop and React running on a kiosk in the Academy of Model Aeronautic's lobby.

After all of that, we launched the new https://amafoundation.modelaircraft.org, migrating it from WordPress to Backdrop CMS.