IWANT - наш собственный fashion-магазин. Несколько лет он жил на InSales: на старте это правильный выбор: быстро, без разработки, всё из коробки. Но в какой-то момент мы уперлись в потолок платформы: каждый нужный модуль - это либо платное приложение, либо «так нельзя». Мы посчитали и решили перевезти магазин на собственный движок.

Это не история «платформы плохие, пишите своё». Это разбор конкретного переезда: что переносили, как устроен ETL из выгрузок InSales, на каком стеке собрали и почему именно на нём, какие модули пришлось писать самим, как прошёл катаут без простоя и кому такой переезд реально нужен, а кому нет.

Почему ушли с InSales

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

Что упиралось в потолок:

1) Нужные функции (серверная корзина между устройствами, кастомная логика писем, кошелёк с кэшбэком) либо платные приложения, либо невозможны в принципе.
2) Любая нестандартная доработка фронта ограничена шаблонной системой.
3) Данные и логика живут в чужом контуре и чем дольше тянешь, тем дороже потом мигрировать.

В какой-то момент аренда платформы со всеми приложениями стала стоить дороже, чем контроль над собственным кодом: тариф InSales с нужными приложениями – это 2 300-10 400 ₽/мес в зависимости от набора. Это и был триггер.

Что переносили

- Каталог товаров со структурой категорий и вариаций.
- 3 123 заказа за 2016-2026 - вся перенесённая история (это объём миграции, а не оборот).
- Клиентов и их историю покупок (включая накопленную лояльность, её нельзя было обнулить).
- SEO-наследие: старые URL, чтобы не потерять позиции.

Главное требование к миграции: ноль потерь данных и сохранение SEO. Падение трафика после переезда - самый частый и самый болезненный провал таких проектов.

ETL: переносим каталог и историю заказов

Каталог и заказы переносили двумя разными путями и засада была ровно в одном месте.

Каталог пришёл из InSales структурированно (товары, вариации, категории, остатки), мы прогоняли его через Zod-схемы и заливали идемпотентным upsert: товары и категории по slug, вариации по sku. Картинки дедуплицировали по MD5: если байты совпали с тем, что уже лежит в S3, повторная заливка пропускается. Это важно, когда импорт 3 700+ товаров с картинками идёт часами и его приходится перезапускать.

Заказы - вот где была первая засада: кодировка. Экспорт заказов InSales отдаётся в UTF-16 LE, tab-separated, ещё и с BOM. Наивное чтение ломает кириллицу. Поэтому декодируем явно и снимаем BOM:

const buffer = readFileSync(path.resolve(csvPath))// Экспорт заказов InSales: UTF-16 LE, tab-separated, с BOMconst text = buffer.toString("utf16le").replace(/^\uFEFF/, "")const rows = parseDelimited(text) // свой RFC4180-парсер, разделитель — таб

// Экспорт заказов InSales: UTF-16 LE, tab-separated, с BOM

const text = buffer.toString("utf16le").replace(/^\uFEFF/, "")

const rows = parseDelimited(text) // свой RFC4180-парсер, разделитель — таб

Стандартный CSV-парсер тут не подходит: поля бывают в кавычках с табами и переводами строк внутри, поэтому делимитер-парсер пришлось написать руками. Вторая засада - это структура: один заказ размазан по нескольким строкам. N строк товаров и отдельная строка «Доставка» делят общий , а пустая строка разделяет заказы. Собираем заказ обратно, группируя по номеру:

