Web Hosting Blog by Nest Nepal | Domain & Hosting Tips

Migrating a Joomla or Drupal Site to WordPress: Complete Tutorial

Migrating from Joomla or Drupal to WordPress isn’t just about moving content; it’s about restructuring entire data architectures, preserving SEO rankings, and ensuring zero downtime during the transition. This comprehensive guide covers everything from database schema mapping to advanced URL preservation techniques that most tutorials skip.

Whether you’re dealing with a simple blog migration or a complex enterprise site with custom modules, taxonomies, and user roles, this tutorial provides the deep technical knowledge needed for a successful migration.

Understanding the Complexity: Why CMS Migrations Are Challenging

Database Architecture Differences

Each CMS stores data differently, and understanding these differences is crucial for successful migration:

WordPress Structure:

  • Posts and pages in the wp_posts table
  • Metadata in wp_postmeta (key-value pairs)
  • Taxonomies in wp_terms, wp_term_taxonomy, wp_term_relationships
  • Simple user roles system

Joomla Structure:

  • Content in #__content table
  • Categories in #__categories (nested set model)
  • Extensions store data in custom tables
  • Complex user group hierarchies in #__usergroups

Drupal Structure:

  • Nodes in the node table with revisions
  • Field data spread across field_data_* tables
  • Taxonomy terms in taxonomy_term_data
  • Entity-based architecture with bundles

Content Type Mapping Strategy

Before starting any migration, create a comprehensive mapping document:

Source CMSContent TypeWordPress EquivalentMigration Method
JoomlaArticlesPostsDirect mapping
JoomlaCategoriesCategories/TagsHierarchy preservation
JoomlaModulesWidgets/ShortcodesManual recreation
DrupalBasic PagePagesDirect mapping
DrupalCustom Content TypesCustom Post TypesPlugin creation
DrupalViewsCustom queries/pluginsManual recreation

Pre-Migration Planning and Assessment

Site Audit and Inventory

Create a complete inventory of your current site:

# Joomla site audit script

#!/bin/bash

JOOMLA_PATH=”/path/to/joomla”

DB_NAME=”joomla_db”

echo “=== JOOMLA SITE AUDIT ===”

echo “Articles count:”

mysql -e “SELECT COUNT(*) FROM ${DB_NAME}.#__content WHERE state=1;”

echo “Categories count:”

mysql -e “SELECT COUNT(*) FROM ${DB_NAME}.#__categories WHERE published=1;”

echo “Users count:”

mysql -e “SELECT COUNT(*) FROM ${DB_NAME}.#__users;”

echo “Installed extensions:”

mysql -e “SELECT name, element, type FROM ${DB_NAME}.#__extensions WHERE enabled=1;”

echo “Menu items:”

mysql -e “SELECT title, link FROM ${DB_NAME}.#__menu WHERE published=1;”

SEO Impact Assessment

Document all URLs that need preservation:

— Joomla URL extraction

SELECT 

    c.id,

    c.title,

    c.alias,

    cat.path as category_path,

    CONCAT(‘/’, cat. path, ‘/’, c.alias) as full_url

FROM #__content c

LEFT JOIN #__categories cat ON c.catid = cat.id

WHERE c.state = 1;

— Drupal URL extraction  

SELECT 

    n.nid,

    n.title,

    ua.alias,

    n.type

FROM node n

LEFT JOIN url_alias ua ON ua.source = CONCAT(‘node/’, n.nid)

WHERE n.status = 1;

Technical Requirements Analysis

Identify advanced features that need special handling:

Complex Migrations Require:

  • Custom database queries for data extraction
  • PHP scripts for content transformation
  • URL mapping files for redirect management
  • User role mapping and permission preservation
  • Media file organization and optimization

Setting Up the Migration Environment

Development Environment Setup

Never attempt migration on a live site. Set up proper staging:

# Create WordPress staging environment

wp core download –path=/var/www/staging

wp config create –dbname=wp_staging –dbuser=staging_user –dbpass=secure_pass –path=/var/www/staging

wp core install –url=staging.yoursite.com –title=”Migration Staging” –admin_user=admin –admin_password=secure_pass –admin_email=admin@yoursite.com –path=/var/www/staging

# Install essential migration plugins

wp plugin install wordpress-importer –activate –path=/var/www/staging

wp plugin install redirection –activate –path=/var/www/staging

wp plugin install custom-post-type-ui –activate –path=/var/www/staging

Database Preparation

Create comprehensive backups and prepare migration databases:

# Backup source databases

mysqldump joomla_db > joomla_backup_$(date +%Y%m%d).sql

mysqldump drupal_db > drupal_backup_$(date +%Y%m%d).sql

# Create analysis database for migration work

mysql -e “CREATE DATABASE migration_analysis;”

mysql migration_analysis < joomla_backup_$(date +%Y%m%d).sql

Joomla to WordPress Migration

Phase 1: Content Extraction and Transformation

Create a comprehensive PHP script for Joomla content extraction:

<?php

// joomla-extractor.php

class JoomlaToWordPressExtractor {

    private $joomlaDB;

    private $wpDB;

