Migrating From Drupal To WordPress Guide

With Drupal the End Of Life (EOL) for versions 7 and 8, you may be among the many website owners deciding whether to migrate to Drupal 9 or WordPress. Either option means a significant investment of time (and resources), especially if your Drupal website has a lot of custom content types, fields and modifications. Another consideration is whether the modules you relied on in your older version of Drupal are still supported in Drupal 9 or whether comparable plugins are available in WordPress.

If you are reading this article, then you have probably already decided to use WordPress. One of the main benefits to using WordPress over Drupal is the ability to activate automatic updates for core and plugins. Drupal developers have debated this functionality, rationalizing that automatic updates could break a website. But honestly, repairing a website due to an update conflict is one hundred times easier than digging out from under a hacked, compromised website because manual updates weren’t done quickly enough. Remember drupalgeddon?

A few of the downsides to using WordPress is that it was created to be a blog. As such, the WordPress directory structure for uploaded media is organized by year/month. It’s URL structure (permalinks) can be quirky and requires tweaking in order to work with custom post types and custom taxonomies. And its need to create 7 variations of every image can eat up server space fairly quickly. Fortunately, these downsides can be modified via WordPress functions.php file to suit your needs.

WordPress Migration Options?

How you proceed with your migration to WordPress really depends on your technical skills (and budget). Of course, you can manually recreate all your Drupal content within WordPress. That would probably be the best option if you don’t have hundreds of articles. Otherwise, it could take weeks to manually recreate all that content and corresponding media within WordPress.

If you are comfortable with using Drupal’s Views module to export data, or are fine with getting under the hood of phpMyAdmin and using MySQL queries (specifically for exporting data to csv files), there are several plugins that can make the process a lot quicker and easier (even almost painless).

You can use Drupal’s “Views Data Export” module to export data from views to a CSV file. Some example queries for manually exporting content from your database using queries are available at the end of this article. We’ve also included a Drupal Views file you can import into Views to use as a starting point to export to CSV using Views Data Export module.

If you would prefer not to use phpMyAdmin and MySQL queries to export your content or want to use Drupal Views to export your content, you have the option of using WordPress plugins that will handle much of this for you. However, if you have custom Drupal content types and taxonomies, you may need to also use paid Pro versions of these plugins to pull most of that data in.

With either option, you will need to have a WordPress website set up and prepared to accept this data.

Planning Migration

One of the benefits of migration is that your Drupal database has probably gotten a little unwielding over time. The core data for articles, taxonomy, media, and fields are very important. But the settings for options for the countless modules that have probably been installed/uninstalled over time and may not all be needed any longer have just taken up space and bogged/slowed down your database.

This is also a problem that can happen with WordPress if you bog it down with countless plugins. Use this time to select only the plugins you absolutely need. Keep your database clean and lean.  Another thing to look for are plugins that allow you to create functionality or make changes, and the plugin can be deleted afterward without losing that functionality.

Planning is probably the most time-intensive process. You will want to have all the WordPress plugins that you’ll need for your website’s functionality already installed and configured before importing any data. Deciding which WordPress plugins are the most comparable to Drupal modules you were using will take a bit of trial and error. You will definitely find yourself installing (and uninstalling) a lot of plugins until you get exactly what you need. We have a recommended list of WordPress Plugins That Are Comparable To Drupal Modules that might help you find the plugins you need.

You will also need to transfer any custom Drupal template coding to the corresponding WordPress files.  Fortunately, WordPress uses a template hierarchy structure which is somewhat similar to Drupal.

The Basics Of Migration

  • Create an instance of WordPress for development purposes and configure all the settings as needed.
  • Install and configure WordPress plugins that are comparable to the Drupal modules you used and will need for similar functionality in WordPress.
  • Install a WordPress theme that closely resembles your Drupal theme, or create a custom theme (How To Create A Simple WordPress Theme From Scratch). There’s no need to drill down the finer details of your theme yet; just enough to give you a visual example of how your data will be displayed on the website. You can fine tune everything in your theme later.
  • Create a few sample posts to see if everything you need is in place and displays as intended.
  • If you will be using a plugin to migrate your content, import only one post initially to ensure everything works as it should.
  • Migrate your content and media to your new WordPress installation.

