Типичная заявка: «переехали на новый домен, главная открывается, в админку пускает, а меню пустое, виджеты слетели, Elementor открывается белым экраном, в кастомайзере ошибка». Иногда мягче: «часть картинок с старого адреса, часть с нового», «письма клиентам уходят со старой ссылкой», «после смены домена пропал футер».
Я почти всегда нахожу одно и то же. Кто-то сделал в phpMyAdmin REPLACE по option_value или прогнал дамп через «найти и заменить» в редакторе. Поля siteurl и home в wp_options — обычные строки, они меняются и сайт «как будто живой». Виджеты, theme_mods, ACF, настройки кэша и куча plugin-опций лежат как PHP-сериализация. Наивная замена портит длину строк внутри этой каши. unserialize() возвращает false, WordPress считает «настроек нет» и рисует пустые сайдбары.
Ниже — как я перевожу домен, чтобы не чинить потом сериализацию руками. Хосты в примерах вымышленные: old.example → shop.example. Свои домены, префикс таблиц и путь к сайту подставляйте, чужой дамп из интернета на прод не накатывайте.
Почему обычный REPLACE ломает сайт
PHP пишет строку не как «текст», а как «длина плюс текст». Упрощённо:
s:19:"https://old.example";
«19» — это число байт внутри кавычек, не «примерно длина». Если в дампе или SQL вы меняете old.example на shop.example, букв становится больше, а префикс s:19: остаётся. Для интерпретатора это уже не валидная сериализация. Массив вокруг тоже разваливается: один битый кусок — и весь option «не читается».
Отсюда картина, которую путают с «плагин не перенёсся»:
- Админка работает: логин, список записей, даже медиатека. Это не сериализация, это обычные таблицы.
- Виджеты пустые или «сбросились на дефолт». Классика —
sidebars_widgetsиwidget_*. - Кастомайзер/меню выглядят как новый сайт на чистой теме:
theme_mods_*не распаковался. - Страница открывается, визуальный редактор — нет. У конструкторов URL ещё и в JSON/CSS, не только в serialize.
- В PHP-логах за ночь:
unserialize(): Error at offset …наoption_valueили postmeta. Это не «хостер глючит», это вы вчера поправили домен в БД.
siteurl/home я вообще не считаю критерием успеха. Их можно поменять руками за минуту. Критерий — живые виджеты, меню, конструктор и отсутствие старого хоста в опциях, которые WordPress реально читает.
Что я снимаю до любой замены
Пока не лежит откат, я замену не запускаю. Испорченная сериализация откатывается из дампа за две минуты и чинится «на глаз» полдня.
Минимум:
- Дамп базы до замены, не «сейчас сниму, если что». И файл не в
wp-content/uploadsэтого же сайта. - Архив файлов: тема,
uploads,mu-plugins. Конструкторы после смены домена часто оставляют CSS со старым хостом уже в файлах, не в MySQL. - Текущие
siteurlиhome, плюс константы изwp-config.php. Если заданыWP_HOME/WP_SITEURL, база может врать, а сайт всё равно ходить на старый адрес — или наоборот, «замена в БД не действует». - Список вариантов URL, которые реально встречаются. Не один «красивый» https.
Варианты, которые я выписываю в блокнот до команды, не в процессе:
https://old.example http://old.example https://www.old.example http://www.old.example //old.example old.example
Слеш на конце — отдельная история. В опциях чаще без него, в контенте и в JSON конструктора бывает с ним. Менять «с слэшем» и «без» двумя проходами безопаснее, чем надеяться, что один проход поймает оба.
Голый old.example без схемы я оставляю на последний проход и смотрю отчёт: он может задеть e-mail вида shop@old.example, CDN, упоминания в тексте. Если почта на этом домене остаётся — голый хост не трогаю или меняю точечно.
Префикс таблиц не угадываю:
# каталог сайта подставьте свой cd /var/www/example-site/public_html wp config get table_prefix wp option get siteurl wp option get home wp config get WP_HOME wp config get WP_SITEURL
Если две последние константы заданы и расходятся с тем, куда вы переезжаете, сначала правлю wp-config.php. Иначе search-replace в базе вы сделаете, а WordPress продолжит считать каноническим старый URL.
WP-CLI: основной путь
На VPS и нормальном хостинге с SSH я не открываю phpMyAdmin для этой задачи. wp search-replace ходит по ячейкам, понимает PHP-сериализацию (и в свежих версиях — JSON), первичные ключи не трогает.
Сначала сухой прогон, чтобы увидеть масштаб, а не «сразу на прод»:
cd /var/www/example-site/public_html wp search-replace 'https://old.example' 'https://shop.example' \ --all-tables-with-prefix \ --precise \ --dry-run \ --report-changed-only
--all-tables-with-prefix нужен потому, что дефолт — таблицы, которые WordPress зарегистрировал в $wpdb. У Magento-в-WP это не про нас, а у WooCommerce, форм, кэша, самописных плагинов таблица wp_something_extra легко остаётся со старым хостом. --all-tables без префикса я на шареде с общей MySQL не включаю: можно вылезти в чужую базу на том же пользователе.
--precise гоняет замену через PHP, не через быстрый SQL. Медленнее, зато меньше шансов проскочить колонку, где сериализация не похожа на «очевидную». На магазине в сотни мегабайт дампа это не мгновенно — зато не потом ночью ловить пустую корзину.
Если сухой прогон выглядит здраво (есть попадания в wp_options, postmeta, может быть post content — и нет сюрприза в wp_users.user_email на тысячи строк), снимаю ещё один свежий дамп и запускаю без --dry-run.
GUID записей я обычно пропускаю:
wp search-replace 'https://old.example' 'https://shop.example' \ --all-tables-with-prefix \ --precise \ --skip-columns=guid \ --report-changed-only
guid в WordPress — идентификатор, не «красивая ссылка». Менять его при переезде домена не обязательно, а фиды и импорты от этого иногда едут. Если у вас старый зоопарк, где в GUID руками писали permalink — смотрите отчёт dry-run по колонке и решайте точечно, не «всем REPLACE».
Дальше те же команды для http://, www и протоколо-относительного //old.example. Один проход по «основному» https почти никогда не вычищает всё: редирект на https могли включить год назад, а в виджете 2019 года лежит http.
Если WP-CLI есть на старом сервере, а на новом ещё нет, удобен экспорт уже с заменой:
wp search-replace 'https://old.example' 'https://shop.example' \ --all-tables-with-prefix \ --precise \ --skip-columns=guid \ --export=/root/shop-example-after-replace.sql
Накатываю этот SQL на новое место, а не «сырой дамп плюс sed». sed по .sql — тот же класс ошибки, что REPLACE в phpMyAdmin, только в файле на 400 МБ её ещё и не видно.
Если WP-CLI нет
На дешёвом shared без SSH люди ставят плагин вроде Better Search Replace. Под капотом та же идея, что у interconnect/it: идти по ячейкам и пересобирать serialize. Это лучше, чем правка дампа. Это хуже, чем WP-CLI, потому что плагин работает из-под веба, на больших таблицах упирается в timeout, и его ещё надо не забыть удалить.
Правила, без которых я этот путь не рекомендую:
- Сначала дамп. Плагин «умеет dry-run» — dry-run обязателен, как у CLI.
- Ставить, прогнать, снести. Search-Replace, оставленный в корне сайта, — готовая дыра: это скрипт с доступом к базе, который не должен торчать в интернет ни минуты после работы.
- Не мешать в одном заходе «домен» и «путь на диске». Это разные строки. Путь
/home/u1234/public_htmlв сериализации тоже бывает, его меняют отдельно, когда переезд ещё и на другой аккаунт хостинга.
Перенос «архивом плагина миграции» (Duplicator и родственники) сериализацию обычно переживает, потому что замену они делают своим движком, не вашим REPLACE. Имеет смысл, когда нет SSH. Не имеет смысла, когда вы уже на VPS и можете одну команду. Я не тащу лишний плагин на прод ради смены домена.
Что search-replace не видит
База — не весь сайт. После удачной замены в MySQL у меня регулярно всплывает старый хост в файлах.
cd /var/www/example-site/public_html grep -R --binary-files=without-match -l 'old.example' \ wp-content/themes wp-content/plugins wp-content/mu-plugins \ wp-content/uploads wp-config.php 2>/dev/null | head
Типичные находки:
WP_HOME/WP_SITEURLвwp-config.php— перекрывают базу, спор «я же заменил в опциях» решается здесь.- CSS/JSON конструктора в
wp-content/uploads/elementor/(и аналоги у других билдеров). Это файлы. Их WP-CLI не трогает. У Elementor после смены домена я ещё регенерирую CSS из админки или, если есть их CLI, отдельной командой замены URL, не надеясь только на MySQL. - Жёстко прошитый адрес в дочерней теме: логотип, пиксель, кастомный endpoint. Это уже код, не контент.
- Объектный кэш. Redis/Memcached продолжает отдавать старые сериализованные blobs. После замены — flush. Иначе «в базе уже новый домен, в браузере старый» до истечения TTL, и вы начнёте чинить то, что уже починено.
Мультисайт — не тот же чеклист. Там wp_blogs, wp_site, домены блогов, и CLI нужно звать с --url конкретного сайта плюс --network, когда меняете сразу сеть. Если у вас не мультисайт, не копируйте чужие однострочники с --network «на всякий случай».
Как понять, что сериализация жива
Первая проверка — не «главная открылась». Я смотрю то, что сериализация обычно убивает, и то, что REPLACE обычно забывает.
# канонические URL — уже новый домен, без редиректа в голове wp option get siteurl wp option get home # остатки старого хоста по всем таблицам с префиксом wp db search 'old.example' --all-tables-with-prefix --table_column_once # виджеты реально читаются, не «таблица на месте» wp widget list sidebar-1 wp option get sidebars_widgets
Если sidebars_widgets печатает массив с именами сайдбаров — сериализация этого option жива. Если пусто, false или PHP Notice на unserialize — откатились к дампу, не «восстанавливаем виджеты руками».
Грубый проход по опциям, которые WordPress считает сериализованными, но уже не может распаковать:
wp eval '
foreach ( wp_load_alloptions() as $name => $value ) {
if ( ! is_serialized( $value ) ) { continue; }
$restored = @unserialize( $value );
if ( $value === "b:0;" || $restored !== false || $value === serialize( false ) ) { continue; }
echo $name, PHP_EOL;
}
'
Пустой вывод — хороший знак. Имена вроде widget_block, theme_mods_…, external_updates в этом списке — плохой: именно они после «успешного» REPLACE приезжают битыми.
В админке руками:
- Внешний вид → Виджеты / редактор блоков виджетов: блоки на месте, не пустой холст.
- Меню и расположения меню. «Меню существует, но нигде не назначено» часто значит, что theme_mods не прочитался.
- Медиа: открыть пару старых записей, не главную. В контенте
https://old.example/wp-content/uploads/…после нормальной замены быть не должно. - Конструктор: открыть на редактирование страницу, которую точно верстали не вчера. Белый экран при живой публичной странице — почти всегда JSON/CSS со старым хостом или битая meta.
Смешанный контент (главная по https, картинки по http старого домена) я проверяю не глазами, а по исходнику и по логам. Браузер может дорисовать из кэша.
Что смотреть overnight
Днём вы видите то, что кликнули. Ночью всплывает cron, письма и боты.
За ночь мне нужны:
- Письма сайта: сброс пароля, WooCommerce, формы. В теле и в заголовке
From/ссылках не должен торчать старый хост. Это часто неsiteurl, а отдельная опция плагина или константа SMTP. - Cron:
wp cron event listи системный crontab. Старый URL в HTTP-cron (wget наwp-cron.php) продолжает дергать прежний домен, пока DNS жив. - Карта сайта и robots: плагины SEO пишут абсолютные URL. Rank Math/Yoast после переезда я открываю и пересохраняю карту, не надеясь, что «само подхватит».
- Редирект со старого домена. Пока он не 301 на тот же путь нового, Google и закладки клиентов будут кормить оба хоста. Сам search-replace редирект не настраивает — это nginx/хостер.
- Логи PHP на unserialize и на mixed content в error_log. Одна-две строки offset — повод не «подождать», а свернуть на дамп и прогнать замену нормально.
- REST и превью:
/wp-json/должен отдавать новыйhomeвurl/home. Иначе мобильные приложения и часть плагинов продолжают стучать на старое имя.
Если старый домен ещё резолвится в тот же DocumentRoot без редиректа, вы будете неделю ловить «то новый, то старый» в кэше и в сериализации, которую кто-то снова сохранит из админки, зайдя по старому имени. Схема одна: один канонический хост, второй только 301, константы и опции смотрят на канон.
Переезд домена в WordPress — это не «поменять две опции в wp_options». Две опции как раз переживают любой REPLACE. Ломается то, что вы не видите в phpMyAdmin с первого экрана: длины строк внутри serialize. Я меняю URL инструментом, который эти длины пересчитывает, потом отдельно ищу хвосты в файлах и кэше, и только после живых виджетов и пустого поиска по старому хосту считаю переезд состоявшимся — не в момент, когда открылась главная.
Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?
Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.
Обсудить задачуПомогла статья? Поблагодари автора!
Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.
Угостить автора чашечкой кофе