    public function __construct($joomlaConfig, $wpConfig) {

        $this->joomlaDB = new PDO(

            “mysql:host={$joomlaConfig[‘host’]};dbname={$joomlaConfig[‘db’]}”, 

            $joomlaConfig[‘user’], 

            $joomlaConfig[‘pass’]

        );

        $this->wpDB = new PDO(

            “mysql:host={$wpConfig[‘host’]};dbname={$wpConfig[‘db’]}”, 

            $wpConfig[‘user’], 

            $wpConfig[‘pass’]

        );

    }

    public function extractArticles() {

        $query = “

            SELECT 

                c.id,

                c.title,

                c.alias,

                c.introtext,

                c.fulltext,

                c.created,

                c.modified,

                c.state,

                c.catid,

                c.created_by,

                cat.title as category_title,

                cat.alias as category_alias,

                u.username,

                u.email

            FROM #__content c

            LEFT JOIN #__categories cat ON c.catid = cat.id

            LEFT JOIN #__users u ON c.created_by = u.id

            WHERE c.state IN (1, 2)

            ORDER BY c.id

        “;

        $stmt = $this->joomlaDB->prepare($query);

        $stmt->execute();

        return $stmt->fetchAll(PDO::FETCH_ASSOC);

    }

    public function transformContent($joomlaContent) {

        $content = $joomlaContent[‘introtext’];

        if (!empty($joomlaContent[‘fulltext’])) {

            $content .= “\n<!–more–>\n” . $joomlaContent[‘fulltext’];

        }

        // Transform Joomla-specific shortcodes

        $content = $this->transformShortcodes($content);

        // Fix image paths

        $content = preg_replace(‘/images\//’, ‘wp-content/uploads/’, $content);

        // Transform Joomla modules to WordPress shortcodes

        $content = preg_replace(‘/\{module\s+([^}]+)\}/’, ‘[joomla_module name=”$1″]’, $content);

        return $content;

    }

