Типичная заявка: «переехали на новый домен, главная открывается, в админку пускает, а меню пустое, виджеты слетели, Elementor открывается белым экраном, в кастомайзере ошибка». Иногда мягче: «часть картинок с старого адреса, часть с нового», «письма клиентам уходят со старой ссылкой», «после смены домена пропал футер».

Я почти всегда нахожу одно и то же. Кто-то сделал в phpMyAdmin REPLACE по option_value или прогнал дамп через «найти и заменить» в редакторе. Поля siteurl и home в wp_options — обычные строки, они меняются и сайт «как будто живой». Виджеты, theme_mods, ACF, настройки кэша и куча plugin-опций лежат как PHP-сериализация. Наивная замена портит длину строк внутри этой каши. unserialize() возвращает false, WordPress считает «настроек нет» и рисует пустые сайдбары.

Ниже — как я перевожу домен, чтобы не чинить потом сериализацию руками. Хосты в примерах вымышленные: old.exampleshop.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 инструментом, который эти длины пересчитывает, потом отдельно ищу хвосты в файлах и кэше, и только после живых виджетов и пустого поиска по старому хосту считаю переезд состоявшимся — не в момент, когда открылась главная.

Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?

Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.

Обсудить задачу

Помогла статья? Поблагодари автора!

Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.

Угостить автора чашечкой кофе