П/ВИН

Сайт ветклиники likedog: 780 услуг и 28 специалистов

·9 мин чтения

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

Контекст: ветклиника, 365 статей и провал в трафике

likedog.ru — это сайт ветеринарной клиники в Москве (Сивцев Вражек, 18 — это ЦАО, Хамовники, очень дорогой и плотно конкурентный район). На сайте уже был блог: 365 статей про породы собак и кошек, написанных по шаблону a-la «Сибирский хаски — это порода собак, которая была выведена в Сибири». Контент был, индексация была, но трафик после Q1 просел: летела SSL/DNS-миграция на Hetzner, и Яндекс с Google какое-то время просто не могли нормально достучаться до сайта.

Когда мы вернулись к работе, у владельца был заготовлен план: «давай переписывать статьи, делать их человечнее». Я честно сказал, что это даст marginal эффект, не transformational. Главное починилось само — миграция отдала боту HTTP 200, и трафик начнёт восстанавливаться по мере перекраулинга. А вот настоящие точки роста для ветклиники в ЦАО — это коммерческие и локальные страницы: «вызов ветеринара на дом СВАО», «круглосуточный ветеринар», «стерилизация кошки цена». Конверсия в звонок там в разы выше, чем в блоге про хаски.

В итоге решили делать оба фронта параллельно: editorial pass на 365 статей идёт фоном (с почти нулевой ценой через Claude CLI на subscription), а основные силы — на новый раздел /uslugi/.

Editorial pass: 365 статей через Claude CLI с concurrency=8

Первая задача — нужно было прогнать все 365 статей через Claude и получить более живые тексты. Скрипт scripts/edit-articles.ts уже был, но работал последовательно — три статьи в пилоте заняли заметное время. На 365 это было бы вечно.

Добавил пул воркеров. Сначала smoke-test на 2 статьях с concurrency=2: wall-time 218 секунд, CPU-time 384 секунды — параллелизм даёт ~1.76x на двух воркерах. Никаких race conditions, идемпотентность по seo_edited_at отрабатывает. После этого запустил на все 365 с concurrency=8 в фоне.

Хранение оригинала — в колонке full_description_legacy, чтобы можно было откатить любой текст до исходника. Дельта вошла в миграцию 0003_article_seo_edit.sql. К моменту начала работы над /uslugi/ фоновый процесс уже отрапортовал 195/365 готово, из них 191 OK и 4 timeout-fail на самых длинных статьях — их потом перезапустили с флагом --rerun --slug .... Стоимость прогона — буквально $0, потому что Claude CLI ходит по подписке Max, а не по API-токенам.

Разведка конкурентов: два параллельных агента вместо одного

Заказчик прислал ссылку на mobil-veterinar.ru как пример «топового сайта в Яндексе по семантике вызова на дом». Я сделал то, что обычно стоит делать в таких случаях, — диспатчнул двух subagent'ов параллельно вместо того, чтобы прожигать собственный контекст на сырой HTML.

  • Агент №1: подробный разбор mobil-veterinar.ru — структура URL, sitemap, перелинковка, FAQ-блоки.
  • Агент №2: парсинг органической выдачи Яндекса по ключам «вызов ветеринара на дом», «ветклиника круглосуточно Москва» — топ-3 конкурентов с разбором их структуры.

Получил отчёт по четырём сильным игрокам — svoydoctor.ru (~2600 URL в sitemap, 20 филиалов = 20 локальных лендингов, 661 статья), doctor-vet.ru (174 URL, плотная воронка «дом / 24ч / усыпление»), vetclinika.moscow и сам mobil-veterinar.ru. Главные паттерны: явные посадочные под «вызов на дом», отдельная страница под эвтаназию, локальные лендинги по районам, и — самое важное — реальные цены прямо на странице услуги. Это сильный сигнал и для пользователя (сразу понятно, попадаешь ли в бюджет), и для Яндекса (карточки услуг с микроразметкой Schema.org Service индексируются в товарных колдунщиках).

Реальный прайс: 780 строк, 28 категорий по специалистам