    private function transformShortcodes($content) {

        // Common Joomla to WordPress shortcode transformations

        $transformations = [

            ‘/\{gallery\}([^{]*)\{\/gallery\}/i’ => ‘‘,

            ‘/\{youtube\}([^{]*)\{\/youtube\}/i’ => ‘https://www.youtube.com/watch?v=$1‘,

            ‘/\{readmore\}/i’ => ‘<!–more–>’

        ];

        foreach ($transformations as $pattern => $replacement) {

            $content = preg_replace($pattern, $replacement, $content);

        }

        return $content;

    }

    public function insertIntoWordPress($articles) {

        foreach ($articles as $article) {

            $postContent = $this->transformContent($article);

            $insertQuery = “

                INSERT INTO wp_posts (

                    post_title,

                    post_name,

                    post_content,

                    post_date,

                    post_modified,

                    post_status,

                    post_type,

                    post_author

                ) VALUES (?, ?, ?, ?, ?, ?, ‘post’, 1)

            “;

            $status = $article[‘state’] == 1 ? ‘publish’ : ‘draft’;

            $stmt = $this->wpDB->prepare($insertQuery);

            $stmt->execute([

                $article[‘title’],

                $article[‘alias’],

                $postContent,

                $article[‘created’],

                $article[‘modified’],

                $status

            ]);

            $postId = $this->wpDB->lastInsertId();

            // Insert post metadata

            $this->insertPostMeta($postId, ‘joomla_id’, $article[‘id’]);

            $this->insertPostMeta($postId, ‘joomla_category’, $article[‘category_title’]);

        }

    }

    private function insertPostMeta($postId, $metaKey, $metaValue) {

        $stmt = $this->wpDB->prepare(“INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (?, ?, ?)”);

        $stmt->execute([$postId, $metaKey, $metaValue]);

    }

}

// Usage

$joomlaConfig = [

    ‘host’ => ‘localhost’,

    ‘db’ => ‘joomla_db’,

    ‘user’ => ‘joomla_user’,

    ‘pass’ => ‘joomla_pass’

];

$wpConfig = [

    ‘host’ => ‘localhost’, 

    ‘db’ => ‘wp_staging’,

    ‘user’ => ‘wp_user’,

    ‘pass’ => ‘wp_pass’

];

$extractor = new JoomlaToWordPressExtractor($joomlaConfig, $wpConfig);

$articles = $extractor->extractArticles();

$extractor->insertIntoWordPress($articles);

echo “Migrated ” . count($articles) . ” articles successfully.\n”;

?>

Phase 2: Category and Menu Migration

Joomla’s nested category system requires special handling:

<?php

// joomla-categories.php

class JoomlaCategoryMigrator {

    private $joomlaDB;

    private $wpDB;

    public function migrateCategoriesWithHierarchy() {

        // Get Joomla categories with hierarchy

        $query = “

            SELECT 

                id,

                title,

                alias,

                description,

                parent_id,

                level,

                path

            FROM #__categories 

            WHERE published = 1 

            AND extension = ‘com_content’

            ORDER BY level, lft

        “;

        $stmt = $this->joomlaDB->prepare($query);

        $stmt->execute();

        $categories = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $categoryMapping = [];

        foreach ($categories as $category) {

            $parentId = 0;

            // Find WordPress parent category

            if ($category[‘parent_id’] > 1) { // Joomla root is 1

                $parentId = $categoryMapping[$category[‘parent_id’]] ?? 0;

            }

            // Insert into WordPress

            $wpCatId = $this->insertWordPressCategory($category, $parentId);

            $categoryMapping[$category[‘id’]] = $wpCatId;

        }

        return $categoryMapping;

    }

    private function insertWordPressCategory($joomlaCategory, $parentId) {

        $stmt = $this->wpDB->prepare(“

            INSERT INTO wp_terms (name, slug) VALUES (?, ?)

        “);

        $stmt->execute([$joomlaCategory[‘title’], $joomlaCategory[‘alias’]]);

        $termId = $this->wpDB->lastInsertId();

        $stmt = $this->wpDB->prepare(“

            INSERT INTO wp_term_taxonomy (term_id, taxonomy, description, parent, count) 

            VALUES (?, ‘category’, ?, ?, 0)

        “);

        $stmt->execute([$termId, $joomlaCategory[‘description’], $parentId]);

        return $termId;

    }

}

?>

Phase 3: User and Permission Migration

<?php

// joomla-users.php

class JoomlaUserMigrator {

    public function migrateUsers() {

        $query = “

            SELECT 

                u.id,

                u.username,

                u.email,

                u.password,

                u.registerDate,

                u.lastvisitDate,

                g.title as user_group

            FROM #__users u

            LEFT JOIN #__user_usergroup_map ugm ON u.id = ugm.user_id

            LEFT JOIN #__usergroups g ON ugm.group_id = g.id

            WHERE u.block = 0

        “;

        $stmt = $this->joomlaDB->prepare($query);

        $stmt->execute();

        $users = $stmt->fetchAll(PDO::FETCH_ASSOC);

        foreach ($users as $user) {

            $wpRole = $this->mapJoomlaRoleToWordPress($user[‘user_group’]);

            $stmt = $this->wpDB->prepare(“

                INSERT INTO wp_users (

                    user_login,

                    user_email,

                    user_pass,

                    user_registered,

                    user_status

                ) VALUES (?, ?, ?, ?, 0)

            “);

            $stmt->execute([

                $user[‘username’],

                $user[’email’],

                $user[‘password’], // Note: May need password rehashing

                $user[‘registerDate’]

            ]);

            $userId = $this->wpDB->lastInsertId();

            // Set user role

            $this->setUserRole($userId, $wpRole);

        }

    }

    private function mapJoomlaRoleToWordPress($joomlaRole) {

        $roleMapping = [

            ‘Super Users’ => ‘administrator’,

            ‘Administrator’ => ‘administrator’,

            ‘Manager’ => ‘editor’,

            ‘Author’ => ‘author’,

            ‘Editor’ => ‘editor’,

            ‘Publisher’ => ‘editor’,

            ‘Registered’ => ‘subscriber’

        ];

        return $roleMapping[$joomlaRole] ?? ‘subscriber’;

    }

}

?>

Drupal to WordPress Migration

Drupal migrations are more complex due to its entity-field system:

Phase 1: Drupal Content Analysis

<?php

// drupal-analyzer.php

class DrupalContentAnalyzer {

    private $drupalDB;

    public function analyzeContentTypes() {

        // Get all content types

        $query = “SELECT type, name, description FROM node_type”;

        $stmt = $this->drupalDB->prepare($query);

        $stmt->execute();

        $contentTypes = $stmt->fetchAll(PDO::FETCH_ASSOC);

        foreach ($contentTypes as $type) {

            echo “Content Type: {$type[‘type’]} ({$type[‘name’]})\n”;

            // Analyze fields for this content type

            $fields = $this->getContentTypeFields($type[‘type’]);

            foreach ($fields as $field) {

                echo ”  Field: {$field[‘field_name’]} – {$field[‘type’]}\n”;

            }

        }

        return $contentTypes;

    }

    private function getContentTypeFields($contentType) {

        $query = “

            SELECT DISTINCT

                fci.field_name,

                fc.type,

                fc.data

            FROM field_config_instance fci

            JOIN field_config fc ON fci.field_name = fc.field_name

            WHERE fci.bundle = ?

            AND fci.entity_type = ‘node’

        “;

        $stmt = $this->drupalDB->prepare($query);

        $stmt->execute([$contentType]);

        return $stmt->fetchAll(PDO::FETCH_ASSOC);

    }

    public function generateMigrationPlan($contentTypes) {

        $plan = [];

        foreach ($contentTypes as $type) {

            $fields = $this->getContentTypeFields($type[‘type’]);

            $plan[$type[‘type’]] = [

                ‘wp_post_type’ => $this->suggestWordPressPostType($type[‘type’]),

                ‘fields’ => [],

                ‘migration_method’ => ‘custom’

            ];

            foreach ($fields as $field) {

                $plan[$type[‘type’]][‘fields’][$field[‘field_name’]] = [

                    ‘drupal_type’ => $field[‘type’],

                    ‘wp_equivalent’ => $this->mapFieldType($field[‘type’]),

                    ‘migration_method’ => $this->getFieldMigrationMethod($field[‘type’])

                ];

            }

        }

        return $plan;

    }

    private function mapFieldType($drupalFieldType) {

        $fieldMapping = [

            ‘text’ => ‘post_content’,

            ‘text_long’ => ‘post_content’, 

            ‘text_with_summary’ => ‘post_content’,

            ‘image’ => ‘featured_image’,

            ‘file’ => ‘attachment’,

            ‘taxonomy_term_reference’ => ‘category/tag’,

            ‘entityreference’ => ‘post_relationship’,

            ‘link_field’ => ‘custom_field’,

            ‘date’ => ‘custom_field’,

            ‘datetime’ => ‘custom_field’

        ];

        return $fieldMapping[$drupalFieldType] ?? ‘custom_field’;

    }

}

?>

Phase 2: Advanced Drupal Content Migration

<?php

// drupal-migrator.php

class DrupalToWordPressMigrator {

    private $drupalDB;

    private $wpDB;

    public function migrateNodes($contentType, $wpPostType = ‘post’) {

        $query = “

            SELECT 

                n.nid,

                n.title,

                n.status,

                n.created,

                n.changed,

                n.uid,

                nr.body_value,

                nr.body_summary,

                ua.alias

            FROM node n

            LEFT JOIN field_revision_body nr ON n.nid = nr.entity_id AND n.vid = nr.revision_id

            LEFT JOIN url_alias ua ON ua.source = CONCAT(‘node/’, n.nid)

            WHERE n.type = ?

            ORDER BY n.nid

        “;

        $stmt = $this->drupalDB->prepare($query);

        $stmt->execute([$contentType]);

        $nodes = $stmt->fetchAll(PDO::FETCH_ASSOC);

        foreach ($nodes as $node) {

            $this->migrateNode($node, $wpPostType);

        }

        return count($nodes);

    }

    private function migrateNode($node, $wpPostType) {

        $content = $node[‘body_value’];

        // Handle Drupal-specific content transformations

        $content = $this->transformDrupalContent($content);

        // Prepare post data

        $postSlug = $node[‘alias’] ? basename($node[‘alias’]) : sanitize_title($node[‘title’]);

        $postStatus = $node[‘status’] == 1 ? ‘publish’ : ‘draft’;

        $stmt = $this->wpDB->prepare(“

            INSERT INTO wp_posts (

                post_title,

                post_name,

                post_content,

                post_excerpt,

                post_date,

                post_modified,

                post_status,

                post_type,

                post_author

            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)

        “);

        $stmt->execute([

            $node[‘title’],

            $postSlug,

            $content,

            $node[‘body_summary’],

            date(‘Y-m-d H:i:s’, $node[‘created’]),

            date(‘Y-m-d H:i:s’, $node[‘changed’]),

            $postStatus,

            $wpPostType,

            $this->mapDrupalUser($node[‘uid’])

        ]);

        $postId = $this->wpDB->lastInsertId();

        // Migrate custom fields

        $this->migrateCustomFields($node[‘nid’], $postId);

        // Migrate taxonomy terms

        $this->migrateTaxonomyTerms($node[‘nid’], $postId);

        return $postId;

    }

    private function migrateCustomFields($drupalNid, $wpPostId) {

        // Get all field data for this node

        $tables = $this->getDrupalFieldTables();

        foreach ($tables as $table) {

            $fieldName = str_replace([‘field_data_’, ‘field_revision_’], ”, $table);

            $query = “SELECT * FROM {$table} WHERE entity_id = ? AND entity_type = ‘node'”;

            $stmt = $this->drupalDB->prepare($query);

            $stmt->execute([$drupalNid]);

            $fieldData = $stmt->fetchAll(PDO::FETCH_ASSOC);

            foreach ($fieldData as $data) {

                $this->processFieldData($wpPostId, $fieldName, $data);

            }

        }

    }

    private function getDrupalFieldTables() {

        $stmt = $this->drupalDB->prepare(“SHOW TABLES LIKE ‘field_data_%'”);

        $stmt->execute();

        return $stmt->fetchAll(PDO::FETCH_COLUMN);

    }

    private function transformDrupalContent($content) {

        // Transform Drupal-specific markup

        $transformations = [

            // Drupal image syntax to WordPress

            ‘/\[image:(\d+)\s*([^\]]*)\]/i’ => ‘[img id=”$1″ $2]’,

            // Drupal node references

            ‘/\[node:(\d+)\]/i’ => ‘[post id=”$1″]’,

            // Drupal views embed

            ‘/\[view:([^\]]+)\]/i’ => ‘[drupal_view name=”$1″]’,

            // Clean up Drupal-specific classes

            ‘/class=”field field-[^”]*”/i’ => ”,

        ];

        foreach ($transformations as $pattern => $replacement) {

            $content = preg_replace($pattern, $replacement, $content);

        }

        return $content;

    }

}

?>

Advanced Migration Techniques

Custom Post Types and Fields

For complex content structures, create custom post types:

<?php

// Register custom post types for migrated content

function register_migrated_post_types() {

    // Example: Drupal “Event” content type

    register_post_type(‘event’, [

        ‘labels’ => [

            ‘name’ => ‘Events’,

            ‘singular_name’ => ‘Event’

        ],

        ‘public’ => true,

        ‘has_archive’ => true,

        ‘supports’ => [‘title’, ‘editor’, ‘thumbnail’, ‘custom-fields’],

        ‘rewrite’ => [‘slug’ => ‘events’]

    ]);

    // Example: Joomla “Product” articles

    register_post_type(‘product’, [

        ‘labels’ => [

            ‘name’ => ‘Products’,

            ‘singular_name’ => ‘Product’

        ],

        ‘public’ => true,

        ‘has_archive’ => true,

        ‘supports’ => [‘title’, ‘editor’, ‘thumbnail’, ‘custom-fields’],

        ‘rewrite’ => [‘slug’ => ‘products’]

    ]);

}

add_action(‘init’, ‘register_migrated_post_types’);

?>

Media Migration and Optimization

<?php

// media-migrator.php

class MediaMigrator {

