П/ВИН

SEO-агрегатор: объединяем два AI-проекта в один

·7 мин чтения

Иногда у тебя есть два работающих прототипа, и ты понимаешь: они делают разные вещи, но по сути решают одну проблему. Именно в такой ситуации я оказался, когда закончил работу над двумя отдельными проектами — генератором бизнес-контента с AI-инструментами и планировщиком SEO-анализа ключевых слов. Оба работали, оба были нужны, но жить порознь им становилось неудобно. Так родилась идея SEO-агрегатора.

Откуда всё началось: два прототипа

Первый проект — v0-prajs-yandeks — я собрал как SaaS-платформу с 9 AI-инструментами для генерации бизнес-контента. Там был генератор объявлений для Авито, скрипты продаж, маркетинговые тексты, HR-документы и GEO-чекер позиций в Яндексе и Google. Платформа работала, интерфейс был чистым, пользоваться ею можно было прямо сейчас. Но у неё было два критичных ограничения: данные хранились in-memory и терялись при каждом рестарте сервера, а GEO-чекер возвращал случайные числа вместо реальных позиций — просто заглушка для демонстрации.

Второй проект — v0-keyword-analysis-plan — был brainstorming-прототипом для анализа ключевых слов. Там была логика работы с Wordstat, идеи кластеризации запросов, планировщик задач для SEO-аудита. По сути — весь инструментарий SEO-специалиста, но без нормального UI и без связки с реальными данными.

Отдельно друг от друга оба проекта были неполными. Вместе — они могли стать чем-то серьёзным.

Что я хотел получить в итоге

Цель была сформулирована так: единая модульная SEO-платформа, где бизнес-пользователь приходит, генерирует контент через AI, тут же проверяет реальные позиции сайта в поисковиках, анализирует ключевые слова и получает рекомендации — всё в одном месте, без переключения между десятками сервисов.

Это не новая идея, но большинство подобных инструментов либо стоят как крыло самолёта, либо сделаны так, что нужно читать мануал неделю. Я хотел сделать что-то, чем можно пользоваться сразу.

Архитектурно план выглядел так:

  • Модуль «Креатор» — весь AI-контент из первого проекта
  • Модуль «SEO-анализ» — ключевые слова, позиции, Wordstat из второго
  • Единая база данных — Supabase с персистентным хранением
  • Реальный GEO-чекер — интеграция с XMLRiver или XMLStock вместо рандома

Стек: Next.js + TypeScript + Tailwind + shadcn/ui + Supabase. Всё знакомое, ничего лишнего.

Планирование: как мы разбили задачу

Первым делом я сделал дизайн-документ и разбил работу на 8 задач:

Задача 1 — Scaffold. Берём Recplace (первый проект) как основу, копируем структуру, переименовываем, убираем всё лишнее. Это быстрее, чем начинать с нуля — UI уже есть, роутинг настроен, компоненты написаны.

Задача 2 — Supabase. Создаём схему seo_aggregator, пишем миграции, подключаем клиент. Таблицы: пользователи, сессии, проекты, ключевые слова, история позиций, шаблоны контента.

Задача 3 — Store → Supabase. Заменяем in-memory хранилище на реальные запросы к БД. Это ключевое изменение — после него данные не теряются.

Задача 4 — SEO-модуль. Переносим весь код из второго проекта в новый раздел платформы, адаптируем под общий стиль UI.

Задача 5 — Реальный GEO-чекер. Убираем заглушку с рандомными числами, подключаем XMLRiver API для получения реальных позиций в Яндексе и Google.

Задача 6 — Sidebar и навигация. Обновляем боковую панель с учётом новых модулей, добавляем ребрендинг.

Задача 7-8 — Деплой и документация. Настраиваем на домен, пишем README.

Параллельный трек: AdminKit для AI-агрегатора

Пока шла работа над SEO-платформой, параллельно возник другой вопрос — административная панель для основного AI-агрегатора, который переехал на базу LobeChat.

Проблема была в том, что LobeChat — это чат-платформа. У неё нет встроенной админки для управления контентом, пользователями и тарифами. А в старом агрегаторе всё это было: дашборд, управление моделями, планами подписки, SEO-блог с настроенной индексацией в Яндексе через IndexNow и кроны для автоматической отправки sitemap.

Возникло несколько вариантов:

  • Отдельный поддомен admin.gptweb.ru
  • Path routing на ask.gptweb.ru/admin
  • Написать с нуля

Выбрали путь B2 — path routing, и решение написать с нуля. Вот почему: старый код был намертво вшит в монорепо со своими абстракциями и собственным DB-слоем. Вытаскивать его и адаптировать заняло бы больше времени, чем написать заново. Логика блога на самом деле простая — CRUD постов, категорий, авторов плюс несколько HTTP-вызовов для IndexNow и Яндекс.Вебмастера.

