П/ВИН

ИИ для генерации контента: автопилот SEO-блога ветклиники

·9 мин чтения

Сайт, который сам ведёт свой блог без единого человека, звучит как маркетинговая сказка - но именно такую систему я собрал для ветклиники likedog.ru. Это ии для генерации контента, который сам собирает поисковую семантику, сам пишет под неё статьи, сам отслеживает, какие темы дают трафик, и сам усиливает выигрышные кластеры, пока я только смотрю в дашборд. Началось всё, как водится, с простой проверки «а работает ли вообще наша автоматизация».

С чего всё началось: ревизия, которая вскрыла пустоту

Задача звучала буднично: «братишка, проверь нашу автоматизацию по блогу — что всё пишется верно и вовремя, что новости тоже выходят, что семантика набирается и используется, что мы не пишем статьи про VPN (иначе РКН может прилететь), и что выигрышные кластеры усиливаются». То есть предполагалось, что система уже есть и просто надо убедиться в её здоровье после недельного простоя.

Я полез проверять — и не нашёл ничего. В репозитории likedog лежали только разовые миграционные скрипты (migrate-from-fixture, seed-specialists, import-pricing). В systemd был один-единственный юнит likedog-preview.service — это сам Next.js-сервер сайта, а вовсе не контент-автоматика. В crontab — ни строчки про likedog. Полноценный SEO-пайплайн с таймерами, Wordstat и кластерами действительно существовал, но жил он в соседнем проекте gptweb.ru, а likedog была просто открытой рядом папкой, которая всех сбила с толку.

Вывод оказался отрезвляющим: автоматизации, которую просили «проверить», на этом проекте никогда не было. И тогда задача переобулась на лету — не проверять, а построить. «Прочитай KNOWLEDGE проекта gptweb, особенно инструкцию посева, и сделай так же».

Контекст: что такое likedog и почему ему нужен автопилот

likedog.ru — это сайт ветеринарной клиники: страницы услуг, специалисты, прайс, посадочные по районам города и блог. На момент старта в базе уже лежало 355 живых статей и 6 новостей — наследие ручной миграции с фикстур. Сайт сделан как «pixel-perfect 1:1 миграция», то есть фронт трогать нельзя — любое движение должно происходить на уровне данных.

Проблема классическая для коммерческого сайта: органический поисковый трафик не растёт сам по себе. Чтобы клиника попадала в выдачу по запросам вроде «чем кормить щенка», «почему кошка чихает» или «вакцинация собак цена», нужен постоянный поток качественного, заточенного под семантику контента. Писать его руками — дорого и медленно. А на gptweb уже был отлаженный движок, который делает это автономно. Логичный ход — портировать его.

Исходник: что представлял собой пайплайн gptweb

Инструкция по посеву на gptweb оказалась документом на 43 КБ. За ней стояла внушительная машина:

  • около 13 bash-скриптов (~2900 строк) — сбор ключей, построение кластеров, генерация статей, SEO-аудит, реоптимизация, трекинг позиций;
  • отдельная схема в PostgreSQL под весь блог;
  • набор systemd-таймеров на каждый этап;
  • интеграции: Wordstat через xmlriver для частотности, Yandex Webmaster API для позиций, Claude CLI для написания текста;
  • SEO-гейт (черновик → аудит → публикация), кластеры, реоптимизация и — важная деталь — guard, отсекающий запрещённую тематику.

Ключевой нюанс архитектуры gptweb: вся логика ингеста ключей и сохранения статей жила в TypeScript-приложении (webgpt-admin), а bash-скрипты просто POST-или в его API-роуты. Для likedog я сознательно решил пойти другим путём — без единой строчки нового Next.js-кода. Всё общение с базой — напрямую.

Решение: семь ключевых развилок

Прежде чем копировать скрипты, я прогнал дизайн через серию архитектурных решений. Слепой порт «один в один» сломал бы likedog, потому что инфраструктура и тематика другие.

1. Куда складывать статьи. На gptweb статья проходит цикл draft → SEO-аудит → published, для чего нужен статус, preview-роут и фильтрация на фронте. У likedog все статьи «живые», статуса нет. Решение: генерируем сразу в таблицу article как опубликованные, SEO-аудит делаем информативным (он не блокирует выход), preview-роут не заводим. Это радикально упростило фронт — новая строка в таблице просто появляется в списке и в sitemap.

2. Каденс публикаций. gptweb пишет 8 статей в день. Для ветклиники это путь к «контент-ферме» и быстрому выжиганию качественной семантики. Снизил до 2–3 статей в день — вет-тематика вечнозелёная, спешить некуда.

