сайт в бете
нашли баг? напишите
левин. записаться
весь блог

Перенос контента с 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, никакой авторизации, работа идёт с базой напрямую.

Вторая привычка — разделить прогон на два этапа.

  1. Дамп. Скачиваем всё в локальные JSON-файлы и папку с медиа. Сеть трогаем один раз.
  2. Импорт. Читаем с диска, раскладываем по коллекциям.

При ошибке маппинга вы перезапускаете второй этап, не долбя чужой продакшн заново. Плюс дамп остаётся артефактом: к нему можно вернуться через месяц, когда выяснится, что забыли перенести подписи к фото.

Где ломаются медиа

Картинки дают больше всего сюрпризов.

Один и тот же файл в 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 валидируется в тестере структурированных данных.

Само написание такого скрипта занимает несколько часов. Первый прогон покажет с десяток проблем, второй — две-три, третий пройдёт чисто. Ручной перенос той же сотни материалов эти часы съест и не оставит после себя ничего, что можно запустить повторно.

теги #wordpress#миграция#автоматизация#payload cms#редиректы

один разговор — и поймём, чем я могу помочь.

В эпоху ИИ человеку нужен человек. Сяду рядом и доведу до результата — встреча длится столько, сколько нужно. Без скрипта продаж и пакетов «за 999 000 ₽». Если пойму, что помочь не смогу, — скажу сразу.