Migrating from ExpressionEngine to WordPress using an XML / WXR import file

Aug 23 2012

One of my first projects here at RD2 was to migrate a site from ExpressionEngine over to WordPress. Part of that process involved making a hard decision:

“Do we migrate the content manually, create a custom database migration script, or is there a better way?”

It turned out that there was a much better way to accomplish what we were after. It was a little custom, but more importantly – it was reusable. The solution I came up with was to create a template within ExpressionEngine, that would output a WordPress XML / WXR import file. That sounds twisted, right?

Because we believe in open source and that it would truly be a disservice to the community of users and developers to keep this sort of thing to ourselves, we’ve decided to release this code snippet for everyone to use. Now thinking about migrating from ExpressionEngine to WordPress might not be such a challenge. You can tweak this to serve your own specific needs; I’ve set it up in a way that should cover enough ground for many use-cases.

Step 1

Create a new template in ExpressionEngine, put it in the root of your site, name it something like “my-export” for the URL.

Step 2

Set the “Type” to “XML”, mark the template to “Allow PHP”, allow “PHP Parse on Output”, and restrict the role to “Admin” so not just anyone can grab your data.

Step 3

Place the following code in the template and save.

<?php
// EE to WP Export template
// Run at http://site.com/template-location/CHANNEL_NAME/POST_TYPE/CONTENT_FIELD_NAME/FEATURED_IMAGE_FIELD_NAME/
// It's important to use a root level template, and restrict access to the admin role
// Set Type to XML, allow PHP, and PHP Parse on Output

$DB = &$this->EE->db;
echo '<' . '?' . 'xml version="1.0" encoding="UTF-8"' . '?' . '>';
?>

<!-- This is a WordPress eXtended RSS file generated by WordPress as an export of your blog. -->
<!-- It contains information about your blog's posts, comments, and categories. -->
<!-- You may use this file to transfer that content from one site to another. -->
<!-- This file is not intended to serve as a complete backup of your blog. -->

<!-- To import this information into a WordPress blog follow these steps. -->
<!-- 1. Log in to that blog as an administrator. -->
<!-- 2. Go to Tools: Import in the blog's admin panels (or Manage: Import in older versions of WordPress). -->
<!-- 3. Choose "WordPress" from the list. -->
<!-- 4. Upload this file using the form provided on that page. -->
<!-- 5. You will first be asked to map the authors in this export file to users -->
<!-- on the blog. For each author, you may choose to map to an -->
<!-- existing user on the blog or to create a new user -->
<!-- 6. WordPress will then import each of the posts, comments, and categories -->
<!-- contained in this file into your blog -->

<rss version="2.0"
 xmlns:excerpt="http://wordpress.org/export/1.0/excerpt/"
 xmlns:content="http://purl.org/rss/1.0/modules/content/"
 xmlns:wfw="http://wellformedweb.org/CommentAPI/"
 xmlns:dc="http://purl.org/dc/elements/1.1/"
 xmlns:wp="http://wordpress.org/export/1.0/">

<channel>
 <title>{exp:xml_encode}{site_name}{/exp:xml_encode}</title>
 <link>{site_url}</link>
 <description>Site</description>
 <pubDate>2012-03-22T00:00:00 +0000</pubDate>
 <language>en</language>
 <wp:wxr_version>1.0</wp:wxr_version>
 <wp:base_site_url>{site_url}</wp:base_site_url>
 <wp:base_blog_url>{site_url}</wp:base_blog_url>
 <generator>http://expressionengine.com/</generator>