DIY WordPress Migration With Free Plugins Or Upgrade To Pro Plugins

If you don’t want to export csv or xml data from your Drupal database either manually or using Drupal Views, you really only have 3 options.

  1. Either manually recreate every Drupal article as a WordPress post
  2. Hire a company to perform the migration for you
  3. Use a paid or free plugins such as FG Drupal to WordPress

If you are able to manually export csv from Drupal, Ultimate CSV Importer Free is a strong option for content migration.

FG Drupal to WordPress

FG Drupal to WordPress is a good option if your Drupal content is all contained in the default article content type. It will also save your redirects (great for SEO). Your taxonomy, users, nodes and media import will all be done for you.

We tried the paid version of FG and liked how our default article nodes, taxonomies and media were imported. It was quick and easy and you have the option to select what to (or not) import. If you have custom content types, custom fields, and/or custom taxonomies, you will also need to purchase additional plugins to import those for you. We had a bunch of custom content, which the paid version imported but weren’t applied to corresponding plugins. We found we needed several other paid plugins in addition to FG Drupal to WordPress in order to import and display all our Drupal content. But our default articles, media and content were imported within minutes. It didn’t fit our specific needs due to all the custom content and fields on our Drupal site, but is a good investment if you don’t have a lot of customization on your Drupal website and are not comfortable using MySQL queries.

If you plan to export/import your Drupal content on your own, there are two great free plugins (with paid options) to help facilitate the process. We’ve tested both of them (the free versions) using an exported CSV file and we’ll start with the highlights of our favorite.

Ultimate CSV Importer Free

Ultimate CSV Importer Free provides the most import options available for free to get your Drupal content imported into a WordPress website, especially if you’ve already activated all the plugins you’ll need. It provides a mapping feature which even mapped our exported custom Drupal content types and fields to the WordPress plugins we installed, including All SEO. The option to import into WordPress Custom Fields was available as long as we weren’t using the Advanced Custom Fields plugin (this may have changed during their most recent update). We managed to get the bulk of data we needed inserted into each corresponding WordPress post. The only thing missing from the free version that we needed was the ability to import media.

All Import

All Import was our second favorite. It also offers a drag and drop interface which made importing nodes to post almost enjoyable. Their drag and drop interface lets you include exported data easily, plus you can format and preview your posts before you import. It also allows a post to have more than one category. The only reason it isn’t our favorite is because mapping to custom fields wasn’t an option unless we purchased the pro version. It’s a great option if you don’t have custom content types or custom fields. But if you have custom fields that can just be included in your post’s body, you can easily just pop it in there rather than use custom fields. Like Ultimate CSV Importer Free, it also required the pro version in order to import media.

Exporting Your Drupal Content To CSV

The basic data you will want to export from Drupal is:

  • Drupal Nid (this is only used as a point of reference)
  • Drupal Title
  • Drupal Created Date
  • Drupal Author uid
  • Drupal Body
  • Drupal Article Image Path
  • Drupal Taxonomy Terms (comma separated list if a node has multiple taxonomies)
  • Drupal Article Path
  • Drupal Content Type (if you have custom Drupal Content Types and want to replicate that in WordPress as custom Post Types)

Using Drupal Views Data Export Module

If you decide to use Drupal’s Views Data Export to manually export a csv list of your content, and you are already displaying views of your content on Drupal, you may only need to create the CSV export under the existing View rather than create a new view from scratch.  A guide as well as a Views Import are available at the end of this article.

Using phpMyAdmin

Create a csv file with headers by exporting your node data via phpMyAdmin. The headers are important because you won’t have to guess what the content of each CSV columns. If you had a lot of custom fields in your content, also include them in your export. Rename the export columns in your MySQL query using “AS” statements to clearly identify what the column contains. You will want to already have these custom fields created in WordPress or have the plugins installed that will need this custom content before you begin the import.

