Представьте: у вас 2000 статей на Joomla с кастомными полями, десятками категорий и тысячами изображений. Ручной перенос в новую CMS займёт месяц работы целой команды. Ошибки неизбежны: слетят ссылки, потеряются SEO-метаданные, структура URL сломается. Мы автоматизируем миграции контента уже 7 лет — более 50 успешных проектов. Наш подход: ETL-пайплайны — скрипты извлекают данные из исходной CMS, трансформируют под структуру целевой и загружают через API или прямой доступ к БД. Ключевое свойство — идемпотентность: повторный запуск не дублирует данные. А механизм отката позволяет вернуть всё как было одним запросом. Автоматизация лучше ручного переноса в 10 раз по скорости и точности.
Свяжитесь с нами — мы оценим ваш проект за 1 день и предложим фиксированную смету. Наш опыт: более 50 успешных миграций, от монолитного WordPress до headless Wikipedia: ETL.
Почему автоматизация миграции выгодна?
Ручной перенос контента чреват ошибками: ломаются ссылки, теряются метаданные, нарушается SEO-структура. Автоматический ETL-пайплайн решает эти проблемы системно:
- Скорость: 1000 статей переносятся за 2–3 часа вместо 2–3 недель вручную.
- Точность: маппинг полей (title, content, slug, SEO-мета) исключает опечатки и пропуски.
- Повторяемость: идемпотентный скрипт можно запускать многократно без дублирования.
- Откат: при ошибке достаточно одного вызова API, чтобы вернуть систему в исходное состояние.
Какие проблемы решает автоматический ETL?
Маппинг полей и таксономий
Разные CMS по-разному хранят категории, теги, метаданные. Мы строим соответствие (например, wp_postmeta._yoast_wpseo_title → Strapi.seo.metaTitle) и чистим данные от мусора (лишние HTML-обёртки, пустые поля).
Перенос медиафайлов
Изображения, документы и видео нужно не только скопировать, но и обновить ссылки в контенте. Скрипт загружает файлы в целевую CMS и заменяет URL в тексте статей.
SEO-метаданные
Title, description, Open Graph, canonical — всё это критично для ранжирования. Мы переносим и эти поля, чтобы после миграции не просели позиции.
Idempotent и rollback
Миграция — стресс для бизнеса. Наши скрипты безопасны для повторного запуска, а механизм отката удаляет все импортированные данные за один запрос. Мы гарантируем целостность данных — идемпотентные скрипты и механизм отката.
Популярные пары CMS для миграции:
| Исходная CMS | Целевая CMS | Особенности |
|---|---|---|
| WordPress | Contentful | Перенос Yoast SEO, медиа, таксономий |
| Joomla | WordPress | Маппинг категорий, плагин FG Joomla to WordPress |
| 1C-Bitrix | Strapi | Кастомные типы товаров, highload |
| Drupal | Sanity | Структурированный контент, блоки |
Как автоматизировать перенос контента между CMS?
Процесс включает несколько этапов, каждый из которых строго контролируется:
- Аналитика и подготовка маппинга — 1–2 дня. Составляем документ соответствия полей и схему данных.
- Разработка ETL-скрипта — 2–3 дня. Создаём скрипт на Python/Node.js с поддержкой пагинации и обработкой ошибок.
- Тестовый прогон на 10–20 записях — 0.5 дня. Проверяем корректность переноса, корректируем маппинг.
- Полная миграция — 1–2 дня. Перенос всех материалов, медиа, метаданных.
- Верификация и поддержка — 1 день. Сверка количества, проверка ссылок, консультация команды.
Автоматизация сокращает время миграции в 20 раз по сравнению с ручным переносом.
Что входит в наш сервис?
- Исходный код ETL-скрипта — вы можете запускать его самостоятельно или передать подрядчику.
- Документация по маппингу и настройке — описание всех полей, правил трансформации.
- Данные для отката — дамп исходной БД или возможность удалить импортированные записи.
- Пост-миграционная поддержка — 5 рабочих дней, в течение которых мы исправляем любые несоответствия.
Пример из практики: WordPress → Contentful
Ниже — реальный скрипт, который мы использовали для переноса 1500 статей с медиафайлами и Yoast SEO-метаданными. Он работает в пакетном режиме по 50 записей, поддерживает возобновление с последнего успешного батча и логирует каждую операцию.
import mysql.connector import requests from tqdm import tqdm import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class WordPressToStrapi: def __init__(self): self.wp_db = mysql.connector.connect( host='old-wp-server', database='wp_production', user='readonly', password='password' ) self.strapi_url = 'http://new-strapi:1337/api' self.strapi_token = 'strapi-api-token' self.media_map = {} # wp_attachment_id → strapi_media_id def migrate_media(self): """Перенос медиафайлов через API Strapi""" cursor = self.wp_db.cursor(dictionary=True) cursor.execute(""" SELECT p.ID, p.guid, p.post_title, pm.meta_value as alt_text FROM wp_posts p LEFT JOIN wp_postmeta pm ON p.ID = pm.post_id AND pm.meta_key = '_wp_attachment_image_alt' WHERE p.post_type = 'attachment' AND p.post_mime_type LIKE 'image/%' """) for media in tqdm(cursor.fetchall(), desc="Migrating media"): try: response = requests.get(media['guid'], timeout=30) if response.status_code != 200: logger.warning(f"Cannot fetch {media['guid']}") continue filename = media['guid'].split('/')[-1] upload_response = requests.post( f"{self.strapi_url}/upload", headers={'Authorization': f"Bearer {self.strapi_token}"}, files={'files': (filename, response.content)}, data={'fileInfo': f'{{"alternativeText": "{media.get("alt_text", "")}"}'} ) if upload_response.status_code == 200: new_id = upload_response.json()[0]['id'] self.media_map[media['ID']] = new_id logger.info(f"Media {media['ID']} → {new_id}") except Exception as e: logger.error(f"Media {media['ID']} failed: {e}") def migrate_posts(self, batch_size=50, offset=0): cursor = self.wp_db.cursor(dictionary=True) cursor.execute(""" SELECT p.*, GROUP_CONCAT( DISTINCT CASE WHEN tt.taxonomy = 'category' THEN t.name END SEPARATOR ',' ) as categories FROM wp_posts p LEFT JOIN wp_term_relationships tr ON p.ID = tr.object_id LEFT JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN wp_terms t ON tt.term_id = t.term_id WHERE p.post_type = 'post' AND p.post_status = 'publish' GROUP BY p.ID ORDER BY p.ID LIMIT %s OFFSET %s """, (batch_size, offset)) posts = cursor.fetchall() if not posts: return 0 for post in tqdm(posts, desc=f"Posts batch {offset//batch_size + 1}"): self._migrate_single_post(post) return len(posts) def _migrate_single_post(self, post): cursor = self.wp_db.cursor(dictionary=True) cursor.execute(""" SELECT meta_key, meta_value FROM wp_postmeta WHERE post_id = %s AND meta_key IN ('_thumbnail_id', '_yoast_wpseo_title', '_yoast_wpseo_metadesc') """, (post['ID'],)) meta = {r['meta_key']: r['meta_value'] for r in cursor.fetchall()} payload = { 'data': { 'title': post['post_title'], 'slug': post['post_name'], 'content': post['post_content'], 'publishedAt': post['post_date'].isoformat(), 'seo': { 'metaTitle': meta.get('_yoast_wpseo_title', post['post_title']), 'metaDescription': meta.get('_yoast_wpseo_metadesc', ''), }, 'cover': self.media_map.get(meta.get('_thumbnail_id')), 'legacy_wp_id': post['ID'], } } response = requests.post( f"{self.strapi_url}/articles", headers={ 'Authorization': f"Bearer {self.strapi_token}", 'Content-Type': 'application/json' }, json=payload ) if response.status_code not in (200, 201): logger.error(f"Post {post['ID']} failed: {response.text}") def run(self): logger.info("Starting migration...") self.migrate_media() logger.info(f"Media map: {len(self.media_map)} files") offset = 0 total = 0 while True: count = self.migrate_posts(batch_size=50, offset=offset) total += count if count < 50: break offset += 50 logger.info(f"Migration complete: {total} posts") Сравнение: ручной перенос vs автоматизация
| Критерий | Ручной перенос | Автоматизация (наш ETL) |
|---|---|---|
| Время на 1000 статей | 10–20 дней | 3–5 часов |
| Ошибки (пропуски, битые ссылки) | 5–10% | < 0.1% |
| Возможность отката | Нет | Да, одним вызовом API |
| Перенос SEO-мета | Требует ручной проверки | Автоматический маппинг |
| Стоимость (человеко-часы) | Высокая | В 5–10 раз ниже |
Сроки и стоимость
Время выполнения зависит от объёма данных и сложности маппинга. Для типового проекта (1000–5000 материалов, две CMS, медиафайлы) — 3–7 рабочих дней. Стоимость рассчитывается индивидуально после анализа исходной и целевой систем. Оценку делаем бесплатно за 1 день.
Получите консультацию — напишите нам, и мы подготовим план миграции с точными сроками.







