Migrating from Zen Cart to WooCommerce on New Server

I’m using Cart2Cart, which for like $100 will migrate all of our Zen Cart products with additional images as well as creating 301 redirects from the original site to the new one. Kind of cool.

I’m using Docker and specifically Docker-compose for development. I’m using the WP plugin Error Log Monitor to check for php errors and this requires setting up error logging on the development server.

Here’s how ya do that with Docker:

Get the name of the Docker image:
docker ps

Then get into the server:
docker exec -ti my_server_1 bash

Install vim:
apt-get update && apt-get install vim

Create your php error log file:
vim /var/www/php-errors.log

Then you add a couple of lines to your wp-config file following plugin directions.

I may add a couple of directives to my bash script that creates the Docker-compose instance to map the file to the server:

if [ $# -eq 0 ] ; then
    echo usage: directory_name
    mkdir "$WPDIR" && cd "$WPDIR"
    touch 'php-errors.log'

cat >> docker-compose.yml <<EOL
  image: mariadb
    - "8081:3306"
    MYSQL_ROOT_PASSWORD: dockerpass

  image: wordpress
    - ./:/var/www/html
    - php-errors.log:/var/www/php-errors.log
    - "8080:80"
    - my-wpdb:mysql

  image: tatemz/wp-cli
    - my-wp
    - my-wpdb:mysql
  entrypoint: wp  
  command: "--info"

docker-compose up -d

The first challenge is that it was pulling in the small version of the images, when we need that full size images.

So I created some mod_rewrite rules and conditions with the help of some Stack Overflow here and here. Came up with this:

Options +FollowSymLinks -MultiViews
RewriteEngine on
RewriteBase /
# from our domain
RewriteCond %{HTTP_HOST} ^(www\.)?example\.com$ [NC]
# isn't one of the large ones, which would create an infinite loop
RewriteCond %{REQUEST_URI} !^/images/large/
# a large version of the document exists
RewriteCond %{DOCUMENT_ROOT}/images/large/$1/$2_LRG.$3 -f
RewriteRule ^images/(.*)/(.*)\.(jpe?g|png)$ images/large/$1/$2_LRG.$3 [E=VAR1:$1,E=VAR2:$2,E=VAR3:$3,L,NC]
RewriteRule ^images/(.*))\.(jpe?g|png)$ images/large/$1_LRG.$2 [E=VAR1:$1,E=VAR2:$2,L,NC]

I also experimented with adding the NC (No Escape) flag to deal with image files that had spaces in the names.