3. Новости. Подключил полноценный путь через сервис агрегации новостей (agent-news-007), заведя для likedog отдельный вет-профиль: интересны ветеринария, болезни и уход за питомцами, ветпрепараты, породы, зоозаконы РФ; исключены политика, не-животная медицина и крипто-шум.

4. Защита от РКН. Это прямо упоминалось в исходной задаче. На gptweb был VPN-guard под свою специфику; на likedog я заменил его на vet-guard.sh — фильтр мусора и нерелевантной тематики, который заодно гарантирует, что система не генерирует статьи про VPN и обход блокировок. Для коммерческого сайта попадание в такую тематику — прямой риск блокировки со стороны РКН, поэтому фильтр стоит на входе семантики, а не на выходе.

5. Доступ к базе. В CLAUDE.md была заметка, что PostgREST не всегда видит кастомные схемы. Проверил — и оказалось, что заметка устарела: REST для схемы likedog работает, PGRST_DB_SCHEMAS её уже включает. Значит, доступ к данным идёт через PostgREST с заголовком Accept-Profile, как на gptweb, только профиль меняется с ai_aggregator на likedog. Порт стал почти механическим.

6. Формат контента. Деталка статьи на likedog рендерит поле full_description как HTML через dangerouslySetInnerHTML. Значит, генератор должен отдавать HTML, а не Markdown — иначе на странице будут видны решётки и звёздочки.

7. Сортировка ленты. Список /articles сортировался по id ASC — то есть новые статьи уезжали бы в конец пагинации, где их никто не увидит. Переключил на newest-first, чтобы свежий контент попадал на первую страницу.

Реализация: от схемы до 4000 ключей

Шаг 1. Миграция базы

Первым делом — новые таблицы в схеме likedog, зеркало структуры gptweb, но адаптированное под то, что пост у likedog — это строка в article. Важная деталь всплыла из «ground truth» живой базы: likedog.article.id имеет тип bigint (у gptweb был uuid), поэтому все внешние ключи post_id / used_in_post_id пришлось делать bigint.

-- очередь семантики
create table likedog.blog_keywords (
  id bigserial primary key,
  keyword text not null,
  status text default 'pending',      -- pending / used / skipped
  priority int default 0,
  impressions int default 0,
  category_slug text,
  cluster_id bigint references likedog.blog_clusters(id),
  used_in_post_id bigint references likedog.article(id),
  source text
);
 
-- тематические кластеры
create table likedog.blog_clusters (
  id bigserial primary key,
  primary_keyword text not null,
  related_keywords text[],
  category_slug text,
  status text default 'active',
  total_impressions int default 0
);

Миграция применилась чисто: 5 таблиц + 7 категорий + колонка article.cluster_id. Фундамент готов.

Шаг 2. Общие библиотеки

Написал lib/slot-parity.sh — распределение публикаций по трём слотам в день с биасом на свежее покрытие (пока трафика нет, важнее охватить как можно больше тем). И lib/vet-guard.sh — фильтр мусора без VPN-логики. Фильтр намеренно сделал узким: одиночный латинский мусор отсекаем, а мульти-токенные латинские бренды (названия препаратов и кормов) пропускаем, чтобы не резать легитимную семантику.

Шаг 3. Доступ к данным через REST

Вместо нового API-роута — прямая работа с PostgREST. Меняется только профиль схемы:

curl -s "$SUPABASE_URL/rest/v1/blog_keywords?status=eq.pending" \
  -H "apikey: $SERVICE_KEY" \
  -H "Accept-Profile: likedog"

Проверка прошла — запрос вернул засеянные категории, значит REST на новых таблицах работает.

Шаг 4. Посев семантики и первый баг

Запустил seed-vet-keywords.sh: 30 вет-сидов прогоняются через Wordstat, отфильтровываются guard'ом и вставляются в blog_keywords. Первый прогон упал на вставке — классика: инлайновый curl -d с огромным JSON-телом превысил лимит длины командной строки.

Before (падает на больших объёмах):

curl -X POST "$URL/rest/v1/blog_keywords" -d "$BIG_JSON_PAYLOAD"

After (пишем тело в файл, передаём ссылкой):

printf '%s' "$BIG_JSON_PAYLOAD" > /tmp/kw_payload.json
curl -X POST "$URL/rest/v1/blog_keywords" -d @/tmp/kw_payload.json