const orders = new Map<string, ParsedOrder>()for (const row of rows.slice(1)) {  const n = get(row, "№")  if (!n) continue // пустая строка-разделитель  const order = orders.get(n) ?? createOrder(row)  orders.set(n, order)
  const title = get(row, "Наименование товара (услуги)")  if (title === "Доставка") {    order.shipping += num(get(row, "Сумма для получения"))  } else if (title) {    order.items.push(parseItem(row)) // Артикул → ProductVariant.sku  }}

for (const row of rows.slice(1)) {

const n = get(row, "№")

if (!n) continue // пустая строка-разделитель

const order = orders.get(n) ?? createOrder(row)

orders.set(n, order)

const title = get(row, "Наименование товара (услуги)")

if (title === "Доставка") {

order.shipping += num(get(row, "Сумма для получения"))

} else if (title) {

order.items.push(parseItem(row)) // Артикул → ProductVariant.sku

}

}

Заливку сделали идемпотентной, чтобы повторный прогон не задваивал историю. Каждый перенесённый заказ получает номер IS-<№> (новые заказы в магазине нумеруются IW-YYMM-…, так что коллизий нет), а клиент привязывается по e-mail, иначе по нормализованному телефону:

const orderNumber = `IS-${order.num}`if (existing.has(orderNumber)) { skipped++; continue } // уже импортирован
const customerId =  byEmail.get(order.email.toLowerCase()) ??  byPhone.get(normalisePhone(order.phone)) ?? null

if (existing.has(orderNumber)) { skipped++; continue } // уже импортирован

const customerId =

byEmail.get(order.email.toLowerCase()) ??

byPhone.get(normalisePhone(order.phone)) ?? null

Это потом окупилось: на перенесённой истории заказов (с реальными датами с 2016 года) сразу заработали win-back-письма и рекомендации. Им было на чём учиться с первого дня, а не с нуля.

Катаут без простоя

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

Порядок деплоя жёсткий: prisma migrate deploy прогоняется до сборки (схема БД всегда впереди кода), затем собирается standalone-бандл, обновляется статика и перезапускается сервис. Письма идут через собственную очередь с идемпотентными ключами - повторный прогон джобы не задваивает заказ или уведомление. В день катаута магазин работал без перерыва, заказы шли.

Стек и почему именно он

Next.js 15 (App Router, RSC, Server Actions) - серверный рендеринг каждой карточки, а не только главной. Для SEO это принципиально.
Prisma + PostgreSQL - одна база как источник правды для каталога, заказов, клиентов и контента.
Tailwind + shadcn/ui - быстрая и предсказуемая вёрстка без своего дизайн-зоопарка.
Хостинг: собственный VPS (Next standalone за Nginx, деплой скриптом), изображения на S3 и отдача через CDN.
Sentry с первого дня - чтобы ловить ошибки на живом трафике, а не по жалобам.

Почему self-host, а не Vercel или облачный BaaS: контроль над данными (для РФ-аудитории это в том числе вопрос 152-ФЗ), предсказуемая стоимость и отсутствие привязки к ещё одной платформе. Инфраструктура в итоге выходит около 1000 ₽/мес. Эо меньше, чем стоила подписка платформы с приложениями.

Модули, которые на InSales были платными мы собрали как first-party

Это главная причина переезда. То, за что платформа берёт отдельно или не даёт вообще, теперь часть нашего кода:

1) Серверная корзина живёт на сервере, синхронизируется между устройствами и не теряется при смене девайса.
2) Retention-письма (welcome, брошенная корзина, win-back, «снова в наличии») - своя очередь и cron, с учётом согласий по 152-ФЗ.
3) Подарочные карты, кошелёк и кэшбэк - отдельная сущность транзакций в БД с полным жизненным циклом.
4) Чекаут под РФ - ЮKassa (карта/СБП) с фискальным чеком по 54-ФЗ, СДЭК (курьер, ПВЗ, расчёт тарифа), промокоды.
5) Отзывы с проверкой покупки, подбор размера, блог на собственной CMS, YML-фид для маркетплейсов.

Каждый модуль - это код в репозитории, а не аренда чужой фичи. Нужна новая логика, пишем, а не ждём, разрешит ли её платформа.

Сверх стандартного магазина добавили IRIS - ИИ-стилиста на GigaChat (RU-hosted, 152-ФЗ). Он собирает образ прямо на сайте, отвечает строгим JSON и - важная деталь - валидирует подобранные вещи против реального каталога, поэтому не «галлюцинирует» товарами, которых нет.

Аналитика: написали свою вместо чужих счётчиков

Раз уж данные теперь в нашем контуре, логично и считать их у себя. Сделали две системы.

Бизнес-аналитика (/admin/analytics) на RSC, без внешних сервисов: воронка «корзина → заказ», средний чек, повторные покупки, брошенные корзины в рублях, бестселлеры и неудовлетворённый спрос. Что искали и не нашли in-stock. Это не дашборд ради дашборда, а данные, по которым реально решаем, что закупать и что чинить в воронке.

И отдельно - собственный cookieless-трекинг трафика, мини-Plausible. Маячок на sendBeacon шлёт POST /api/track, а посетитель считается как HMAC(secret, дата | ip | ua) с ротацией соли в полночь. На выходе аналитика по источникам, устройствам и страницам без единого cookie и без персональных данных: согласие по 152-ФЗ не требуется, DNT уважается, боты и /admin исключены. Никакой зависимости от внешнего счётчика и его политики и баннер согласия не блокирует сбор базовой статистики.

SEO: чтобы переезд не уронил трафик

Это была критичная часть. Терять накопленные позиции мы не имели права.