{exp:channel:entries channel="{segment_2}" orderby="date" sort="desc" status="open|featured|home_news" limit="9999" dynamic_start="yes" cache="no" refresh="0" disable="member_data|categories|category_fields|pagination"}
 <item>
 <title>{exp:xml_encode}{title}{/exp:xml_encode}</title>
 <link>{title_permalink}</link>
 <pubDate>{entry_date format='%Y-%m-%d %h:%i:%s'}</pubDate>
 <dc:creator>{exp:xml_encode}{author}{/exp:xml_encode}</dc:creator>
 <guid isPermaLink="false"></guid>
 <description></description>
 <excerpt:encoded></excerpt:encoded>
 <wp:post_id>1000{entry_id}</wp:post_id>
 <wp:post_date>{entry_date format='%Y-%m-%d %h:%i:%s'}</wp:post_date>
 <wp:post_date_gmt>{gmt_entry_date format='%Y-%m-%d %h:%i:%s'}</wp:post_date_gmt>
 <wp:comment_status>open</wp:comment_status>
 <wp:ping_status>open</wp:ping_status>
 <wp:post_name>{exp:xml_encode}{url_title}{/exp:xml_encode}</wp:post_name>
 <wp:status>publish</wp:status>
 <wp:post_parent>0</wp:post_parent>
 <wp:menu_order>0</wp:menu_order>
 <wp:post_type>{segment_3}</wp:post_type>
 <wp:post_password></wp:post_password>
 <wp:is_sticky>0</wp:is_sticky>
 <category domain="category" nicename="news"><![CDATA[News]]></category>
<!--
 SELECT
 exp_channel_fields.field_id,
 exp_channel_fields.field_name,
 exp_channel_fields.field_type
 FROM exp_channels
 LEFT JOIN exp_channel_fields
 ON exp_channel_fields.group_id = exp_channels.field_group
 WHERE exp_channels.channel_id = {channel_id}
 AND exp_channels.site_id = {entry_site_id}
 ORDER BY exp_channel_fields.field_order
