Миграция контента при редизайне сайта
Редизайн почти всегда сопровождается изменением структуры контента: URL-схемы, иерархии страниц, форматов полей. Миграция контента при редизайне отличается от CMS-миграции тем, что платформа может остаться прежней, но меняется семантика данных.
Аудит контента перед редизайном
# Сканирование всех URL сайта
npx screaming-frog-seo-spider --crawl https://mysite.com --headless \
--export-tabs "Crawl Overview,Internal,Response Codes" \
--output-folder ./crawl-results
# Или через sitemap
curl https://mysite.com/sitemap.xml | grep '<loc>' | sed 's/<[^>]*>//g' > urls.txt
wc -l urls.txt
Для каждой страницы фиксируем:
- URL (старый и новый)
- Тип контента
- Уникальные элементы (видео, галереи, формы)
- Важность для SEO (трафик из Google Analytics)
Маппинг URL
// scripts/generate-redirects.ts
// Генерируем файл редиректов на основе маппинга
const urlMapping: Record<string, string> = {
'/blog/category/web-development': '/web-development',
'/services/web-design': '/services/design',
'/about-us/team': '/team',
'/portfolio': '/work',
};
// Для Next.js
const redirects = Object.entries(urlMapping).map(([source, destination]) => ({
source,
destination,
permanent: true,
}));
// Для Nginx
const nginxRules = Object.entries(urlMapping)
.map(([from, to]) => `rewrite ^${from}$ ${to} permanent;`)
.join('\n');
Трансформация структуры контента
Пример: реструктуризация блога с добавлением новых полей:
// Было: простой пост с body
// Стало: пост с intro + body (StreamField) + callout + related_posts
async function transformPost(oldPost: OldPost): Promise<NewPost> {
return {
title: oldPost.title,
slug: oldPost.slug,
intro: extractIntro(oldPost.body), // первый параграф
body: convertToStreamField(oldPost.body),
publishedAt: oldPost.date,
author: await findOrCreateAuthor(oldPost.authorName),
tags: oldPost.tags,
seoTitle: oldPost.seoTitle || oldPost.title,
seoDescription: oldPost.seoDescription || extractIntro(oldPost.body, 160),
};
}
function extractIntro(html: string, maxChars = 250): string {
const firstParagraph = html.match(/<p[^>]*>(.*?)<\/p>/s)?.[1] ?? '';
const text = firstParagraph.replace(/<[^>]*>/g, '');
return text.slice(0, maxChars).trim();
}
Обработка медиафайлов
При переходе на другое хранилище нужно обновить все URL в контенте:
async function updateMediaUrls(content: string, urlMap: Map<string, string>): string {
return content.replace(
/https:\/\/old-domain\.com\/wp-content\/uploads\/([^\s"']+)/g,
(match, path) => urlMap.get(path) || `https://cdn.newdomain.com/${path}`
);
}
// Загрузка медиафайлов и создание маппинга
async function migrateMedia(oldUrls: string[]) {
const urlMap = new Map<string, string>();
for (const url of oldUrls) {
const buffer = await downloadFile(url);
const key = url.split('/uploads/')[1];
const newUrl = await uploadToS3(buffer, key);
urlMap.set(key, newUrl);
}
return urlMap;
}
Параллельный запуск при редизайне
- Заморозка контента — за 2 недели до старта финальной миграции
- Migrate-and-verify — мигрируем данные, верифицируем ключевые страницы
- Parallel run — staging с новым дизайном и реальным контентом
- DNS cutover — переключение в низкий трафик
- Post-launch crawl — проверка редиректов и 404
Валидация результата
# Проверяем, что все старые URL либо отдают 301, либо 200
while IFS= read -r url; do
status=$(curl -s -o /dev/null -w "%{http_code}" "$url")
echo "$status $url"
done < old-urls.txt | grep -v "^301\|^200" > broken.txt
Миграция контента при редизайне среднего сайта (100–500 страниц) — 2–4 недели.