Дальше выяснилось, что у клиники есть свой XLS-прайс на 780 позиций в 28 категориях. И вот тут возникла критичная архитектурная развилка. Существующая БД (service) была организована по 15 «зонтикам» (Хирургия, Дерматология, Кардиология...), а реальный прайс — по 28 специалистам (Терапевт 115 услуг, Хирург 112, Офтальмолог 69, Стоматолог 48, УЗИ 28...). Это две разные сетки, и они не маппятся 1:1.

Выбрали стратегию: не ломать существующий /services/, а сделать новый раздел /uslugi/[specialist-slug]/ параллельно. Старые страницы остаются (живые ссылки, индексация, перелинковка), новые — закрывают семантику по специалистам и подуслугам с реальными ценами.

-- 0004_pricing_items.sql
CREATE TABLE pricing_item (
  id            BIGSERIAL PRIMARY KEY,
  specialist_id BIGINT REFERENCES specialist(id),
  name          TEXT NOT NULL,
  price_rub     INT NOT NULL,
  unit          TEXT,
  notes         TEXT,
  source_row    INT,
  created_at    TIMESTAMPTZ DEFAULT now(),
  updated_at    TIMESTAMPTZ DEFAULT now()
);
 
CREATE INDEX pricing_item_specialist_idx ON pricing_item(specialist_id);

Импорт XLS — через библиотеку xlsx (она же SheetJS), один раз пробежался по листам, разнёс по specialist + pricing_item, и закоммитил в репо.

Реконсилиация цен: не трогаем рутину, поднимаем хирургию

Прайс был от июля 2024, и сравнение с медианой рынка ЦАО 2026 года показало интересную картину:

Услугаlikedog 2024Рынок ЦАО 2026Разрыв
Стерилизация кошки~3 500 ₽7 500 ₽×2
Кастрация кота~2 000 ₽4 000 ₽×2
Стерилизация суки 10–25 кг~5 000–6 000 ₽13 000 ₽×2
Первичный приём терапевта~900 ₽1 800 ₽×2

Искушение «обновим всё» здесь сильное, но я отговорил. Если поднять цены на рутину (приёмы, вакцинация) — клиника потеряет позиционирование «доступная клиника в центре», а это часть её SEO-бренда. Поэтому выбрали whitelist-подход: обновляются только хирургия и эвтаназия (12 правил), всё остальное остаётся как есть. Реализовали это отдельным скриптом reconcile-pricing.ts, который применяет правила идемпотентно — можно перезапустить любое количество раз.

Архитектура страниц /uslugi/[slug]/ и SEO-обвязка

Шаблон страницы специалиста собран из шести компонентов:

  1. Заголовок + краткое intro (генерируется через Claude CLI, кэшируется в specialist.intro).
  2. Таблица услуг с ценами (тянется из pricing_item join specialist).
  3. FAQ-блок (3–5 вопросов, тоже генерируется один раз через CLI).
  4. JSON-LD микроразметка MedicalBusiness + Service + FAQPage — компонент JsonLd.tsx уже был, расширили под новые типы.
  5. Перелинковка с /services/ (зонтичные категории) и блогом.
  6. Sticky-CTA «Записаться» + телефон.

И да — для срочной семантики («ветеринар на дом», «круглосуточно», «эвтаназия») мы сделали отдельный B3-блок: три urgent-funnel landing'а с расширенным FAQ, Schema.org EmergencyService и формой быстрого вызова. Их я выделил в отдельный спек именно потому, что воронка там другая — пользователь приходит «прямо сейчас», и страница должна закрывать его за один экран.

Ещё один блок (B4) — карточки врачей: 8 страниц /vrachi/<slug>/ с фото, специализациями и расписанием. Это классический E-E-A-T сигнал для медицинской ниши: Google и Яндекс обоим очень нравится, когда у статьи / услуги есть конкретный автор-эксперт.

Баг, который сожрал час: import-time vs runtime env

Когда я писал верификационный скрипт verify-tables.ts, он стабильно падал с тем, что SUPABASE_URL пустой, хотя .env.local лежит на месте. Полез смотреть. Вот что нашёл:

// scripts/verify-tables.ts — НЕ работает
import { supabaseAdmin } from "@/lib/supabase"; // <- здесь читается process.env
import { config } from "dotenv";
config({ path: ".env.local" }); // <- слишком поздно