    private $sourceDir;

    private $wpUploadDir;

    public function __construct($sourceDir, $wpUploadDir) {

        $this->sourceDir = $sourceDir;

        $this->wpUploadDir = $wpUploadDir;

    }

    public function migrateImages() {

        $images = $this->findAllImages();

        foreach ($images as $image) {

            $this->processImage($image);

        }

    }

    private function findAllImages() {

        $imageExtensions = [‘jpg’, ‘jpeg’, ‘png’, ‘gif’, ‘webp’];

        $images = [];

        foreach ($imageExtensions as $ext) {

            $found = glob($this->sourceDir . “/**/*.{$ext}”, GLOB_BRACE);

            $images = array_merge($images, $found);

        }

        return $images;

    }

    private function processImage($imagePath) {

        $filename = basename($imagePath);

        $yearMonth = date(‘Y/m’);

        $targetDir = $this->wpUploadDir . ‘/’ . $yearMonth;

        if (!is_dir($targetDir)) {

            mkdir($targetDir, 0755, true);

        }

        $targetPath = $targetDir . ‘/’ . $filename;

        // Copy and optimize image

        if (copy($imagePath, $targetPath)) {

            $this->optimizeImage($targetPath);

            $this->createWordPressAttachment($targetPath, $filename);

        }

    }

