Перенос контента с WordPress: сотни материалов одним прогоном скрипта
Как забрать тексты и медиа со старого WordPress и разложить их по коллекциям новой CMS одним запуском скрипта. Разведка источников, идемпотентный импорт, пережатие картинок и карта 301-редиректов.
Перенос контента со старого сайта выглядит скучной задачей ровно до момента, когда начинаешь считать. Полсотни страниц ещё можно перекопипастить за вечер под сериал. На двухстах материалах с картинками, галереями и внутренними ссылками ручной перенос превращается в неделю работы и гарантированную россыпь опечаток. Скрипт делает то же самое за минуты и повторяет прогон столько раз, сколько понадобится.
Ниже — то, как я подхожу к этой задаче на реальном проекте: старый сайт на WordPress, новый — на Next.js с Payload CMS и PostgreSQL.
Сначала разведка источников
Забрать контент из WordPress можно тремя способами, и они дают разное качество.
- WP REST API —
/wp-json/wp/v2/posts?per_page=100&page=2. Открыт по умолчанию, отдаёт посты, страницы, медиатеку, таксономии в JSON. Пагинация честная, общее число страниц приходит в заголовкеX-WP-TotalPages. - Экспорт WXR из админки — самый полный вариант: черновики, кастомные поля, метаданные. Требует доступа к админке.
- Парсинг публичного HTML — крайний случай.
Разница между ними заметнее всего на медиа. Через API и WXR вы получаете ссылки на оригиналы из библиотеки. Из HTML тянутся уже пережатые превью с суффиксами размера вроде -1024x683 — качество фото просядет, и это необратимо.
На проекте, который я веду сейчас, доступа к админке WordPress не было, и этап переноса висел заблокированным именно по этой причине. Публичный сайт при этом открыт, так что запасной путь оставался: REST API плюс обход по sitemap.xml.
Скрипт, который можно запускать сто раз
Главное свойство импортёра — идемпотентность. Первый прогон почти наверняка окажется неудачным: где-то поедет разметка, где-то не встанет обложка. Скрипт, который при повторном запуске создаёт дубли, придётся сопровождать ручной чисткой базы, и вся экономия испарится.
Лечится это ключом. Берём slug как естественный идентификатор, ищем запись, обновляем найденное, создаём отсутствующее. В Payload это Local API прямо из Node — payload.find по where: { slug: { equals } }, дальше create или update. Никакого HTTP, никакой авторизации, работа идёт с базой напрямую.
Вторая привычка — разделить прогон на два этапа.
- Дамп. Скачиваем всё в локальные JSON-файлы и папку с медиа. Сеть трогаем один раз.
- Импорт. Читаем с диска, раскладываем по коллекциям.
При ошибке маппинга вы перезапускаете второй этап, не долбя чужой продакшн заново. Плюс дамп остаётся артефактом: к нему можно вернуться через месяц, когда выяснится, что забыли перенести подписи к фото.
Где ломаются медиа
Картинки дают больше всего сюрпризов.
Один и тот же файл в WordPress часто лежит в медиатеке несколькими копиями с разными именами. Дедупликация по SHA-1 содержимого срезает лишнее до заливки. Дальше — пережатие: sharp конвертирует в WebP и AVIF, оригинал стоит сохранить отдельно на случай, если позже понадобится другой размер.
Отдельная работа — ссылки внутри текста. В теле статьи остаются абсолютные URL на старый домен: https://старый-сайт/wp-content/uploads/.... Их нужно переписать на новые пути. Я держу словарь «старый URL → новый ID медиа», который наполняется на этапе заливки файлов, и прогоняю по нему HTML каждого материала.
Разметка WordPress в блоки новой CMS
HTML из WordPress редко ложится в блочный редактор один в один. Внутри встречаются шорткоды ([gallery], [caption]), обёртки от плагинов, инлайновые стили от визуального редактора. Разумная стратегия: конвертировать основную массу автоматически, а список материалов с нераспознанными конструкциями выводить в лог для ручного разбора. Обычно таких оказывается единицы.
URL и редиректы
Старые адреса собирают ссылочный вес и приходят из закладок. Правило простое: где структура URL сохраняется — оставляем как есть, где меняется — ставим 301.
Карта редиректов собирается тем же скриптом. Он и так знает пару «старый путь → новый slug» для каждой записи, остаётся выгрузить расхождения в конфиг. Заодно на новом сайте нужно закрыть от индексации служебные разделы — у меня в robots.txt под запретом личный кабинет:
User-agent: *
Allow: /
Disallow: /cabinet/
Sitemap: https://example.com/sitemap.xml
И добавить структурированные данные: Organization на главной, Article на материалах, Event на анонсах мероприятий.
Чек-лист приёмки
Перенос считается завершённым, когда выполнено всё перечисленное:
- все страницы и материалы наполнены, пустых заготовок нет;
- медиа перенесены и пережаты в WebP/AVIF;
- URL-структура сохранена, для изменённых адресов работают 301;
sitemap.xmlиrobots.txtотдаются корректно;- JSON-LD валидируется в тестере структурированных данных.
Само написание такого скрипта занимает несколько часов. Первый прогон покажет с десяток проблем, второй — две-три, третий пройдёт чисто. Ручной перенос той же сотни материалов эти часы съест и не оставит после себя ничего, что можно запустить повторно.