По базе данных тоже приняли чёткое решение: Supabase убираем из цепочки для adminkit, всё идёт в PostgreSQL LobeChat. Одна БД — меньше сложности. Но блог остаётся в Supabase, потому что лендинг gptweb.ru живёт на другом сервере (VPS #2, Dokploy) и уже подключён — незачем ломать работающую схему.

Технические решения, которые стоит запомнить

In-memory → персистентное хранение

Самая частая ошибка в прототипах — начать с in-memory стора и забыть его заменить. В первом проекте всё состояние жило в React-стейте и серверных переменных. При рестарте — всё сбрасывалось.

// Было: хранение в памяти
let tools: Tool[] = []
let userSessions: Map<string, Session> = new Map()
 
// Стало: Supabase
import { createClient } from '@supabase/supabase-js'
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)
 
export async function getTools() {
  const { data, error } = await supabase
    .from('tools')
    .select('*')
    .order('created_at', { ascending: false })
  if (error) throw error
  return data
}

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

Реальный GEO-чекер вместо заглушки

GEO-чекер позиций — это отдельная история. В прототипе стояла вот такая «реализация»:

// Было: полная фиктивность
function checkPosition(keyword: string, domain: string) {
  return {
    yandex: Math.floor(Math.random() * 100) + 1,
    google: Math.floor(Math.random() * 100) + 1
  }
}

Для демонстрации — нормально. Для реального использования — ноль ценности. Настоящий чекер должен делать запросы к поисковику через API (XMLRiver, XMLStock) или через собственный парсинг с ротацией прокси. Это сложнее, стоит денег, но только так можно получить данные, которым можно доверять.

Кодировки при импорте данных

Отдельный урок пришёл из другого проекта в той же сессии разработки — tg-army. При импорте аккаунтов из архивов lolz.team падала ошибка:

'utf-8' codec can't decode byte 0xc8 in position 99: invalid continuation byte

Байт 0xc8 в cp1251 — это буква «И». Файлы с lolz.team в Windows-1251, Python 3 на Linux читает UTF-8 по умолчанию. Фикс простой, но потерял на нём время:

# Было:
with open(json_file) as f:
    meta = json.load(f)
 
# Стало: автоопределение кодировки
with open(json_file, 'rb') as f:
    raw = f.read()
 
try:
    content = raw.decode('utf-8')
except UnicodeDecodeError:
    content = raw.decode('cp1251')
 
meta = json.loads(content)

Это классический паттерн при работе с файлами из старых русскоязычных сервисов — всегда проверяй кодировку.

Архитектурные принципы, которые работают

После нескольких итераций планирования и разработки оформились несколько правил, которые я теперь применяю по умолчанию.

Правило 1: Одна БД на сервис. Когда появляется соблазн добавить вторую базу данных «для удобства» — почти всегда это сигнал, что нужно пересмотреть архитектуру. Две БД означают синхронизацию, два места для миграций, два набора прав доступа. В нашем случае Supabase осталась только для лендинга, потому что он на другом сервере и там это оправдано. Для adminkit — только LobeChat PostgreSQL.

Правило 2: Прототип → продакшн требует явного решения по стейту. Любой прототип с in-memory хранением должен иметь в README пометку: «данные не персистентны». Когда переходишь к реальному использованию — это первое, что нужно менять.

Правило 3: Объединяй дублирующиеся UI-элементы. В проекте tg-army было два раздела импорта, которые делали одно и то же. Пользователь путался. Решение — объединить в один экран с разными методами загрузки. Меньше навигации, меньше когнитивной нагрузки.

Правило 4: Пиши с нуля, когда старый код — это монолит. Соблазн переиспользовать существующий код велик, но если он намертво вшит в большой контекст — время на вытаскивание и адаптацию превысит время на чистую разработку. Оценивай честно.

Что в итоге

SEO-агрегатор находится в активной разработке. Scaffold проекта сделан, Supabase подключена, Task 1 и Task 2 закрыты. Впереди — перенос SEO-модуля, реальный GEO-чекер и деплой на seotest.pashavin.ru.

Параллельно идёт работа над adminkit для gptweb.ru — с блогом, SEO-индексацией и управлением пользователями.

Оба проекта — хороший пример того, как прототип «на посмотреть» превращается в рабочий инструмент через серию конкретных технических решений. Не революция, а эволюция: берёшь то, что работает, убираешь заглушки, добавляешь персистентность, объединяешь модули.

Если ты сейчас смотришь на два своих прототипа и думаешь «они должны быть одним» — скорее всего, ты прав. Главное — не тащить технический долг из прототипа, а сразу решить архитектурные вопросы: где хранятся данные, как устроена авторизация, что будет единой точкой входа.

Следующая статья будет про реальную реализацию GEO-чекера позиций — это отдельная тема со своими нюансами по работе с поисковыми API.


Полезные ссылки:

Паша Вин
Паша Вин

Фулстек-разработчик, строю SaaS-продукты и автоматизации на Next.js, Python и AI. Пишу о реальных кейсах из продакшена.

Связанный проект

Смотреть в портфолио →