    private function optimizeImage($imagePath) {

        $imageInfo = getimagesize($imagePath);

        if ($imageInfo[0] > 1920) { // Resize large images

            $this->resizeImage($imagePath, 1920);

        }

        // Compress image based on type

        switch ($imageInfo[‘mime’]) {

            case ‘image/jpeg’:

                $this->compressJPEG($imagePath);

                break;

            case ‘image/png’:

                $this->compressPNG($imagePath);

                break;

        }

    }

    private function createWordPressAttachment($filePath, $filename) {

        $uploadDir = wp_upload_dir();

        $relativeUrl = str_replace($uploadDir[‘basedir’], ”, $filePath);

        $attachment = [

            ‘post_mime_type’ => mime_content_type($filePath),

            ‘post_title’ => pathinfo($filename, PATHINFO_FILENAME),

            ‘post_content’ => ”,

            ‘post_status’ => ‘inherit’

        ];

        $attachmentId = wp_insert_attachment($attachment, $filePath);

        if (!is_wp_error($attachmentId)) {

            require_once(ABSPATH . ‘wp-admin/includes/image.php’);

            $attachmentData = wp_generate_attachment_metadata($attachmentId, $filePath);

            wp_update_attachment_metadata($attachmentId, $attachmentData);

        }

        return $attachmentId;

    }

}

?>

URL Preservation and SEO Migration

Comprehensive Redirect Management

<?php

// redirect-manager.php

class MigrationRedirectManager {

    private $oldUrls = [];

    private $newUrls = [];

    public function generateRedirectMap($migrationData) {

        foreach ($migrationData as $item) {

            $oldUrl = $this->buildOldUrl($item);

            $newUrl = $this->buildNewUrl($item);

            $this->oldUrls[] = $oldUrl;

            $this->newUrls[] = $newUrl;

        }

        $this->generateHtaccessRedirects();

        $this->generateRedirectionPluginRules();

    }

    private function generateHtaccessRedirects() {

        $htaccessRules = “# Migration Redirects\n”;

        for ($i = 0; $i < count($this->oldUrls); $i++) {

            $oldPath = parse_url($this->oldUrls[$i], PHP_URL_PATH);

            $newPath = parse_url($this->newUrls[$i], PHP_URL_PATH);

            $htaccessRules .= “Redirect 301 {$oldPath} {$newPath}\n”;

        }

        file_put_contents(‘migration-redirects.htaccess’, $htaccessRules);

    }

    private function generateRedirectionPluginRules() {

        $rules = [];

        for ($i = 0; $i < count($this->oldUrls); $i++) {

            $rules[] = [

                ‘source’ => $this->oldUrls[$i],

                ‘target’ => $this->newUrls[$i],

                ‘type’ => 301,

                ‘match_type’ => ‘url’

            ];

        }

        // Insert into Redirection plugin database

        $this->insertRedirectionRules($rules);

    }

    private function insertRedirectionRules($rules) {

        global $wpdb;

        foreach ($rules as $rule) {

            $wpdb->insert(

                $wpdb->prefix . ‘redirection_items’,

                [

                    ‘url’ => $rule[‘source’],

                    ‘action_data’ => $rule[‘target’],

                    ‘action_type’ => ‘url’,

                    ‘match_type’ => $rule[‘match_type’],

                    ‘status’ => ‘enabled’,

                    ‘group_id’ => 1

                ]

            );

        }

    }

}

?>

SEO Metadata Migration

<?php

// seo-migrator.php

class SEOMigrator {

