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 CMS | Content Type | WordPress Equivalent | Migration Method |
Joomla | Articles | Posts | Direct mapping |
Joomla | Categories | Categories/Tags | Hierarchy preservation |
Joomla | Modules | Widgets/Shortcodes | Manual recreation |
Drupal | Basic Page | Pages | Direct mapping |
Drupal | Custom Content Types | Custom Post Types | Plugin creation |
Drupal | Views | Custom queries/plugins | Manual 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
Issue | Symptoms | Solution |
Character Encoding Problems | Special characters display as � | Convert database to UTF-8, use proper PHP encoding |
Memory Exhaustion | Scripts timeout during migration | Increase PHP memory limit, process in batches |
Broken Internal Links | Links point to old domain | Run comprehensive search-replace on database |
Missing Images | Images show as broken | Verify file paths, run media migration script |
User Login Issues | Users can’t log in after migration | Check password hashing, reset user passwords |
Taxonomy Issues | Categories/tags not properly assigned | Verify 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:
- Lower TTL values 24 hours before migration
- Update A records to point to new server
- Monitor DNS propagation using tools like whatsmydns.net
- Update CDN settings if using CloudFlare or similar
- 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.