Открываешь аптайм-монитор - всё зелёное, ни одного инцидента за месяц. Открываешь Search Console - трафик по категории падает третью неделю. Причина находится за пять минут: недели две назад кто-то поправил H1 "для красоты", и вместе с ним из тайтла исчез ключевой запрос. Сайт всё это время честно отвечал 200 и был жив в любом мониторинге аптайма просто отвечал не то, что нужно.
Это второй воркфлоу из связки n8n-seo-monitor-suite. Про первый, GEO-мониторинг видимости бренда в ИИ-ответах, я уже писал. Здесь задача звучит скучнее, но по факту не менее полезна: регулярная уверенность в том, что контент на сайте соответствует SEO стратегии. Ссылка на GitHub - в конце статьи.

Один конфиг, два прогона
У аптайма и у контента разная физика отказа. Статус и редирект ломаются мгновенно и требуют частой, дешёвой проверки. Состав каталога, микроразметка, ALT-атрибуты и наличие товара портятся медленно и незаметно - их достаточно перепроверять раз в неделю, но проверка тяжелее: нужно не просто дёрнуть URL, а разобрать HTML.
Отсюда разделение на два прогона с одним источником правды - вкладкой Daily_Monitor: URL, тип (target / redirect), ожидаемые статус, H1, Title, Description, редирект-таргет, ожидаемое число товаров и список обязательных названий. Ежедневный прогон читает её всю, еженедельный только строки с expected_status, содержащим 200, то есть реальные страницы, а не чистые редиректы, у которых и контента-то нет.
Схема пайплайна (ASCII)
[ ВХОД: ЕДИНЫЙ КОНФИГ ] Google Sheets - Daily_Monitor (заполняется вручную) url · type · expected_status/target/h1/title/desc · active expected_products · expected_names │ ┌────────────────┴─────────────────┐ │ все active-строки │ active-строки с expected_status~200 ▼ ▼ ┌────────────────────────┐ ┌──────────────────────────┐ │ ЕЖЕДНЕВНО, 07:00 │ │ ЕЖЕНЕДЕЛЬНО, ПН 09:00 │ │ (9 нод) │ │ (16 нод) │ ├─────────────────────────┤ ├────────────────────────────┤ │ • статус / редирект │ │ • аптайм за 7 дней (из лога)│ │ • noindex │ │ • Я.Вебмастер: индекс, ИКС │ │ • H1 / Title / Desc │ │ • товары: счётчик + список │ │ │ │ • JSON-LD по типу страницы │ │ │ │ • наличие (из той же схемы)│ │ │ │ • ALT в SEO-блоке текста │ └───────────┬──────────────┘ └─────────────┬──────────────┘ │ │ ▼ ▼ Daily_Log (append) Weekly_Log (append)
Обе ветки фетчат HTML не напрямую, а через один и тот же эндпоинт Yandex Cloud Function: воркфлоу отправляет туда URL и получает обратно готовый html_snippet. Что конкретно делает сама функция - рендерит, чистит, кеширует за пределами этого репозитория: со стороны n8n это просто общая точка входа вместо двух независимых HTTP-нод.
Ежедневный прогон: бинарные проверки
Триггера два: расписание в 07:00, либо срочная, внеплановая проверка. Нода Prepare Active Items отфильтровывает неактивные строки и приводит булево active к нормальному виду (в Google Sheets это может прийти и как TRUE, и как true, и как 1):
const isActive = String(row.active).toUpperCase() === 'TRUE' || row.active === true || row.active === 1;
Дальше фетч через прокси с вежливым троттлингом (batchSize: 1, интервал 2500 мс между вызовами) и щедрым таймаутом в 120 секунд, и нода Process & Aggregate Daily, которая на каждую строку присваивает один из трёх уровней: ✅ - совпало, ⚠️ - тег есть, но не совпадает, ❌ - статус не тот, сети нет, noindex, тег отсутствует или редирект ведёт не туда.
// noindex const robots = extractMeta(rawHtml, 'robots').toLowerCase(); if (robots.includes('noindex')) { icon = "❌"; notes.push("[noindex!]"); errorCount++; } // H1 сравнение по вхождению подстроки, не по точному совпадению if (orig.expected_h1) { const actualH1 = extractTag(rawHtml, /<h1[^>]*>([\s\S]*?)<\/h1>/i); if (!actualH1) { icon = "❌"; notes.push("[H1 отсутствует]"); errorCount++; } else if (!actualH1.toLowerCase().includes(orig.expected_h1.toLowerCase())) { icon = "⚠️"; notes.push("[H1 расходится]"); errorCount++; } }
Сравнение по вхождению, а не по точному совпадению это сознательный выбор: тайтл может обрасти сезонной припиской или эмодзи, это не повод поднимать тревогу. А вот если ключевая фраза из H1 пропала целиком - это уже ⚠️ или ❌ в зависимости от того, остался ли тег вообще. Тот же принцип - для Title, Description и для проверки редиректа: сравниваются не URL целиком, а путь без домена, чтобы протокол или www не давали ложных срабатываний.
Отчёт в Telegram - сводка на два предложения, детали во вложенном файле:
? SEO Monitor (Daily scan): Проверено страниц: 27 ⚠️ Найдено предупреждений: 2 Детальный отчёт в сообщение ниже.
Пример полного Markdown-файла с деталями проверкиПример полного Markdown-файла с деталями проверки
# Daily SEO Report **Инициатор:** Daily scan **Дата:** 2026-09-05 ## Page details - ✅ **200** | /collection/example-category - ✅ **301** | /collection/old-slug-redirect - ⚠️ **200** | /collection/mismatched-title [Title не соответсвует] - ❌ **ERR** | /collection/timeout-page [Failed: Timeout] **Всего ошибок:** 2
Initiator - не декоративное поле: воркфлоу определяет, кто запустил проверку, по наличию данных от Telegram-триггера, и подписывает отчёт либо Daily scan, либо Triggered in Telegram (@username). Мелочь, но когда отчёт прилетает не по расписанию, сразу понятно, что это была ручная перепроверка после правки, а не сбой мониторинга.
Еженедельный прогон: контент, а не только доступность
Здесь логика тяжелее, потому что здесь мы проверяем весь контент страницы.
Аптайм из истории, не из измерения. Нода Calculate Uptime не пингует сайт заново, она читает последние записи Daily_Log, сворачивает их в проценты по последним 7 дням и тут же собирает конфиг графика для quickchart.io:
backgroundColor: chartErrors.map(e => e > 0 ? '#e63946' : '#2a9d8f'), // ... const chartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify(chartConfig))}&bkg=%231b1f27&w=800&h=400`;
Зелёный бар - чистый день, красный - были ошибки. Дальше график уходит в Telegram фотографией, а еженедельная сводка едет подписью к ней же - одно сообщение вместо двух.