    public function migrateSEOData($sourceData, $wpPostId) {

        // Migrate meta titles and descriptions

        if (!empty($sourceData[‘meta_title’])) {

            update_post_meta($wpPostId, ‘_yoast_wpseo_title’, $sourceData[‘meta_title’]);

        }

        if (!empty($sourceData[‘meta_description’])) {

            update_post_meta($wpPostId, ‘_yoast_wpseo_metadesc’, $sourceData[‘meta_description’]);

        }

        // Migrate meta keywords (if applicable)

        if (!empty($sourceData[‘meta_keywords’])) {

            update_post_meta($wpPostId, ‘_yoast_wpseo_focuskw’, $sourceData[‘meta_keywords’]);

        }

        // Set canonical URL if different

        if (!empty($sourceData[‘canonical_url’])) {

            update_post_meta($wpPostId, ‘_yoast_wpseo_canonical’, $sourceData[‘canonical_url’]);

        }

        // Migrate Open Graph data

        $this->migrateOpenGraphData($sourceData, $wpPostId);

    }

    private function migrateOpenGraphData($sourceData, $wpPostId) {

        $ogMapping = [

            ‘og_title’ => ‘_yoast_wpseo_opengraph-title’,

            ‘og_description’ => ‘_yoast_wpseo_opengraph-description’,

            ‘og_image’ => ‘_yoast_wpseo_opengraph-image’,

            ‘twitter_title’ => ‘_yoast_wpseo_twitter-title’,

            ‘twitter_description’ => ‘_yoast_wpseo_twitter-description’

        ];

        foreach ($ogMapping as $sourceKey => $wpMetaKey) {

            if (!empty($sourceData[$sourceKey])) {

                update_post_meta($wpPostId, $wpMetaKey, $sourceData[$sourceKey]);

            }

        }

    }

}

?>

Post-Migration Optimization and Testing

Performance Optimization Scripts

#!/bin/bash

# post-migration-optimization.sh

echo “Starting post-migration optimization…”

# Clear all caches

wp cache flush

wp transient delete –all

# Regenerate thumbnails for migrated images

wp media regenerate –yes

# Update permalink structure

wp rewrite structure ‘/%postname%/’ –hard

wp rewrite flush –hard

# Optimize database

wp db optimize

# Update search indexes

wp search-replace ‘oldsite.com’ ‘newsite.com’ –dry-run

wp search-replace ‘oldsite.com’ ‘newsite.com’

# Generate XML sitemap

wp plugin install google-sitemap-generator –activate

wp sitemap generate

echo “Optimization complete!”

Comprehensive Testing Framework

<?php

// migration-tester.php

class MigrationTester {

    private $testResults = [];

    public function runAllTests() {

        $this->testContentIntegrity();

        $this->testURLRedirects();

        $this->testImageMigration();

        $this->testUserMigration();

        $this->testSEOData();

        $this->testPerformance();

        return $this->generateReport();

    }

    private function testContentIntegrity() {

        echo “Testing content integrity…\n”;

        // Count posts in both systems

        $sourceCount = $this->getSourceContentCount();

        $wpCount = $this->getWordPressContentCount();

        $this->testResults[‘content_count’] = [

            ‘source’ => $sourceCount,

            ‘wordpress’ => $wpCount,

            ‘status’ => $sourceCount === $wpCount ? ‘PASS’ : ‘FAIL’

        ];

        // Test content quality

        $this->testContentQuality();

    }

    private function testContentQuality() {

        $posts = get_posts([‘numberposts’ => 10, ‘post_status’ => ‘publish’]);

        $qualityIssues = [];

        foreach ($posts as $post) {

            $content = $post->post_content;

            // Check for broken shortcodes

            if (preg_match(‘/\{[^}]*\}/’, $content)) {

                $qualityIssues[] = “Post {$post->ID}: Contains unmigrated shortcodes”;

            }

            // Check for broken image references

            if (preg_match(‘/src=”[^”]*oldsite\.com[^”]*”/’, $content)) {

                $qualityIssues[] = “Post {$post->ID}: Contains old domain image references”;

            }

            // Check for empty content

            if (strlen(strip_tags($content)) < 50) {

                $qualityIssues[] = “Post {$post->ID}: Content too short or empty”;

            }

        }

        $this->testResults[‘content_quality’] = [

            ‘issues’ => $qualityIssues,

            ‘status’ => empty($qualityIssues) ? ‘PASS’ : ‘WARN’

        ];

    }

    private function testURLRedirects() {

        echo “Testing URL redirects…\n”;

        $testUrls = $this->getTestUrls();

        $redirectResults = [];

        foreach ($testUrls as $oldUrl => $expectedNewUrl) {

            $response = $this->testRedirect($oldUrl);

            $redirectResults[$oldUrl] = [

                ‘expected’ => $expectedNewUrl,

                ‘actual’ => $response[‘location’] ?? ‘No redirect’,

                ‘status_code’ => $response[‘status_code’],

                ‘pass’ => $response[‘status_code’] === 301 && $response[‘location’] === $expectedNewUrl

            ];

        }

        $this->testResults[‘redirects’] = $redirectResults;

    }

    private function testRedirect($url) {

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);

        curl_setopt($ch, CURLOPT_HEADER, true);

        curl_setopt($ch, CURLOPT_NOBODY, true);

        $response = curl_exec($ch);

        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        preg_match(‘/Location: (.*)/’, $response, $matches);