Once you have your CSV file, open it and preview the results.

  • Check for any articles that are missing fields (such as taxonomy)
  • Find and replace the url for any inline images within the body and the image column. You will want to replace the url containing Drupal’s old file structure to the new WordPress url. ie https://mysite.com/sites/default/files/images/myimage.jpg to https://mysite.com/wp-content/uploads/images/myimage.jpg

Pro tip! Create a test CSV file that contains only one article to import. Import the test CSV and then view the new WordPress post to ensure everything is working as intended.

What To Do With The Media That Isn’t Imported In The Free Versions Of Import Plugins

You will need to manually move all your article images to the WordPress image folder. Before moving the images, this would be the perfect time to optimize all your image sizes.
You can import them individually via WordPress admin menu by selecting the Media tab, or just ftp them over to your WordPress image folder.

Obviously, you have the option to buy the pro version of the import plugins to address media imports. The starting price for both is $99. That’s really not bad considering how much time you might need to invest to do it manually.

But if you are up to doing it manually, some code is available at the end of this article that can help.

Using Drupal Views Data Export Module Guide

You will need to install Views Data Export.  If you already have a View that contains all your content data, just add the CSV option to that view.  Otherwise, you can use our Views Data Export code to import into Views.  This will create a CSV Views export containing the following Drupal content data:

Drupal Title
Drupal Nid
Drupal Created Date
Drupal Author uid
Drupal Body
Drupal Article Image Path
Drupal Taxonomy Terms
Drupal Article Path
Drupal Content Type

Import this code into Views:

Views Data Export

$view = new view();
$view->name = 'data_export';
$view->description = '';
$view->tag = 'default';
$view->base_table = 'node';
$view->human_name = 'Data Export';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */

/* Display: Master */
$handler = $view->new_display('default', 'Master', 'default');
$handler->display->display_options['title'] = 'Article Data Export';
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['access']['perm'] = 'access administration menu';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['query']['options']['distinct'] = TRUE;
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'none';
$handler->display->display_options['pager']['options']['offset'] = '0';
$handler->display->display_options['style_plugin'] = 'table';
/* Relationship: File Usage: File */
$handler->display->display_options['relationships']['node_to_file']['id'] = 'node_to_file';
$handler->display->display_options['relationships']['node_to_file']['table'] = 'file_usage';
$handler->display->display_options['relationships']['node_to_file']['field'] = 'node_to_file';
/* Field: Content: Title */
$handler->display->display_options['fields']['title']['id'] = 'title';
$handler->display->display_options['fields']['title']['table'] = 'node';
$handler->display->display_options['fields']['title']['field'] = 'title';
$handler->display->display_options['fields']['title']['label'] = 'Drupal Title';
$handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE;
$handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE;
$handler->display->display_options['fields']['title']['alter']['strip_tags'] = TRUE;
$handler->display->display_options['fields']['title']['link_to_node'] = FALSE;
/* Field: Content: Nid */
$handler->display->display_options['fields']['nid']['id'] = 'nid';
$handler->display->display_options['fields']['nid']['table'] = 'node';
$handler->display->display_options['fields']['nid']['field'] = 'nid';
$handler->display->display_options['fields']['nid']['label'] = 'Drupal Nid';
/* Field: Content: Post date */
$handler->display->display_options['fields']['created']['id'] = 'created';
$handler->display->display_options['fields']['created']['table'] = 'node';
$handler->display->display_options['fields']['created']['field'] = 'created';
$handler->display->display_options['fields']['created']['label'] = 'Drupal Created Date';
$handler->display->display_options['fields']['created']['date_format'] = 'custom';
$handler->display->display_options['fields']['created']['custom_date_format'] = 'm-d-Y';
$handler->display->display_options['fields']['created']['second_date_format'] = 'long';
/* Field: Content: Author uid */
$handler->display->display_options['fields']['uid']['id'] = 'uid';
$handler->display->display_options['fields']['uid']['table'] = 'node';
$handler->display->display_options['fields']['uid']['field'] = 'uid';
$handler->display->display_options['fields']['uid']['label'] = 'Drupal Author uid';
$handler->display->display_options['fields']['uid']['link_to_user'] = FALSE;
/* Field: Content: Body */
$handler->display->display_options['fields']['body']['id'] = 'body';
$handler->display->display_options['fields']['body']['table'] = 'field_data_body';
$handler->display->display_options['fields']['body']['field'] = 'body';
$handler->display->display_options['fields']['body']['label'] = 'Drupal Body';
/* Field: File: Path */
$handler->display->display_options['fields']['uri']['id'] = 'uri';
$handler->display->display_options['fields']['uri']['table'] = 'file_managed';
$handler->display->display_options['fields']['uri']['field'] = 'uri';
$handler->display->display_options['fields']['uri']['relationship'] = 'node_to_file';
$handler->display->display_options['fields']['uri']['label'] = 'Drupal Article Image Path';
$handler->display->display_options['fields']['uri']['file_download_path'] = TRUE;
/* Field: Content: All taxonomy terms */
$handler->display->display_options['fields']['term_node_tid']['id'] = 'term_node_tid';
$handler->display->display_options['fields']['term_node_tid']['table'] = 'node';
$handler->display->display_options['fields']['term_node_tid']['field'] = 'term_node_tid';
$handler->display->display_options['fields']['term_node_tid']['label'] = 'Drupal Taxonomy Terms';
$handler->display->display_options['fields']['term_node_tid']['link_to_taxonomy'] = FALSE;
$handler->display->display_options['fields']['term_node_tid']['vocabularies'] = array(
'category' => 0,
'days_categories' => 0,
'days_type' => 0,
);
/* Field: Content: Type */
$handler->display->display_options['fields']['type']['id'] = 'type';
$handler->display->display_options['fields']['type']['table'] = 'node';
$handler->display->display_options['fields']['type']['field'] = 'type';
$handler->display->display_options['fields']['type']['label'] = 'Drupal Content Type';
/* Sort criterion: Content: Post date */
$handler->display->display_options['sorts']['created']['id'] = 'created';
$handler->display->display_options['sorts']['created']['table'] = 'node';
$handler->display->display_options['sorts']['created']['field'] = 'created';
$handler->display->display_options['sorts']['created']['order'] = 'DESC';
/* Filter criterion: Content: Published status */
$handler->display->display_options['filters']['status']['id'] = 'status';
$handler->display->display_options['filters']['status']['table'] = 'node';
$handler->display->display_options['filters']['status']['field'] = 'status';
$handler->display->display_options['filters']['status']['value'] = 1;
$handler->display->display_options['filters']['status']['group'] = 1;
$handler->display->display_options['filters']['status']['expose']['operator'] = FALSE;
/* Filter criterion: Content: Type */
$handler->display->display_options['filters']['type']['id'] = 'type';
$handler->display->display_options['filters']['type']['table'] = 'node';
$handler->display->display_options['filters']['type']['field'] = 'type';
$handler->display->display_options['filters']['type']['value'] = array(
'article' => 'article',
);

/* Display: Article Query */
$handler = $view->new_display('page', 'Article Query', 'page');
$handler->display->display_options['defaults']['filter_groups'] = FALSE;
$handler->display->display_options['defaults']['filters'] = FALSE;
/* Filter criterion: Content: Type */
$handler->display->display_options['filters']['type']['id'] = 'type';
$handler->display->display_options['filters']['type']['table'] = 'node';
$handler->display->display_options['filters']['type']['field'] = 'type';
$handler->display->display_options['filters']['type']['value'] = array(
'article' => 'article',
);
$handler->display->display_options['path'] = 'data-export';

/* Display: Article Data export */
$handler = $view->new_display('views_data_export', 'Article Data export', 'views_data_export_1');
$handler->display->display_options['pager']['type'] = 'none';
$handler->display->display_options['pager']['options']['offset'] = '0';
$handler->display->display_options['style_plugin'] = 'views_data_export_csv';
$handler->display->display_options['style_options']['provide_file'] = 0;
$handler->display->display_options['style_options']['parent_sort'] = 0;
$handler->display->display_options['style_options']['quote'] = 1;
$handler->display->display_options['style_options']['trim'] = 0;
$handler->display->display_options['style_options']['replace_newlines'] = 0;
$handler->display->display_options['style_options']['newline_token'] = '1';
$handler->display->display_options['style_options']['header'] = 1;
$handler->display->display_options['style_options']['keep_html'] = 1;
$handler->display->display_options['path'] = 'article-export.csv';

 