После фикса посев отработал и собрал 3985 ключей в статусе pending. Вет-семантики хватит надолго. Кстати, был забавный момент: мой фоновый «ждун» сообщал, что процесс «всё ещё работает», хотя он давно закончился — pgrep -f seed-vet-keywords.sh находил сам ждун, потому что имя скрипта было в его собственной командной строке. Ложное срабатывание, не баг.

Шаг 5. Таймеры и трекинг

Развернул systemd-таймеры на каждый этап — сбор ключей, генерацию, трекинг позиций, реоптимизацию. systemd.timer удобен тем, что переживает перезагрузки и не требует внешнего планировщика.

# likedog-blog-keywords.timer — сбор семантики по утрам
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
 
[Install]
WantedBy=timers.target

По урокам прошлых инцидентов (LLM в таймере без guard'а однажды сжёг бюджет за ночь) каждый скрипт, дёргающий платный API, делает exit 0 до первого вызова, если работы нет.

Для трекинга проверил доступы: токен Яндекс.Метрики работает для счётчика likedog, а в Yandex Webmaster домен likedog.ru уже верифицирован в том же аккаунте — host_id получен, трекинг трафика заведётся сразу. track-positions.sh адаптировал под пути /articles/, счётчик likedog и схему likedog, попутно выкинув VPN-логику. Он же выступает продюсером расширения кластеров: темы, которые начинают приносить показы, порождают новые связанные ключи.

Результат

Что получилось на момент сдачи фундамента:

  • Схема likedog: 5 новых таблиц (blog_keywords, blog_clusters, blog_positions, reoptimize_queue и связки) + 7 категорий + article.cluster_id.
  • 3985 ключей реальной вет-семантики, собранной из Wordstat и отфильтрованной guard'ом, лежат в очереди готовыми к использованию.
  • systemd-таймеры на сбор ключей, генерацию, трекинг и реоптимизацию — запущены, с cost-guard'ом на каждом платном вызове.
  • Vet-guard на входе семантики гарантирует, что система не пишет про VPN/обход блокировок — ровно та защита от РКН, ради которой всё затевалось.
  • Новости через вет-профиль в agent-news-007.
  • Трекинг через Метрику и Webmaster подключён и проверен.
  • Фронту правок почти не понадобилось: лента и sitemap читают article напрямую, новые строки появляются сами.

Генерация статей запущена и выкатывается поэтапно — сначала фундамент и семантика, затем end-to-end-генерация и реоптимизация (последняя всё равно не имеет смысла без накопленных данных по трафику). Это сознательный staged rollout, а не недоделка: сначала доказываем срез на данных, потом включаем дорогие LLM-этапы.

Выводы

Первый урок — всегда проверяй, что система вообще существует, прежде чем «проверять её здоровье». Задача пришла как ревизия работающей автоматики, а на деле автоматики не было вовсе — её перепутали с соседним проектом из-за открытой рядом папки. Полчаса разведки сэкономили дни ложных гипотез. Если бы я доверился формулировке задачи и начал «чинить» несуществующее, то искал бы причину поломки там, где ломаться нечему.

Второй урок — порт это не копипаста, а перевод. Соблазн «сделай так же» — взять 2900 строк и прогнать через sed. Но между gptweb и likedog лежала пропасть: uuid против bigint, API-роуты против прямого REST, Markdown против HTML, draft-цикл против живых статей, VPN-guard против вет-guard. Каждое из этих различий, пропущенное при слепом копировании, превратилось бы в продакшн-баг. Семь архитектурных решений до первой строки кода сделали последующий порт действительно механическим — потому что вся неопределённость была снята заранее.

Третий урок — устаревшая документация опаснее отсутствующей. Заметка в CLAUDE.md уверяла, что PostgREST не видит кастомные схемы, и я мог бы на этом основании городить лишний слой API. Пятиминутная проверка живой конфигурации показала, что ограничение давно снято. Доверяй, но проверяй каждое инфраструктурное допущение фактом из живой системы, а не строкой в доке полугодовой давности.

Четвёртый урок — автоматизация, которая тратит деньги, обязана иметь предохранитель. LLM в systemd-таймере — это заряженное ружьё: один цикл без early-exit, когда работы нет, и ты платишь за пустые вызовы всю ночь. Поэтому архитектура с самого начала закладывает exit 0 до первого платного вызова, staged rollout дорогих этапов и явный подсчёт стоимости одного прогона. Самодвижущийся контент-движок хорош ровно настолько, насколько хорошо он умеет останавливаться, когда делать нечего.

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

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.