        $location = isset($matches[1]) ? trim($matches[1]) : null;

        curl_close($ch);

        return [

            ‘status_code’ => $statusCode,

            ‘location’ => $location

        ];

    }

    private function testImageMigration() {

        echo “Testing image migration…\n”;

        $images = get_posts([

            ‘post_type’ => ‘attachment’,

            ‘post_mime_type’ => ‘image’,

            ‘numberposts’ => 50

        ]);

        $imageResults = [

            ‘total_images’ => count($images),

            ‘broken_images’ => 0,

            ‘missing_thumbnails’ => 0

        ];

        foreach ($images as $image) {

            $imagePath = get_attached_file($image->ID);

            if (!file_exists($imagePath)) {

                $imageResults[‘broken_images’]++;

            }

            $metadata = wp_get_attachment_metadata($image->ID);

            if (empty($metadata[‘sizes’])) {

                $imageResults[‘missing_thumbnails’]++;

            }

        }

        $this->testResults[‘images’] = $imageResults;

    }

    private function testUserMigration() {

        echo “Testing user migration…\n”;

        $users = get_users([‘number’ => 100]);

        $userIssues = [];

        foreach ($users as $user) {

            // Check for duplicate usernames

            $duplicates = get_users([‘search’ => $user->user_login]);

            if (count($duplicates) > 1) {

                $userIssues[] = “Duplicate username: {$user->user_login}”;

            }

            // Check for missing email addresses

            if (empty($user->user_email)) {

                $userIssues[] = “Missing email for user: {$user->user_login}”;

            }

        }

        $this->testResults[‘users’] = [

            ‘total_users’ => count($users),

            ‘issues’ => $userIssues,

            ‘status’ => empty($userIssues) ? ‘PASS’ : ‘WARN’

        ];

    }

    private function testSEOData() {

        echo “Testing SEO data migration…\n”;

        $posts = get_posts([‘numberposts’ => 20]);

        $seoIssues = [];

        foreach ($posts as $post) {

            $metaTitle = get_post_meta($post->ID, ‘_yoast_wpseo_title’, true);

            $metaDesc = get_post_meta($post->ID, ‘_yoast_wpseo_metadesc’, true);

            if (empty($metaTitle) && empty($metaDesc)) {

                $seoIssues[] = “Post {$post->ID}: Missing SEO metadata”;

            }

        }

        $this->testResults[‘seo’] = [

            ‘issues’ => $seoIssues,

            ‘status’ => count($seoIssues) < 5 ? ‘PASS’ : ‘WARN’

        ];

    }

    private function testPerformance() {

        echo “Testing site performance…\n”;

        $startTime = microtime(true);

        $response = wp_remote_get(home_url());

        $loadTime = microtime(true) – $startTime;

        $this->testResults[‘performance’] = [

            ‘load_time’ => round($loadTime, 3),

            ‘status_code’ => wp_remote_retrieve_response_code($response),

            ‘status’ => $loadTime < 3.0 ? ‘PASS’ : ‘WARN’

        ];

    }

    private function generateReport() {

        $report = “=== MIGRATION TEST REPORT ===\n”;

        $report .= “Generated: ” . date(‘Y-m-d H:i:s’) . “\n\n”;

        foreach ($this->testResults as $testName => $results) {

            $report .= strtoupper($testName) . ” TEST\n”;

            $report .= str_repeat(“-“, 20) . “\n”;

            if (is_array($results)) {

                foreach ($results as $key => $value) {

                    if (is_array($value)) {

                        $report .= “$key: ” . json_encode($value) . “\n”;

                    } else {

                        $report .= “$key: $value\n”;

                    }

                }

            }

            $report .= “\n”;

        }

        file_put_contents(‘migration-test-report.txt’, $report);

        echo $report;

        return $this->testResults;

    }

}

// Run tests

$tester = new MigrationTester();

$results = $tester->runAllTests();

?>

Advanced Troubleshooting and Problem Resolution

Common Migration Issues and Solutions

IssueSymptomsSolution
Character Encoding ProblemsSpecial characters display as �Convert database to UTF-8, use proper PHP encoding
Memory ExhaustionScripts timeout during migrationIncrease PHP memory limit, process in batches
Broken Internal LinksLinks point to old domainRun comprehensive search-replace on database
Missing ImagesImages show as brokenVerify file paths, run media migration script
User Login IssuesUsers can’t log in after migrationCheck password hashing, reset user passwords
Taxonomy IssuesCategories/tags not properly assignedVerify term relationships in database

Database Cleanup and Optimization

— Clean up migration artifacts

DELETE FROM wp_postmeta WHERE meta_key LIKE ‘%joomla_%’;

DELETE FROM wp_postmeta WHERE meta_key LIKE ‘%drupal_%’;

— Remove empty meta entries

DELETE FROM wp_postmeta WHERE meta_value = ”;

— Optimize tables after cleanup

OPTIMIZE TABLE wp_posts;

OPTIMIZE TABLE wp_postmeta;

OPTIMIZE TABLE wp_terms;

OPTIMIZE TABLE wp_term_relationships;

— Update post counts

UPDATE wp_term_taxonomy SET count = (

    SELECT COUNT(*) FROM wp_term_relationships 

    WHERE wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id

);

Security Hardening Post-Migration

<?php

// security-hardening.php