1) Перенесли 7 636 страниц с полным покрытием 301-редиректами, без потери позиций.
2) Актуальный sitemap из БД - 3 748 URL: служебный InSales-бэклог и out-of-stock сознательно не отдаём в индекс, sitemap собирается из базы, а не руками.
3) 530+ 301-редиректов, построенных из реальной аналитики Метрики и GSC, а не «на глаз»: приоритет отдавали URL, которые реально приносили трафик.
4) Серверный рендеринг каждой карточки + Schema.org-разметка (Product, Offer, Breadcrumb, FAQ) на ключевых шаблонах.
5) Динамические OG-изображения.

AEO: чтобы находил не только Google, но и AI

Отдельно заложили готовность к AI-поиску: в 2026 это уже не «на будущее»:

- Динамический llms.txt: бренд-нарратив, доставка, категории и до 24 in-stock товаров из БД плюс гайдлайны для AI (проверять цены, не выдумывать).
- Страница /about-for-ai - человекочитаемый профиль бренда с Organization JSON-LD.
- robots не блокирует AI-краулеры.
- RU/EN с hreflang, причём EN - это полноценно индексируемая локаль, а не noindex-зеркало, как в большинстве проектов.

Про производительность. Честно.

Отдельная история - это mobile-LCP. После катаута первый замер показал 11.8с, и виновником оказался не hero-баннер, а карусель «Это может вас заинтересовать» глубоко под фолдом: она грузила первые фото с fetchPriority=high и отбирала канал у hero-картинки на throttled-4G. Сняли приоритет с внефолдовых блоков, убрали дублирующий preload. Лучшие прогоны упали до 3.2с, CLS на блоге свели с 0.321 к нулю. SEO и Best-practices по Lighthouse - 100, но над стабильностью LCP на холодном кэше работаем дальше. Это процесс, а не «100 из коробки».

Кому такой переезд нужен, а кому нет

Честно: переезжать с платформы стоит не всем.

Оставайтесь на платформе, если она вас не ограничивает: небольшой каталог, стандартный сценарий продаж, нет потребности в своей логике. Своя разработка тогда - это лишние расходы и лишняя ответственность.

Думайте о переезде, если: платформа не даёт нужных модулей (или даёт только платно), вы переросли шаблон, хотите контроль над данными и кодом, или подписка с приложениями превратилась в ощутимую статью расходов.

Мы прошли этот путь на собственном магазине, поэтому считаем не по учебнику, а по своим граблям. Если вы на InSales, Битрикс, Tilda или Shopify и взвешиваете переезд, я разберу ваш случай и покажу, как посчитать, стоит ли оно того.

Об авторе

Я Яков Радченко, founder ETERN8 - бутика индивидуальной веб-разработки. Делаем интернет-магазины, маркетплейсы и бизнес-порталы на Next.js с фиксированной ценой и сроком. Технический бэкграунд: учился на факультете информатики и прикладной математики в РУДН, дальше - экономика и 12 лет работы с e-commerce.

Telegram: @yakov_etern8.

Комментарии (2)


  1. Wiggin2014
    09.06.2026 14:57

    Че-то не понял. 2016-2026 - это 10 лет, ~3000 заказов за 10 лет - примерно 1 заказ в день. Ну или 2, если первые 5 лет не работали. Стоило такое городить ради 2-3 заказов в день?


    1. yakov_etern8 Автор
      09.06.2026 14:57

      Справедливо на первый взгляд, но тут смешались две разные метрики.

      3 123 заказа - это не «оборот в день», а объём перенесённой истории: сколько заказов за всё время жизни магазина именно на Insales мы вытащили и переложили в свою БД без потерь и со связями заказ↔товар↔клиент и сохранёнными статусами.

      Метрика про чистоту миграции, а не про то, как магазин торгует. Смысл переезда тоже не в объёме, а в стоимости владения и контроле. Подписка InSales с нужными платными приложениями выходила в 2 300–10 400 ₽/мес, своя инфраструктура на VPS меньше 1 000 ₽/мес.

      На горизонте года это окупается даже при скромном потоке заказов, а сверху полный контроль над кодом, данными и SEO, которого на платформе нет.

      И историю тащили не ради красивой цифры: на реальных датах и связях сразу заработали win-back-письма и рекомендации: с «чистого листа» они бы не ожили.

      Да, магазин небольшой, и это наш собственный бренд - поэтому миграцию и обкатывали «на себе», без риска для клиента.

      Статья именно про инженерную часть: ETL из UTF-16, 301-редиректы, свой движок - а не про обороты.