-->
<?php
 $query = $DB->query( "SELECT
 exp_channel_fields.field_id,
 exp_channel_fields.field_name,
 exp_channel_fields.field_type
 FROM exp_channels
 LEFT JOIN exp_channel_fields
 ON exp_channel_fields.group_id = exp_channels.field_group
 WHERE exp_channels.channel_id = {channel_id}
 AND exp_channels.site_id = {entry_site_id}
 ORDER BY exp_channel_fields.field_order" );
 $content = '';
 $attachment = '';
 if ( 0 < $query->num_rows ) {
 foreach( $query->result_array() as $row ) {
 $field = $DB->query( "SELECT field_id_" . $row[ 'field_id' ] . " AS field_value FROM exp_channel_data WHERE entry_id = {entry_id} AND site_id = {entry_site_id} AND channel_id = {channel_id}" );
 if ( 0 == $field->num_rows )
 continue;
 $field = current( $field->result_array() );
 $field_value = $field[ 'field_value' ];
 $field_value = str_replace( '{' . 'filedir_3' . '}', 'http://' . $_SERVER[ 'HTTP_HOST' ] . '/images/backgrounds/', $field_value);
 if ( '{segment_4}' == $row[ 'field_name' ] )
 $content = $field_value;
 if ( '{segment_5}' == $row[ 'field_name' ] )
 $attachment = $field_value;
?>
 <wp:postmeta>
 <wp:meta_key><?php echo $row[ 'field_name' ]; ?></wp:meta_key>
 <wp:meta_value><![CDATA[<?php echo $field_value; ?>]]></wp:meta_value>
 </wp:postmeta>
<?php
 }
 }
 if ( !empty( $attachment ) ) {
 ?>
 <wp:postmeta>
 <wp:meta_key>_thumbnail_id</wp:meta_key>
 <wp:meta_value><![CDATA[2000{entry_id}]]></wp:meta_value>
 </wp:postmeta>
 <?php
 }
?>
{!--
<?php
/*if (in_array('comment', $this->EE->TMPL->modules)) {
 global $IN;
 $IN->QSTR = '{entry_id}';*/
?>
 {exp:comment:entries weblog="ipr_digest" sort="asc"}
 <wp:comment>
 <wp:comment_id>{comment_id}</wp:comment_id>
 <wp:comment_author><![CDATA[{name}]]></wp:comment_author>
 <wp:comment_author_email>{email}</wp:comment_author_email>
 <wp:comment_author_url>{url}</wp:comment_author_url>
 <wp:comment_author_IP>{ip_address}</wp:comment_author_IP>
 <wp:comment_date>{comment_date format="%Y-%m-%d %h:%i:%s"}</wp:comment_date>
 <wp:comment_date_gmt>{gmt_comment_date format="%Y-%m-%d %h:%i:%s"}</wp:comment_date_gmt>
 <wp:comment_content><![CDATA[{comment}]]></wp:comment_content>
 <wp:comment_approved>1</wp:comment_approved>
 <wp:comment_type></wp:comment_type>
 <wp:comment_parent>0</wp:comment_parent>
 <wp:comment_user_id></wp:comment_user_id>
 </wp:comment>
 {/exp:comment:entries}
<?php
//}
?>
--}
 <content:encoded><![CDATA[<?php echo $content; ?>]]></content:encoded>
 </item>
<?php
 if ( !empty( $attachment ) ) {
?>
 <item>
 <title>{exp:xml_encode}{title}{/exp:xml_encode}</title>
 <link>{title_permalink}-attachment</link>
 <pubDate>{entry_date format='%Y-%m-%d %h:%i:%s'}</pubDate>
 <dc:creator>{exp:xml_encode}{author}{/exp:xml_encode}</dc:creator>
 <guid isPermaLink="false"><?php echo $attachment; ?></guid>
 <description></description>
 <content:encoded></content:encoded>
 <excerpt:encoded></excerpt:encoded>
 <wp:post_id>2000{entry_id}</wp:post_id>
 <wp:post_date>{entry_date format='%Y-%m-%d %h:%i:%s'}</wp:post_date>
 <wp:post_date_gmt>{gmt_entry_date format='%Y-%m-%d %h:%i:%s'}</wp:post_date_gmt>
 <wp:comment_status>closed</wp:comment_status>
 <wp:ping_status>closed</wp:ping_status>
 <wp:post_name>{exp:xml_encode}{url_title}{/exp:xml_encode}-attachment</wp:post_name>
 <wp:status>inherit</wp:status>
 <wp:post_parent>1000{entry_id}</wp:post_parent>
 <wp:menu_order>0</wp:menu_order>
 <wp:post_type>attachment</wp:post_type>
 <wp:post_password></wp:post_password>
 <wp:is_sticky>0</wp:is_sticky>
 <wp:attachment_url><?php echo $attachment; ?></wp:attachment_url>
 </item>
<?php
 }
?>
{/exp:channel:entries}

</channel>
</rss>

Step 4

Then simply fill in the portions of this example URL with the real parts. Parameters should be in all lowercase; I’ve capitalized them for readability.

mysite.com / my-export / CHANNEL / POST_TYPE / CONTENT_FIELD / FEATURED_IMG_FIELD

Step 5

Save the output into a file locally you get as something like “ee-import-to-wp.xml” — in a browser just use the “Save As..” option, the code above will automatically tell your browser it’s an XML file.

Step 6

Go to the WordPress Import screen located at Tools ยป Import area of your WordPress Dashboard (/wp-admin/admin.php?import=wordpress), select the file and click “Upload file and import”

Step 7

Follow the instructions on the screen from there, if prompted, choose to import attachments / images, this will save them into your WordPress site instead of linking to them on your ExpressionEngine site.

Enjoy

It’s time to sit back and enjoy, or you can rinse and repeat for additional post types / channels by following steps 4-7 again.

Tags

10 Comments to “Migrating from ExpressionEngine to WordPress using an XML / WXR import file”

  1. This is a good method and I approve, but the problem with ExpressionEngine sites is that frequently, they’re highly custom. Using various extensions of some kind to add tags, for example, or having customized author fields, etc. So the template will work only in the generic sense, it would likely have to be customized to the particular site’s requirements if the site is at all complex.

    For a migration I helped with, I wrote a PHP script to essentially read the relevant data from the EE database directly, and make the proper WP function calls to insert the data into a fresh WP database. This turned out to be easier than expected, and it’s a valid option if you know your way around PHP and can find the data you need in the EE database. For complex migrations, I’d consider that to be a better approach, simply because you can make your migration more complex to support whatever the new site that you are designing is going to be. For example, if you need to have custom taxonomies, then you can create the taxonomy in advance, then migrate the old data into the new database, and have code to preserve the taxonomy in the process.

    When doing any migration, it’s best to plan out and create the new site in advance, without any data, first. Migrating the data in should be the last step, basically. Writing a custom script to do the migration then gives you more control over the process.

    By Otto on August 23rd, 2012 at 11:41 am
  2. Totally agree, there are a few ways to go at it. The data we were migrating was pretty simple and we didn’t have to do any custom PHP / DB migration scripting, just set this up and it was reusable for the whole site’s content.

    By Scott Kingsley Clark on August 27th, 2012 at 5:38 pm
  3. What version of Expression Engine were you exporting from? I don’t see some of these options.

    By ugarles on September 21st, 2012 at 12:46 pm
  4. Specifically, my only choices for step 2 are “Web Page” “CSS Stylesheet” and “RSS page” and I’m not sure if any of them correspond to XML or are at all useful.

    By ugarles on September 21st, 2012 at 12:51 pm
  5. @ugarles I’m not sure what version of Expression Engine that it was that had those options. They may have moved them or separated them out into another area in one of the most recent updates.

    By Scott Kingsley Clark on October 15th, 2012 at 9:07 am
  6. I’m not sure what you mean by “POST_TYPE” or what I’m supposed to put in that segment. And there is no “feature image field” that we’re utilizing in this export.

    Also, there are two fields “body” and “extended”, but both need to be imported into the main body field within WordPress. Any suggestions?

    Thank you!

    By Joelle on October 23rd, 2012 at 1:05 pm
  7. Hi again, I figured out the POST_TYPE info and I finally got it to generate something, but then it stops. It also doesn’t parse the “site_name” and “site_url” info youv’e got in there.

    I’m also migrating from 1.7x, so I did change all the “channel” references to “weblog”. But it gets as far as starting to spit out the most recent post, then stops. I’d welcome your insight.

    Thanks!

    By Joelle on October 23rd, 2012 at 4:09 pm
  8. Thank you, anyway, but I decided to go with another method that was a bit less complicated for me. :)

    http://www.hopstudios.com/blog/exporting_from_expressionengine_into_wordpress

    Thank you!

    By Joelle on October 23rd, 2012 at 5:56 pm
  9. Hmmm… It looks interesting, but I can’t get it to work for me.

    The URL structure in my case is ‘domain.org/blog/entry/unique_name’.

    First of all, where do I put the template? I already have template groups such as ‘blog’, ‘css’, ‘widgets’ etc.

    If I place it inside ‘blog’ group, the URL will automatically look like ‘domain.org/blog/my-export’. How would it work?

    Then, I’m not sure what ‘POST_TYPE’ is. Is it ‘entry’ in my case?

    What about ‘CONTENT_FIELD’? Here’s what I have in the blog channel:

    ‘blog_intro’ (this one is what shows up on the front page)
    ‘blog_image’
    ‘blog_body’ (gets appended to ‘blog_intro’ in actual pages)
    ‘tags’

    And then, again, what is ‘FEATURED_IMG_FIELD’? Isn’t it on the same hierarchy level as ‘CONTENT_FIELD’?

    Thanks!

    By Alexandre on October 24th, 2012 at 1:12 pm
  10. Sorry I can’t be of more help, I don’t currently have the latest ExpressionEngine version running anywhere accessible for me. This was a post describing the method we used for a project which the site was rebuilt on WordPress where the old site was on ExpressionEngine (likely an older ExpressionEngine version than what’s out today).

    By Scott Kingsley Clark on October 24th, 2012 at 3:52 pm

Leave a Reply