Переход с WordPress на MODX — серьёзное решение, которое может кардинально улучшить производительность и управляемость сайта. За 18 лет работы я помог десяткам клиентов избавиться от «макаронного» кода WordPress и перейти на чистую архитектуру MODX. Расскажу, как это сделать правильно.
Почему стоит мигрировать с WordPress
Проблемы WordPress
Архитектурные проблемы:
- «Макаронный» код — PHP, HTML и JavaScript смешаны в одном файле
- Жёсткая структура — сложно создать нестандартную логику
- Избыточность — функционал, который никогда не используется
- Конфликты плагинов — один плагин может сломать весь сайт
Проблемы производительности:
- Медленные запросы — wp_postmeta создаёт огромное количество записей
- Раздутая админка — загружается много лишнего JS/CSS
- Плохое кеширование — сложно настроить эффективный кеш
Безопасность:
- Популярная цель — хакеры знают все уязвимости WordPress
- Небезопасные плагины — много кода от неопытных разработчиков
- Частые обновления — постоянный риск что-то сломать
Преимущества MODX
Чистая архитектура:
- Разделение логики и представления — PHP-код отдельно от HTML
- Гибкость — можно реализовать любую идею
- Производительность — только нужный функционал
SEO из коробки:
- ЧПУ — настраиваются без плагинов
- Редиректы — встроенная система управления
- Метатеги — гибкая настройка для каждой страницы
Безопасность:
- Малоизвестная система — меньше автоматических атак
- Контролируемый код — вы знаете, что установлено на сайте
- Стабильные обновления — редкие, но качественные релизы
Планирование миграции
Анализ текущего сайта
# Инструменты для анализа WordPress-сайта
# 1. Экспорт контента
wp export --allow-root
# 2. Анализ структуры URL
wp post list --post_type=page --format=csv --fields=ID,post_title,post_name
# 3. Список активных плагинов
wp plugin list --status=active
# 4. Используемые кастомные поля
wp db query "SELECT DISTINCT meta_key FROM wp_postmeta ORDER BY meta_key"
# 5. Анализ медиафайлов
find wp-content/uploads -type f | wc -l
du -sh wp-content/uploads
Карта миграции функционала
WordPress → MODX
Страницы/Записи → modResource
Категории → Родительские ресурсы
Метки → TV-параметры или теги
Кастомные поля → TV-параметры
Меню → pdoMenu/Wayfinder
Виджеты → Сниппеты в шаблоне
Плагины → Дополнения MODX
Пользователи → modUser
Комментарии → Tickets/Disqus
Контрольный чек-лист
Подготовка:
- [ ] Полный бэкап WordPress-сайта
- [ ] Список всех URL для редиректов
- [ ] Анализ используемого функционала
- [ ] Подготовка тестового домена
Контент:
- [ ] Экспорт страниц и записей
- [ ] Сохранение медиафайлов
- [ ] Список кастомных полей
- [ ] Экспорт пользователей
SEO:
- [ ] Карта редиректов
- [ ] Список ключевых страниц
- [ ] Настройки метатегов
- [ ] Sitemap.xml
Функционал:
- [ ] Формы обратной связи
- [ ] Интеграции (аналитика, CRM)
- [ ] E-commerce функционал
- [ ] Кастомный код/скрипты
Автоматизированная миграция контента
Скрипт экспорта из WordPress
<?php
// export_wordpress.php - скрипт экспорта данных WordPress
require_once 'wp-config.php';
// Подключение к базе WordPress
$wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
function exportPages($wpdb) {
$pages = $wpdb->get_results("
SELECT
p.ID,
p.post_title,
p.post_name,
p.post_content,
p.post_excerpt,
p.post_date,
p.post_modified,
p.post_parent,
p.menu_order,
p.post_status,
p.post_type
FROM {$wpdb->posts} p
WHERE p.post_type IN ('page', 'post')
AND p.post_status = 'publish'
ORDER BY p.post_parent, p.menu_order
");
$export = [];
foreach ($pages as $page) {
// Получаем мета-поля
$meta = $wpdb->get_results($wpdb->prepare("
SELECT meta_key, meta_value
FROM {$wpdb->postmeta}
WHERE post_id = %d
", $page->ID));
$metaFields = [];
foreach ($meta as $m) {
$metaFields[$m->meta_key] = $m->meta_value;
}
// Получаем категории/теги
$terms = $wpdb->get_results($wpdb->prepare("
SELECT t.name, t.slug, tt.taxonomy
FROM {$wpdb->term_relationships} tr
JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
WHERE tr.object_id = %d
", $page->ID));
$categories = [];
$tags = [];
foreach ($terms as $term) {
if ($term->taxonomy === 'category') {
$categories[] = $term->name;
} elseif ($term->taxonomy === 'post_tag') {
$tags[] = $term->name;
}
}
$export[] = [
'id' => $page->ID,
'title' => $page->post_title,
'alias' => $page->post_name,
'content' => $page->post_content,
'summary' => $page->post_excerpt,
'published_date' => $page->post_date,
'modified_date' => $page->post_modified,
'parent_id' => $page->post_parent,
'menu_order' => $page->menu_order,
'post_type' => $page->post_type,
'meta_fields' => $metaFields,
'categories' => $categories,
'tags' => $tags
];
}
return $export;
}
function exportUsers($wpdb) {
$users = $wpdb->get_results("
SELECT
u.ID,
u.user_login,
u.user_email,
u.user_registered,
u.display_name,
um1.meta_value as first_name,
um2.meta_value as last_name
FROM {$wpdb->users} u
LEFT JOIN {$wpdb->usermeta} um1 ON u.ID = um1.user_id AND um1.meta_key = 'first_name'
LEFT JOIN {$wpdb->usermeta} um2 ON u.ID = um2.user_id AND um2.meta_key = 'last_name'
ORDER BY u.ID
");
return $users;
}
function exportAttachments($wpdb) {
$attachments = $wpdb->get_results("
SELECT
p.ID,
p.post_title,
p.post_name,
p.post_parent,
p.guid,
pm.meta_value as file_path
FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = 'attachment'
AND pm.meta_key = '_wp_attached_file'
");
return $attachments;
}
// Экспортируем данные
$export_data = [
'pages' => exportPages($wpdb),
'users' => exportUsers($wpdb),
'attachments' => exportAttachments($wpdb),
'site_url' => get_option('siteurl'),
'export_date' => date('Y-m-d H:i:s')
];
// Сохраняем в JSON
file_put_contents('wordpress_export.json', json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo "Экспорт завершён. Файл: wordpress_export.json\n";
echo "Страниц экспортировано: " . count($export_data['pages']) . "\n";
echo "Пользователей: " . count($export_data['users']) . "\n";
echo "Медиафайлов: " . count($export_data['attachments']) . "\n";
Скрипт импорта в MODX
<?php
// import_to_modx.php - скрипт импорта в MODX
require_once 'config.core.php';
require_once MODX_CORE_PATH . 'config/' . MODX_CONFIG_KEY . '.inc.php';
require_once MODX_CONNECTORS_PATH . 'index.php';
$modx = new modX();
$modx->initialize('mgr');
// Загружаем данные экспорта
$exportData = json_decode(file_get_contents('wordpress_export.json'), true);
class WordPressImporter {
private $modx;
private $wpToModxMap = []; // Маппинг ID WordPress -> MODX
public function __construct($modx) {
$this->modx = $modx;
}
public function importPages($pages) {
// Сортируем страницы: сначала родители, потом дети
usort($pages, function($a, $b) {
return $a['parent_id'] <=> $b['parent_id'];
});
foreach ($pages as $wpPage) {
$resource = $this->modx->newObject('modResource');
// Определяем родителя
$parentId = 0;
if ($wpPage['parent_id'] > 0 && isset($this->wpToModxMap[$wpPage['parent_id']])) {
$parentId = $this->wpToModxMap[$wpPage['parent_id']];
}
// Определяем шаблон
$template = ($wpPage['post_type'] === 'page') ? 2 : 3; // Страница или статья
// Конвертируем контент
$content = $this->convertContent($wpPage['content']);
$resource->fromArray([
'pagetitle' => $wpPage['title'],
'longtitle' => $wpPage['title'],
'alias' => $wpPage['alias'],
'description' => $this->cleanText($wpPage['summary']),
'content' => $content,
'template' => $template,
'parent' => $parentId,
'published' => 1,
'publishedon' => strtotime($wpPage['published_date']),
'editedon' => strtotime($wpPage['modified_date']),
'menuindex' => $wpPage['menu_order'],
'context_key' => 'web',
'class_key' => 'modDocument'
]);
if ($resource->save()) {
$modxId = $resource->get('id');
$this->wpToModxMap[$wpPage['id']] = $modxId;
// Импортируем мета-поля как TV
$this->importMetaFields($resource, $wpPage['meta_fields']);
// Импортируем категории как теги
if (!empty($wpPage['categories']) || !empty($wpPage['tags'])) {
$allTags = array_merge($wpPage['categories'], $wpPage['tags']);
$resource->setTVValue('tags', implode(',', $allTags));
}
echo "Импортирована страница: {$wpPage['title']} (ID: {$modxId})\n";
} else {
echo "Ошибка импорта страницы: {$wpPage['title']}\n";
}
}
}
private function convertContent($content) {
// Конвертируем шорткоды WordPress
$content = preg_replace('/\[gallery[^\]]*\]/', '[[Gallery]]', $content);
$content = preg_replace('/\[contact-form[^\]]*\]/', '[[FormIt]]', $content);
// Обновляем ссылки на изображения
$content = str_replace('/wp-content/uploads/', '/assets/images/', $content);
// Убираем ненужные классы и атрибуты
$content = preg_replace('/class="[^"]*"/', '', $content);
$content = preg_replace('/style="[^"]*"/', '', $content);
return $content;
}
private function importMetaFields($resource, $metaFields) {
// Маппинг популярных мета-полей WordPress
$fieldMapping = [
'_yoast_wpseo_title' => 'seo_title',
'_yoast_wpseo_metadesc' => 'seo_description',
'_yoast_wpseo_opengraph-title' => 'og_title',
'_yoast_wpseo_opengraph-description' => 'og_description',
'_thumbnail_id' => 'image',
'price' => 'price',
'gallery' => 'gallery'
];
foreach ($metaFields as $key => $value) {
if (isset($fieldMapping[$key])) {
$tvName = $fieldMapping[$key];
// Специальная обработка для thumbnail
if ($key === '_thumbnail_id') {
$value = $this->getAttachmentUrl($value);
}
$resource->setTVValue($tvName, $value);
}
}
$resource->save();
}
private function getAttachmentUrl($attachmentId) {
// Здесь должна быть логика получения URL файла по ID
// Упрощённая версия
return "/assets/images/imported_{$attachmentId}.jpg";
}
public function importUsers($users) {
foreach ($users as $wpUser) {
// Проверяем, не существует ли уже пользователь
$existingUser = $this->modx->getObject('modUser', ['username' => $wpUser->user_login]);
if ($existingUser) {
continue;
}
$user = $this->modx->newObject('modUser');
$user->fromArray([
'username' => $wpUser->user_login,
'password' => md5(time()), // Случайный пароль
'email' => $wpUser->user_email,
'active' => 1,
'createdon' => strtotime($wpUser->user_registered)
]);
if ($user->save()) {
// Создаём профиль
$profile = $this->modx->newObject('modUserProfile');
$profile->fromArray([
'internalKey' => $user->get('id'),
'fullname' => $wpUser->display_name,
'email' => $wpUser->user_email
]);
$profile->save();
echo "Импортирован пользователь: {$wpUser->user_login}\n";
}
}
}
public function generateRedirects() {
// Создаём файл редиректов для .htaccess
$redirects = [];
foreach ($this->wpToModxMap as $wpId => $modxId) {
$modxResource = $this->modx->getObject('modResource', $modxId);
if ($modxResource) {
$newUrl = $this->modx->makeUrl($modxId);
// WordPress URL patterns
$redirects[] = "Redirect 301 /?p={$wpId} {$newUrl}";
$redirects[] = "Redirect 301 /{$modxResource->get('alias')}/ {$newUrl}";
}
}
file_put_contents('redirects.htaccess', implode("\n", $redirects));
echo "Файл редиректов создан: redirects.htaccess\n";
}
private function cleanText($text) {
return trim(strip_tags($text));
}
}
// Запуск импорта
$importer = new WordPressImporter($modx);
echo "Начинаем импорт страниц...\n";
$importer->importPages($exportData['pages']);
echo "Импортируем пользователей...\n";
$importer->importUsers($exportData['users']);
echo "Генерируем редиректы...\n";
$importer->generateRedirects();
echo "Импорт завершён!\n";
Миграция медиафайлов
Скрипт переноса файлов
#!/bin/bash
# migrate_media.sh
WORDPRESS_UPLOADS="/path/to/wordpress/wp-content/uploads"
MODX_ASSETS="/path/to/modx/assets/images"
echo "Начинаем перенос медиафайлов..."
# Создаём структуру папок в MODX
mkdir -p "$MODX_ASSETS/imported"
mkdir -p "$MODX_ASSETS/gallery"
mkdir -p "$MODX_ASSETS/docs"
# Переносим изображения
find "$WORDPRESS_UPLOADS" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" -o -name "*.gif" -o -name "*.webp" \) -exec cp {} "$MODX_ASSETS/imported/" \;
# Переносим документы
find "$WORDPRESS_UPLOADS" -type f \( -name "*.pdf" -o -name "*.doc" -o -name "*.docx" -o -name "*.xls" -o -name "*.xlsx" \) -exec cp {} "$MODX_ASSETS/docs/" \;
# Оптимизируем изображения
if command -v jpegoptim &> /dev/null; then
jpegoptim "$MODX_ASSETS/imported/"*.jpg
fi
if command -v pngquant &> /dev/null; then
pngquant --ext=.png --force "$MODX_ASSETS/imported/"*.png
fi
echo "Перенос медиафайлов завершён"
Обновление ссылок в контенте
<?php
// update_media_links.php
require_once 'config.core.php';
require_once MODX_CORE_PATH . 'config/' . MODX_CONFIG_KEY . '.inc.php';
require_once MODX_CONNECTORS_PATH . 'index.php';
$modx = new modX();
$modx->initialize('mgr');
// Паттерны для замены
$patterns = [
'/wp-content\/uploads\/\d{4}\/\d{2}\//i' => '/assets/images/imported/',
'/https?:\/\/old-domain\.com/i' => 'https://new-domain.com',
'/\[gallery[^\]]*\]/i' => '[[Gallery]]',
'/\[contact-form-7[^\]]*\]/i' => '[[FormIt]]'
];
// Получаем все ресурсы
$resources = $modx->getCollection('modResource');
foreach ($resources as $resource) {
$content = $resource->get('content');
$originalContent = $content;
// Применяем замены
foreach ($patterns as $pattern => $replacement) {
$content = preg_replace($pattern, $replacement, $content);
}
// Если контент изменился, сохраняем
if ($content !== $originalContent) {
$resource->set('content', $content);
$resource->save();
echo "Обновлён ресурс: " . $resource->get('pagetitle') . "\n";
}
}
echo "Обновление ссылок завершено\n";
Настройка редиректов
Автоматическая генерация .htaccess
<?php
// generate_redirects.php
require_once 'config.core.php';
require_once MODX_CORE_PATH . 'config/' . MODX_CONFIG_KEY . '.inc.php';
require_once MODX_CONNECTORS_PATH . 'index.php';
$modx = new modX();
$modx->initialize('web');
class RedirectGenerator {
private $modx;
private $wpUrls = [];
public function __construct($modx) {
$this->modx = $modx;
$this->loadWordPressUrls();
}
private function loadWordPressUrls() {
// Загружаем старые URL из файла экспорта или базы
$exportData = json_decode(file_get_contents('wordpress_export.json'), true);
foreach ($exportData['pages'] as $page) {
$this->wpUrls[$page['id']] = [
'slug' => $page['alias'],
'title' => $page['title'],
'post_type' => $page['post_type']
];
}
}
public function generateRedirects() {
$redirects = [];
// Заголовок файла
$redirects[] = "# WordPress to MODX Redirects";
$redirects[] = "# Generated on " . date('Y-m-d H:i:s');
$redirects[] = "";
// Получаем все ресурсы MODX
$resources = $modx->getCollection('modResource');
foreach ($resources as $resource) {
$modxUrl = $modx->makeUrl($resource->get('id'));
$alias = $resource->get('alias');
// Ищем соответствующую страницу WordPress
foreach ($this->wpUrls as $wpId => $wpData) {
if ($wpData['slug'] === $alias ||
$this->similar($wpData['title'], $resource->get('pagetitle'))) {
// WordPress паттерны URL
if ($wpData['post_type'] === 'post') {
// Записи: /2024/03/post-name/
$redirects[] = "RedirectMatch 301 ^/\\d{4}/\\d{2}/" . preg_quote($wpData['slug'], '/') . "/?$ " . $modxUrl;
// Записи: /?p=123
$redirects[] = "Redirect 301 /?p=" . $wpId . " " . $modxUrl;
} else {
// Страницы: /page-name/
$redirects[] = "Redirect 301 /" . $wpData['slug'] . "/ " . $modxUrl;
// Страницы: /?page_id=123
$redirects[] = "Redirect 301 /?page_id=" . $wpId . " " . $modxUrl;
}
break;
}
}
}
// Специальные редиректы WordPress
$redirects[] = "";
$redirects[] = "# WordPress specific redirects";
$redirects[] = "Redirect 301 /wp-admin/ /manager/";
$redirects[] = "Redirect 301 /feed/ /rss.xml";
$redirects[] = "Redirect 301 /comments/feed/ /rss.xml";
$redirects[] = "RedirectMatch 301 ^/author/.* /";
$redirects[] = "RedirectMatch 301 ^/tag/.* /";
$redirects[] = "RedirectMatch 301 ^/\\d{4}/$ /blog/";
$redirects[] = "RedirectMatch 301 ^/\\d{4}/\\d{2}/$ /blog/";
return implode("\n", $redirects);
}
private function similar($str1, $str2) {
return similar_text(strtolower($str1), strtolower($str2), $percent) && $percent > 80;
}
}
$generator = new RedirectGenerator($modx);
$redirectContent = $generator->generateRedirects();
file_put_contents('.htaccess_redirects', $redirectContent);
echo "Файл редиректов создан: .htaccess_redirects\n";
echo "Добавьте содержимое этого файла в ваш .htaccess перед основными правилами MODX\n";
Мониторинг 404 ошибок
<?php
// Плагин: Monitor404
// События: OnPageNotFound
switch ($modx->event->name) {
case 'OnPageNotFound':
$requestUri = $_SERVER['REQUEST_URI'];
// Логируем 404 ошибки
$logFile = MODX_CORE_PATH . 'cache/logs/404_errors.log';
$logEntry = date('Y-m-d H:i:s') . " | " . $_SERVER['HTTP_HOST'] . $requestUri . " | " .
($_SERVER['HTTP_REFERER'] ?? 'Direct') . " | " . $_SERVER['HTTP_USER_AGENT'] . "\n";
file_put_contents($logFile, $logEntry, FILE_APPEND);
// Пытаемся найти похожую страницу
$suggestedUrl = $this->findSimilarPage($requestUri);
if ($suggestedUrl) {
// Автоматический редирект на похожую страницу
$modx->sendRedirect($suggestedUrl, ['responseCode' => 'HTTP/1.1 301 Moved Permanently']);
}
break;
}
function findSimilarPage($requestUri) {
global $modx;
// Убираем слеши и расширения
$cleanUri = trim(str_replace(['.html', '.php', '/'], '', $requestUri));
// Ищем ресурс с похожим алиасом
$resource = $modx->getObject('modResource', [
'alias:LIKE' => '%' . $cleanUri . '%',
'published' => 1,
'deleted' => 0
]);
if ($resource) {
return $modx->makeUrl($resource->get('id'), '', '', 'full');
}
return null;
}
Замена функционала плагинов
Contact Form 7 → FormIt
<!-- WordPress: Contact Form 7 -->
[contact-form-7 id="123" title="Contact form"]
<!-- MODX: FormIt -->
[[!FormIt?
&hooks=`spam,email,redirect`
&emailTpl=`ContactEmailTpl`
&emailTo=`admin@site.com`
&redirectTo=`2`
&validate=`name:required,email:email:required,message:required`
]]
<form method="post">
<label>Имя: <input type="text" name="name" value="[[!+fi.name]]" required></label>
<label>Email: <input type="email" name="email" value="[[!+fi.email]]" required></label>
<label>Сообщение: <textarea name="message" required>[[!+fi.message]]</textarea></label>
<button type="submit">Отправить</button>
</form>
Yoast SEO → SEO Suite
// WordPress мета-поля Yoast
_yoast_wpseo_title
_yoast_wpseo_metadesc
_yoast_wpseo_canonical
// MODX TV-параметры
seo_title
seo_description
canonical_url
// В шаблоне MODX
<title>[[*seo_title:default=`[[*pagetitle]]`]] - [[++site_name]]</title>
<meta name="description" content="[[*seo_description:default=`[[*description]]`]]">
<link rel="canonical" href="[[*canonical_url:default=`[[~[[*id]]]]`]]">
WooCommerce → miniShop2
// Маппинг структуры данных
// WordPress WooCommerce
wp_posts (post_type = 'product')
wp_postmeta (_price, _stock, _sku)
wp_woocommerce_order_items
// MODX miniShop2
ms2_products
ms2_product_data (price, article, weight)
ms2_orders / ms2_order_products
Тестирование и запуск
Чек-лист тестирования
Контент:
- [ ] Все страницы отображаются корректно
- [ ] Сохранена иерархия страниц
- [ ] Медиафайлы загружаются
- [ ] Формы работают
SEO:
- [ ] URL структура сохранена или настроены редиректы
- [ ] Метатеги перенесены корректно
- [ ] Sitemap.xml обновлён
- [ ] Роботс.txt актуализирован
Функционал:
- [ ] Поиск работает
- [ ] Пользователи могут авторизоваться
- [ ] Интеграции (аналитика, формы) настроены
- [ ] Производительность оптимизирована
Безопасность:
- [ ] Старые файлы WordPress удалены
- [ ] Настроены права доступа к файлам
- [ ] SSL сертификат работает
- [ ] Настроен файрвол
Скрипт финальной очистки
#!/bin/bash
# cleanup_wordpress.sh
echo "Очистка файлов WordPress..."
# Удаляем WordPress файлы (ОСТОРОЖНО!)
rm -rf wp-admin/
rm -rf wp-includes/
rm -f wp-*.php
rm -f xmlrpc.php
rm -f license.txt
rm -f readme.html
# Очищаем .htaccess от WordPress правил
sed -i '/# BEGIN WordPress/,/# END WordPress/d' .htaccess
echo "Очистка завершена"
echo "ВНИМАНИЕ: Проверьте работоспособность сайта!"
Заключение
Миграция с WordPress на MODX — серьёзный проект, который требует тщательного планирования. Ключевые принципы успешной миграции:
- Детальное планирование — анализируйте текущий функционал перед началом
- Поэтапная реализация — тестируйте каждый этап на копии сайта
- Сохранение SEO — настройте все редиректы перед переключением DNS
- Тестирование — проверьте все функции перед запуском
Почему стоит мигрировать:
- Чистый код вместо «макарон» WordPress
- Лучшая производительность и безопасность
- Гибкость в реализации любых идей
- SEO из коробки без дополнительных плагинов
MODX даёт вам контроль над каждой строчкой кода и позволяет создавать действительно профессиональные решения. Время, потраченное на миграцию, окупится удобством разработки и поддержки.
Связанные статьи: Продвинутая разработка на MODX, MODX vs WordPress: сравнение CMS