function post_migration_security_hardening() {

    // Remove migration scripts and temporary files

    $migrationFiles = [

        ‘joomla-extractor.php’,

        ‘drupal-migrator.php’,

        ‘migration-tester.php’,

        ‘temp-migration-data.sql’

    ];

    foreach ($migrationFiles as $file) {

        if (file_exists($file)) {

            unlink($file);

        }

    }

    // Update security keys

    $keys = [

        ‘AUTH_KEY’,

        ‘SECURE_AUTH_KEY’, 

        ‘LOGGED_IN_KEY’,

        ‘NONCE_KEY’,

        ‘AUTH_SALT’,

        ‘SECURE_AUTH_SALT’,

        ‘LOGGED_IN_SALT’,

        ‘NONCE_SALT’

    ];

    $wpConfigPath = ABSPATH . ‘wp-config.php’;

    $wpConfig = file_get_contents($wpConfigPath);

    foreach ($keys as $key) {

        $newValue = wp_generate_password(64, true, true);

        $pattern = “/define\s*\(\s*[‘\”]” . $key . “[‘\”]\s*,\s*[‘\”][^’\”]*[‘\”]\s*\)/”;

        $replacement = “define(‘” . $key . “‘, ‘” . $newValue . “‘)”;

        $wpConfig = preg_replace($pattern, $replacement, $wpConfig);

    }

    file_put_contents($wpConfigPath, $wpConfig);

    // Force password reset for all users

    global $wpdb;

    $wpdb->query(“UPDATE {$wpdb->users} SET user_pass = ””);

    echo “Security hardening complete. All users must reset passwords.\n”;

}

// Run after migration is complete

post_migration_security_hardening();

?>

Final Deployment and Go-Live Checklist

Pre-Launch Verification

#!/bin/bash

# pre-launch-checklist.sh

echo “=== PRE-LAUNCH CHECKLIST ===”

# 1. SSL Certificate

echo “Checking SSL certificate…”

curl -I https://yoursite.com | grep -i “HTTP/2 200”

# 2. Site speed test

echo “Testing site speed…”

curl -o /dev/null -s -w “Total time: %{time_total}s\n” https://yoursite.com

# 3. Redirect tests

echo “Testing critical redirects…”

curl -I -L https://yoursite.com/old-important-page

# 4. Database integrity

echo “Checking database integrity…”

wp db check

# 5. Plugin compatibility

echo “Checking plugin status…”

wp plugin list –status=active

# 6. Cache status

echo “Verifying caching is working…”

curl -I https://yoursite.com | grep -i cache

# 7. Search functionality

echo “Testing search…”

wp search-replace “test-search” “test-search” –dry-run

echo “=== CHECKLIST COMPLETE ===”

DNS and Domain Management

When ready to go live:

  1. Lower TTL values 24 hours before migration
  2. Update A records to point to new server
  3. Monitor DNS propagation using tools like whatsmydns.net
  4. Update CDN settings if using CloudFlare or similar
  5. Verify email routing hasn’t been disrupted

Monitoring and Alert Setup

<?php

// post-launch-monitoring.php

function setup_migration_monitoring() {

    // Monitor for 404 errors

    add_action(‘wp’, function() {

        if (is_404()) {

            $url = $_SERVER[‘REQUEST_URI’];

            error_log(“404 Error post-migration: ” . $url);

            // Send alert if critical pages are 404ing

            $criticalPages = [‘/contact’, ‘/about’, ‘/products’, ‘/services’];

            if (in_array($url, $criticalPages)) {

                wp_mail(‘admin@yoursite.com’, ‘404 Alert: Critical Page’, “Critical page 404: ” . $url);

            }

        }

    });

    // Monitor database queries

    add_action(‘shutdown’, function() {

        if (defined(‘SAVEQUERIES’) && SAVEQUERIES) {

            global $wpdb;

            if (count($wpdb->queries) > 100) {

                error_log(“High query count detected: ” . count($wpdb->queries));

            }

        }

    });

}

add_action(‘init’, ‘setup_migration_monitoring’);

?>

Conclusion and Best Practices

Migrating from Joomla or Drupal to WordPress is a complex undertaking that requires meticulous planning, technical expertise, and thorough testing. The key to success lies in understanding the fundamental differences between these systems and approaching the migration as a complete data transformation project rather than a simple copy operation.

Critical Success Factors:

  • Plan extensively before writing any code
  • Test everything in a staging environment first
  • Preserve SEO value through proper URL mapping and redirects
  • Maintain data integrity through comprehensive validation
  • Document all customizations for future maintenance
  • Train stakeholders on WordPress differences

Post-Migration Maintenance:

Continue monitoring your migrated site for at least 30 days after launch. Keep migration scripts and documentation for reference, as you may need to perform additional data cleanup or handle edge cases that weren’t caught during initial testing.

The investment in a proper migration process pays dividends in the long term through improved site performance, easier content management, and the vast ecosystem of WordPress plugins and themes available for future enhancements.

Remember: A successful migration isn’t just about moving data—it’s about creating a better, more maintainable website that serves your users and business goals more effectively.

Share this article
Shareable URL
Prev Post

Setting Up a WordPress Staging Environment in cPanel Without Extra Plugins

Leave a Reply

Your email address will not be published. Required fields are marked *

Read next