Дело в том, что lib/supabase.ts делает createClient(process.env.SUPABASE_URL, ...) на верхнем уровне модуля. Когда Node резолвит import, он выполняет тело модуля до того, как мой собственный код успевает позвать dotenv.config(). Поэтому в момент создания клиента переменных ещё нет.

Фикс простой — либо подгружать dotenv через --require dotenv/config, либо делать свой локальный createClient прямо в скрипте (как сделано в edit-articles.ts):

// рабочий вариант
import { config } from "dotenv";
config({ path: ".env.local" });
import { createClient } from "@supabase/supabase-js";
 
const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  { db: { schema: "public" } }
);

Выглядит банально, но я вижу эту ошибку в каждом втором проекте на Next.js с самописными CLI-скриптами. Основной workflow Next-приложения подхватывает env через next dev / next build, поэтому проблема не вылезает в обычной разработке — а вот любой tsx scripts/... ломается.

Результат и выводы

Что получилось по итогам работы:

  • 365 статей прогнаны через editorial pass (~95% успешно, остальные перезапущены отдельно), стоимость — $0 благодаря Claude CLI на подписке.
  • 28 страниц специалистов /uslugi/[slug]/ с реальными ценами, FAQ, JSON-LD и перелинковкой — закрывают всю семантику по подуслугам.
  • 780 позиций прайса в нормализованной таблице pricing_item с возможностью идемпотентной реконсилиации.
  • 3 urgent-funnel landing'а под срочную семантику с EmergencyService разметкой.
  • 8 карточек врачей под E-E-A-T.
  • Sitemap, навигация, cross-link обновлены — Яндекс и Google видят новый раздел сразу после деплоя.

Главный урок этой истории — для меня лично — про то, что важно говорить заказчику «не делай этого» так же часто, как «давай сделаем». Если бы я молча взялся переписывать 365 статей, мы бы потратили дни на marginal-улучшение и оставили на столе главный шанс — коммерческие посадочные. Когда заказчик пришёл с готовым решением «давай редачить тексты», правильный ход — поставить под сомнение само решение, а не только его реализацию. SEO-эффект от хорошей страницы услуги с прайсом и FAQ для ветклиники в ЦАО на порядок больше, чем от литературной полировки статьи про сибирского хаски.

Второй урок — про параллельность работы. Editorial pass на 365 статей шёл фоном 2.5 часа, и всё это время я не сидел и не ждал — я работал над дизайном /uslugi/, разведкой конкурентов через subagent'ов, миграциями. Background jobs — это не просто способ ускориться, это способ не блокировать собственное мышление. Каждый раз, когда я ловлю себя на «надо подождать пока скрипт отработает», стоит задаться вопросом «а что я могу делать прямо сейчас». Обычно — много чего.

Третий урок — про две разные онтологии в одной системе. У likedog была сетка «зонтиков» (15 категорий) и сетка «специалистов» (28 категорий из реального прайса). Соблазн «давайте сольём в одну» был сильный, но цена ошибки — поломанные ссылки, потеря индексации, недели работы поверх. Решение «оставить старое, сделать новое параллельно» иногда выглядит как технический долг, но на деле это самый честный путь, когда у двух систем разные источники истины и разные пользователи. Старый /services/ обслуживает зонтичную семантику, новый /uslugi/ — детальную; и они спокойно сосуществуют в одной Postgres базе с двумя таблицами и одним общим JsonLd.tsx.

И последнее — про дисциплину разделения спека и плана. Я честно использовал skill brainstorming → writing-plans → subagent-driven-development, и это сэкономило кучу времени на перерисовках. Спек на 233 строки, план на 1616 строк, 15 задач + pre-flight, каждая задача с явным acceptance critera. Когда после Task 1 я уткнулся в неожиданность — что в прайсе нет позиции «стерилизация кошки», потому что хирургия там организована по «категориям сложности 1–4» — я не запаниковал и не побежал переделывать всё, а спокойно остановился, скорректировал спек и продолжил. Это и есть та самая «дисциплина», ради которой существуют все эти superpowers-skills. Без них на таком объёме (28 страниц специалистов, 780 позиций прайса, 3 funnel'а, 8 врачей) я бы утонул в third-order правках за первый же день.

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

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

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