Images are importing now, but the metadata (_wp_attachment_metadata) is all wrong. There is an easy way to fix this, fortunately. Fix my posts plugin. Apparently the [Regenerate Thumbnails}(https://wordpress.org/plugins/regenerate-thumbnails/) plugin does this.

Since Cart2Cart is not terribly inexpensive, I plan to backup the database & the uploads directory as soon as it’s complete.

Imported all 500 “Products” to local dev server and checked the box to download and import the images, but none of the images made it to the local dev server. So I replaced the local UPLOADS directory with the one we’d imported to. But I had also forgotten to update the URLs in the wordpress export file. Not only did wordpress not recognize the images because they hadn’t been uploaded through the WP uploader, but when I did upload them that way, they weren’t attached to the products. I used Media Cleaner plugin to remove the images and another one that removes all the WooCommerce product images.

This WPMUDev post had the key for migrating the attachments.

Ended up having added an extra slash in the local URL and had to replace those:

UPDATE wp_posts 
SET guid = replace(guid, 'http://localhost:8080//wp-content', 'http://localhost:8080/wp-content')
WHERE guid LIKE '%http://localhost:8080//wp-content%';

This has been tricky and the Bulk Delete plugin is being useful as well. Did I mention Media Cleaner from Meow Apps?

Using the projects-by-woothemes plugin as a starting point for our Project Archive.

Added a couple of files from WooCommerce to support a gallery for each project:

To migrate the categories from WooCommerce am hoping that the Convert Post Types plugin will do the trick. Also found this sql query:

UPDATE wp_term_taxonomy SET taxonomy='project-category' WHERE taxonomy='product_cat';

So two steps:

  1. Convert Post Types with plugin from products to projects.
  2. Run the above sql query.

Now we have Projects in Project Categories. Nice.

Next step I want to migrate the woocommerce breadcrumbs to the Project, which ended up being more work than seemed to make sense, so for now I am using this code:

if( !function_exists( 'inspiry_get_breadcrumbs_items' ) ) :
     * Returns a array of breadcrumbs items
     * Source: https://gist.github.com/saqibsarwar/471cb91a6b17ffc457e2
     * @param $post_id  int Post id
     * @param $breadcrumbs_taxonomy string Taxonomy name
     * @return mixed|void
    function inspiry_get_breadcrumbs_items( $post_id, $breadcrumbs_taxonomy ) {
        // Add home at the beginning of the breadcrumbs
        $inspiry_breadcrumbs_items = array(
                'name' => __( 'Home', 'inspiry' ),
                'url' => esc_url( home_url('/') ),
                'class' => '',
        // Get all assigned terms
        $the_terms = get_the_terms( $post_id, $breadcrumbs_taxonomy );
        if ( $the_terms && ! is_wp_error( $the_terms ) ) :
            $deepest_term = $the_terms[0];
            $deepest_depth = 0;
            // Find the deepest term
            foreach( $the_terms as $term ) {
                $current_term_depth = inspiry_get_term_depth( $term->term_id, $breadcrumbs_taxonomy );
                if ( $current_term_depth > $deepest_depth ) {
                    $deepest_depth = $current_term_depth;
                    $deepest_term = $term;
            // work on deepest term
            if ( $deepest_term ) {
                // Get ancestors if any and add them to breadcrumbs items
                $deepest_term_ancestors = get_ancestors( $deepest_term->term_id, $breadcrumbs_taxonomy );
                if ( $deepest_term_ancestors && ( 0 < count( $deepest_term_ancestors ) ) ) {
                    $deepest_term_ancestors = array_reverse( $deepest_term_ancestors ); // reversing the array is important
                    foreach ( $deepest_term_ancestors as $term_ancestor_id ) {
                        $ancestor_term = get_term_by( 'id', $term_ancestor_id, $breadcrumbs_taxonomy );
                        $inspiry_breadcrumbs_items[] = array(
                            'name' => $ancestor_term->name,
                            'url' => get_term_link( $ancestor_term, $breadcrumbs_taxonomy ),
                            'class' => ''
                // add deepest term
                $inspiry_breadcrumbs_items[] = array(
                    'name' => $deepest_term->name,
                    'url' => get_term_link( $deepest_term, $breadcrumbs_taxonomy ),
                    'class' => ''
        // Add the current page / property
        $inspiry_breadcrumbs_items[] = array(
            'name' => get_the_title( $post_id ),
            'url' => '',
            'class' => 'active',

        return apply_filters( 'inspiry_breadcrumbs_items', $inspiry_breadcrumbs_items );
if( !function_exists( 'inspiry_get_term_depth' ) ) :
     * Returns an integer value that tells the term depth in it's hierarchy
     * @param $term_id
     * @param $term_taxonomy
     * @return int
    function inspiry_get_term_depth( $term_id, $term_taxonomy ) {
        $term_ancestors = get_ancestors( $term_id, $term_taxonomy );
        if ( $term_ancestors ) {
            return count( $term_ancestors );
        return 0;

Which is represented in:

//source: https://gist.github.com/saqibsarwar/471cb91a6b17ffc457e2
        global $post;
        $inspiry_breadcrumbs_items = inspiry_get_breadcrumbs_items( $post->ID, 'project-category' );
        if ( is_array( $inspiry_breadcrumbs_items ) && ( 0 < count( $inspiry_breadcrumbs_items ) ) ) {
            <nav class="projects-breadcrumb">
                foreach( $inspiry_breadcrumbs_items as $item ) :
                    $class = ( !empty( $item['class'] ) ) ? 'class="' . $item['class'] . '"' : '';
                    if ( !empty ( $item['url'] ) ) :
                            <a href="<?php echo esc_url( $item['url'] ); ?>" <?php echo $class ?>><?php echo $item['name']; ?></a>                       
                            <span class="breadcrumb-separator"> / </span>
                    else :
                        <?php echo $item['name']; ?>

Discovered two bash functions to dump and restore the MySQL database from Docker container:

function dumpdb()
    # source https://stackoverflow.com/a/46042938/2223106
    # source https://gist.github.com/spalladino/6d981f7b33f6e0afe6bb
    local wkdir=`basename $PWD`
    local container='echo ${wkdir}_my-wpdb_1'
    docker exec ${container} mysqldump -uroot --password=password wordpress > backup.sql

function restoredb()
    # source: see above
    local wkdir=`basename $PWD`
    local container="${wkdir}_my-wpdb_1"
    cat backup.sql | docker exec -i ${container} /usr/bin/mysql -u root --password=dockerpass wordpress

Also that you can source a file any time from any where. So I added these to a file within the WP subdirectory where I keep my Docker projects.

Learned that running set -x before gives you detailed output as the commands are interpolated. Running set +x turns off the verbose output.

Migrating the Woo categories to Project categories while retaining the hierarchy has been a challenge. I found some good code that looks promising (again). So far haven’t gotten it working, but I came across an old plugin, [Post Type Convertr] (https://wordpress.org/plugins/post-type-convertr/) that’s actually doing the trick.

On the production server, I also needed to run the following SQL query after migrating with the Post Type Convertr plugin:

UPDATE wp_term_taxonomy SET taxonomy='project-category' WHERE taxonomy='product_cat';.