Три запроса за одной цифрой. Yandex Webmaster API не позволяет спросить сводку по хосту напрямую по домену нужно сначала получить user_id, потом список хостов пользователя, и только через host_id - сводку с числом страниц в поиске и ИКС:
Get User ID → Get Hosts → Get Host Summary (/v4/user/) → (/v4/user/{id}/hosts/) → (/v4/user/{id}/hosts/{host_id}/summary/)
Не самая очевидная особенность API, если готовить интеграцию впервые, но зато один и тот же OAuth-токен работает на всю цепочку, и после первой настройки об этом можно забыть, а в случае компрометации - быстро заменить.
Изоляция каталога перед подсчётом. На странице категории кроме товаров могут висеть виджеты "похожие товары" или "спецпредложения" если считать товары по всему HTML, в счётчик попадёт мусор с чужих блоков. Поэтому перед подсчётом ищется первая граница постороннего виджета или футера, и всё, что после неё, отрезается:
const cutoffRegex = /(widget-type_widget_v4_article_previews|widget-type_widget_v4_special_products_tabs|<footer[^>]*>)/i; const cutoffMatch = rawHtml.match(cutoffRegex); if (cutoffMatch && cutoffMatch.index > 5000) { cleanHtml = rawHtml.substring(0, cutoffMatch.index); }
Порог > 5000 - не минимальная длина страницы, а защита от ложного попадания в шапке: те же классы виджетов теоретически могут встретиться в навигации или хлебных крошках в самом начале HTML, и если резать по первому совпадению без разбора, можно обрезать всю страницу до нуля. Условие явно говорит "не режь, если совпадение в первых ~5000 символах" - для короткой категории это может значить, что отсечение вообще не сработает, но тогда просто ничего не обрежется: лучше не отрезать лишнее, чем отрезать нужное.
Дальше - подсчёт по стабильному CSS-классу карточки товара и сверка с expected_products, плюс отдельная проверка: перечисленные в конфиге "обязательные" названия физически присутствуют в тексте страницы. Это ловит случай, который количество не покажет: число товаров совпало, но приоритетную модель заменили на что-то другое при пересборке категории.
Микроразметка по типу страницы, не одна на всех. У категории, статьи и товара разный обязательный набор JSON-LD-блоков:
Тип URL |
Обязательные схемы |
|---|---|
любой |
|
|
+ |
|
+ |
|
+ |
Наличие - побочный продукт, а не отдельная проверка. Раз ItemList уже распарсен для проверки схемы, из него же вытаскиваются товары с offers.availability, содержащим OutOfStock - отдельного запроса к какой-нибудь фиче-фиду не нужно:
if (item.item?.offers?.availability?.includes('OutOfStock')) { outOfStockItems.push(item.item.name || "Неизвестный товар"); }
Тот же принцип, что и в GEO-воркфлоу с детектом без второй модели: не городить отдельный механизм там, где нужные данные уже лежат в том, что и так пришлось разобрать.
ALT только там, где его трогают руками. Проверка картинок ограничена SEO-текстовым блоком страницы (тем самым <article>/виджетом с описанием категории), а не всей вёрсткой, иначе в отчёт попадали бы счётчик Метрики, иконки и служебные ассеты (что и происходило на тестовых прогонах):
if (img.includes('mc.yandex.ru') || img.includes('served_assets') || img.includes('favicon') || img.includes('data:image')) continue;
Детальный еженедельный аудит (Weekly Markdown)
? WEEKLY SEO REPORT (2026-09-05) • Uptime: 86% • Yandex index: 223 pages | ИКС (site quality index): 140 ⚠️ Найдено проблем: (1 категория): ⚠️ /collection/example-category Детальный отчёт в сообщение ниже.
Почему regex, а не полноценный HTML-парсер
В Code-ноде n8n по умолчанию доступны только встроенные модули Node.js, а сторонние пакеты вроде cheerio нужно явно разрешать переменной окружения на инстансе. Для точечных проверок ("есть ли этот класс", "какое значение у этого meta-тега") регулярка эту работу делает без дополнительной настройки хостинга и без зависимости, которая может сломаться при обновлении. Плата за это ровно та, что и в GEO-статье с regex по имени бренда: подход хрупкий к редизайну. Поменяется класс карточки товара в вёрстке — подсчёт товаров начнёт врать, пока кто-то не заметит нулевые категории в отчёте.
Что проверка реально ловит и как это действует на SEO
Что сломалось |
Код ответа |
В чём скрытая опасность для SEO |
Когда заметят без проверки |
Случайный |
|
Шаблон обновился со служебным тегом. Поисковик молча выкидывает страницу из выдачи. |
Через 1–2 недели, когда трафик в Search Console упадёт до нуля |
Правка H1 / Title «для красоты» |
|
Из заголовков снесли ключевую фразу. Сниппет в выдаче перестраивается, проседает CTR и релевантность. |
Через несколько недель по плавному снижению позиций |
Редирект на нерелевантную страницу |
|
Старый URL категории ведёт на главную вместо нового аналога. Накопленный ссылочный вес обнуляется. |
Спустя месяцы, анализируя потерю веса кластера |
Сломанный или пропавший JSON-LD |
|
Слетел селектор блока. Страница теряет расширенные сниппеты (звёзды, FAQ, цепочки хлебных крошек). |
Случайно при ручном визуальном аудите выдачи |
|
|
Сайт сам сообщает поисковику и фидам, что товар недоступен, ухудшая коммерческий сниппет. |
Когда товарные агрегаторы начнут отклонять фид |
Пустой |
|
Страница незаметно выпадает из выдачи по поиску по картинкам (Google/Яндекс Картинки). |
Скорее всего, никогда |
Ограничения
Хрупкость к вёрстке. Подсчёт товаров и извлечение SEO-блока держатся на конкретных CSS-классах и структуре виджетов конкретной темы InSales. Любой редизайн это повод перепроверить регулярки.
Сравнение по подстроке - компромисс, а не полнота. Ловит пропажу и грубую замену, не ловит смысловую деградацию тайтла, в котором нужная фраза формально осталась, то есть проверка - техническая, не полноценный аудит.
Контентная проверка раз в неделю. Регрессия в JSON-LD или в тексте категории может прожить до 6 дней незамеченной - цена того, что содержательная проверка тяжелее дневной. Поэтому можно настроить получение отчётов раз в 3-5 дней.
Стоимость
Практически ноль. Yandex Webmaster API бесплатный. У Yandex Cloud Functions есть бесплатный лимит: первый миллион вызовов функции и 10 ГБ×час выполнения в месяц не тарифицируются вовсе - прогон на пару десятков страниц раз в день и раз в неделю в этот лимит не упирается никогда. quickchart.io для генерации графика бесплатен в пределах разумного использования. Практически весь потенциальный счёт здесь это сам n8n-инстанс, который и так нужен под остальные воркфлоу связки.
Код
Репозиторий тот же - n8n-seo-monitor-suite. Воркфлоу из этой статьи лежит в daily-weekly/ README с точными форматами обоих отчётов, схемой вкладок Daily_Monitor / Daily_Log / Weekly_Log и списком всех флагов, которые может выставить проверка.