SEO-агрегатор: объединяем два AI-проекта в один
Иногда у тебя есть два работающих прототипа, и ты понимаешь: они делают разные вещи, но по сути решают одну проблему. Именно в такой ситуации я оказался, когда закончил работу над двумя отдельными проектами — генератором бизнес-контента с 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. Пишу о реальных кейсах из продакшена.
Связанный проект
Смотреть в портфолио →