Basic MySQL Query For Node Export

(note: If you receive errors, validate csv using https://csvlint.io/)

This query provides the following

Drupal Title => Post Title
Drupal Taxonomy => Category
Node ID => Post ID
Date Created => Created
Image (minus Drupal path) => Featured Image (this is only used if you have the Pro versions of WordPress import plugins)
Author => Author
Drupal Alias => Post Slug (for permalinks)
Drupal Body => Content
Drupal Node Type (just as reference if you have custom content types)

If you already know what WordPress directory you will be uploading your images and other media, insert the folder location at the end of replace(file_managed.uri, ‘public://field/image/’, ”).  For example, if you configured all your WordPress images to be located in /wp-content/uploads/images = replace(file_managed.uri, ‘public://field/image/’, ‘/wp-content/uploads/images’)

SELECT DISTINCT node.title AS drupal_title, GROUP_CONCAT(name) AS drupal_taxonomy, node.nid AS nid, from_unixtime(node.created,'%Y-%m-%d') AS created, replace(file_managed.uri, 'public://field/image/', '') AS drupal_image, node.uid AS drupal_uid, node.type AS node_type, alias AS drupal_url, body_value AS drupal_body
FROM
node
LEFT JOIN file_usage ON node.nid = file_usage.id AND (file_usage.type = 'node')
LEFT JOIN file_managed ON file_managed.fid = file_usage.fid
LEFT JOIN `field_data_body` ON `field_data_body`.entity_id = node.nid
LEFT JOIN `taxonomy_index` ON `taxonomy_index`.nid = node.nid
LEFT JOIN `taxonomy_term_data` ON `taxonomy_term_data`.tid = `taxonomy_index`.tid AND `taxonomy_index`.nid = node.nid
LEFT JOIN `url_alias` ON `url_alias`.source = CONCAT( 'node/', node.nid )

WHERE (( (node.status = '1') AND (node.type IN ('article')) ))
group by nid
ORDER BY nid

Images

If you are not using the Pro versions of WordPress importer plugins, you’ll likely have to import all your images manually. This example demonstrates a basic method to do this. There are probably much more eloquent ways to accomplish this, but it works for us.

1 Populate the necessary post image attachment fields in WordPress

1 Create a drupal image export to drupal.csv
2 Create a wordpress post id export to wordpress.csv
3 Copy then paste cell contents of the wordpress.csv “ID” column into the drupal.csv “post_parent” column
4 Use phpMyAdmin to import the modified drupal.csv into WordPress database

This MySQL query extracts a Drupal article’s image, and relevant data needed to match up with the new WordPress Post ID

The MySQL query to Drupal article images export to drupal_images.csv (replace the guid path to match your wordpress image directory)

Images – Extract Image Data From Drupal Posts (drupal_image.csv)

This query not only exports Drupal images, but reformats the data so that the exported csv file can be used to import into WordPress using phpMyAdmin’s Import feature, after you append the WordPress Post IDs. We’re just using the article’s title to populate the image’s title, but you could expand the query to use the title you set within Drupal.  The additional fields are needed for WordPress field mapping.

SELECT DISTINCT node.uid AS post_author, from_unixtime(node.created,'%Y-%m-%d') AS post_date, post_status AS "inherit", comment_status AS "closed", ping_status AS "closed", node.title AS post_title, node.title AS post_content, replace(file_managed.uri, 'public://field/image/', 'https://mysite.com/wp-content/uploads/') AS guid, "attachment" AS post_type, "image/jpeg" AS post_mime_type, "" AS post_parent
FROM
node
LEFT JOIN file_usage ON node.nid = file_usage.id AND (file_usage.type = 'node')
LEFT JOIN file_managed ON file_managed.fid = file_usage.fid
WHERE (( (node.status = '1') AND (node.type IN ('article')) ))
group by nid
ORDER BY node.title DESC

Images – Extract Image Data From Drupal Posts (drupal-postmeta.csv)

This query will be used to extract data from Drupal to drupal-postmeta.csv, which will be used to populate WordPress wp-postmeta table

SELECT DISTINCT replace(file_managed.uri, 'public://field/image/', '') AS meta_value, "" AS post_id
FROM
node
LEFT JOIN file_usage ON node.nid = file_usage.id AND (file_usage.type = 'node')
LEFT JOIN file_managed ON file_managed.fid = file_usage.fid
WHERE (( (node.status = '1') AND (node.type IN ('article')) ))
group by nid
ORDER BY node.title DESC

Use this MySQL query to extract to wordpress.csv the WordPress Post ID and Post Title of posts you previously imported. You should exclude any Posts that you have added such as Privacy Policy or test posts

Images – Get The Post ID For Newly Imported WordPress Posts (wordpress.csv)

SELECT `ID`, `post_title` FROM `wp_posts` WHERE `post_type` LIKE 'post' AND ID !=1 ORDER BY `post_title` DESC

Update drupal_image.csv and drupal-postmeta.csv with their corresponding WordPress Post ID numbers

Open both csv files and confirm that the order of title in each file matches exactly. If they do, copy all the cell contents in the ID column of the wordpress.csv file into the post_parent column of the drupal_image.csv file.

You will also want to paste the Post ID cell contents to the post_id column of your drupal-postmeta.csv file.

Import drupal_image.csv using phpMyAdmin

Using phpMyAdmin’s Import, import your updated drupal_image.csv fileinto the WordPress post table using the following column header

post_author,post_date,post_status,comment_status,ping_status,post_title,post_content,guid,post_type,post_mime_type,post_parent

Now time to get the new posts

SELECT `ID`, `post_title` FROM `wp_posts` WHERE `post_parent` > 0 AND `post_mime_type` LIKE '%image/jpeg%' ORDER BY `post_title` DESC

Import drupal-postmeta.csv using phpMyAdmin

For reference: https://stackoverflow.com/questions/15585063/how-to-set-wordpress-featured-images-via-sql
For csv import into post_meta for image attachment and _wp_attached_file

post_id,meta_key,meta_value

Sample MySQL Queries For Image Import

UPDATE `wp_posts`
SET post_status = ‘inherit’
WHERE `post_parent` > 0 AND `post_mime_type` LIKE ‘%image/jpeg%’

UPDATE `wp_posts`
SET comment_status = ‘closed’
WHERE `post_parent` > 0 AND `post_mime_type` LIKE ‘%image/jpeg%’

UPDATE `wp_posts`
SET ping_status = ‘closed’
WHERE `post_parent` > 0 AND `post_mime_type` LIKE ‘%image/jpeg%’

INSERT INTO `wp_posts` (`ID`, `post_author`, `post_date`, `post_date_gmt`, `post_content`, `post_title`, `post_excerpt`, `post_status`, `comment_status`, `ping_status`, `post_password`, `post_name`, `to_ping`, `pinged`, `post_modified`, `post_modified_gmt`, `post_content_filtered`, `post_parent`, `guid`, `menu_order`, `post_type`, `post_mime_type`, `comment_count`) VALUES
(26, 1, ‘2019-11-14 16:06:02’, ‘2019-11-14 16:06:02’, ‘Statueofliberty’, ‘Statueofliberty’, ‘Statueofliberty’, ‘inherit’, ‘closed’, ‘closed’, ”, ‘statueofliberty’, ”, ”, ‘2019-11-14 16:06:02’, ‘2019-11-14 16:06:02’, ”, 3, ‘https://calculate-this.com/wordpress/wp-content/uploads/statueofliberty.jpg’, 0, ‘attachment’, ‘image/jpeg’, 0);
COMMIT;

Available for Amazon Prime