У веб-проекта может быть отличный SEO-генератор.
Он автоматически создаёт:
title
description
canonical
H1
JSON-LD
sitemap.xml
robots.txtВ административной панели всё выглядит правильно.
Разработчик открывает несколько страниц:
Title есть.
Sitemap открывается.
robots.txt на месте.
Значит, SEO готово.
Через месяц выясняется, что часть новых страниц содержит canonical на другой домен.
Ещё несколько URL возвращают красивую страницу «Не найдено» с HTTP 200.
После очередного релиза sitemap.xml неожиданно начал включать redirect-страницы.
На PDF появился неправильный X-Robots-Tag.
А генератор структурированных данных стал добавлять AggregateRating, хотя никаких реальных отзывов на странице нет.
Каждый отдельный дефект кажется небольшим.
Но поисковый робот не смотрит на сайт глазами разработчика.
Он получает:
HTTP status
headers
HTML
canonical
robots directives
structured data
sitemap
redirectsи строит представление о сайте именно из этих технических сигналов.
Поэтому в проектах с автоматически создаваемыми SEO-страницами мы предпочитаем относиться к SEO не как к набору настроек в <head>, а как к исполняемому контракту.
Если генератор может создать SEO-страницу программно, значит его результат можно программно проверить.
Если sitemap создаётся кодом, можно тестировать sitemap.
Если приложение определяет canonical автоматически, canonical можно проверять тестом.
Если сервер должен вернуть 404, тест должен запросить настоящий URL и убедиться, что пришёл именно 404.
Именно это мы называем подходом:
SEO as Code — SEO как код.
Почему проверки глазами перестают работать
Для сайта из пяти страниц ручная проверка ещё возможна.
Разработчик открывает:
/
/services
/about
/contacts
/blogсмотрит <head> и заканчивает работу.
Теперь представим редакционную систему или программно создаваемые SEO-страницы.
Есть:
100 статей
40 страниц услуг
региональные страницы
кейсы
категории
пагинация
архивЧасть URL появляется автоматически.
SEO-генератор строит metadata по шаблонам.
Sitemap собирается динамически.
Теперь вручную проверить каждый URL практически невозможно.
Главная проблема даже не в количестве.
Проблема в повторяемости.
Сегодня всё правильно.
Завтра разработчик меняет общий SEO helper.
Одна строка:
canonical = baseUrl + pathname;становится:
canonical = request.url;Теперь tracking-параметр из:
/article?utm_source=directслучайно начинает попадать в canonical.
Одна небольшая правка затронула сотни страниц.
Это обычная regression.
Значит, и ловить её нужно как обычную regression.
SEO можно представить набором инвариантов
Допустим публичная индексируемая статья должна удовлетворять следующим условиям:
HTTP status = 200
Content-Type = text/html
title существует
description существует
H1 существует
canonical существует
canonical использует HTTPS
canonical относится к production-домену
canonical не содержит tracking-параметров
robots не содержит noindex
JSON-LD синтаксически корректен
страница присутствует в sitemapА несуществующая статья:
HTTP status = 404
не попадает в sitemapСтарый URL после миграции:
HTTP status = 301
Location = новый URLПриватный PDF:
X-Robots-Tag = noindexЭто уже не абстрактное:
SEO вроде настроено.
Это набор условий, которые компьютер способен проверить автоматически.
Начинать лучше с модели SEO-страницы
Предположим генератор получает:
{
slug: 'razrabotka-crm',
title: 'Разработка CRM для бизнеса',
description: '...',
h1: 'Разработка CRM',
indexable: true
}и строит:
HTML
canonical
JSON-LD
sitemap entryОчень полезно считать всё это одним SEO-контрактом.
Потому что одна сущность появляется сразу в нескольких местах.
Например URL:
https://example.ru/services/crmдолжен совпасть:
в canonical
в sitemap
в JSON-LD
во внутренних ссылках
в redirect rulesЕсли проверять каждый слой отдельно, можно получить четыре правильных файла, которые противоречат друг другу.
Поэтому нужны не только unit-тесты, но и cross-check
Представим:
<link
rel="canonical"
href="https://example.ru/expert/article-a"
/>А sitemap содержит:
<loc>
https://example.ru/expert/article-a?ref=catalog
</loc>Каждая конструкция сама по себе валидна.
Но вместе они создают конфликтующие сигналы.
Google рассматривает redirects, sitemap и rel="canonical" как сигналы при выборе канонической версии страницы, а указанный владельцем canonical остаётся сигналом, а не безусловной командой. Поэтому особенно полезно делать эти сигналы согласованными.
Тест может проверить:
sitemap URL
===
page canonicalдля каждой индексируемой страницы.
Уровень №1: тестируем сам SEO-генератор
Самый дешёвый тест выполняется ещё без запуска HTTP-сервера.
Например функция:
generateSeo(page)должна возвращать:
{
title,
description,
canonical,
robots,
jsonLd
}Можно проверить:
const seo = generateSeo(article);
expect(seo.title).toBeTruthy();
expect(seo.description).toBeTruthy();
expect(seo.canonical)
.toBe(
'https://example.ru/expert/seo-kak-kod'
);
expect(seo.canonical)
.not.toContain('utm_');Такой тест выполняется за миллисекунды.
Если кто-то случайно меняет алгоритм canonical, CI обнаруживает проблему до deployment.
Но тестировать только наличие недостаточно
Плохая проверка:
expect(title).toBeTruthy();пропустит:
<title>{{SEO_TITLE}}</title>или:
<title>undefined</title>Поэтому useful assertions могут проверять ещё и запрещённые значения:
expect(title).not.toMatch(
/undefined|null|\{\{|TODO/i
);То же самое касается description, canonical и structured data.
Мы не превращаем длину TITLE в «закон поисковой системы»
Это важный момент.
В SEO часто встречаются магические правила:
Title должен быть ровно N символов.
Description — строго M.
Поисковая система может формировать отображение результата самостоятельно, а title/snippet не являются фиксированными полями с гарантированной визуальной длиной.
Но внутри конкретной редакционной системы вполне можно установить собственные редакционные ограничения.
Например:
title не пустой;
title не состоит из одного слова;
нет технических шаблонов;
нет бессмысленного повторения названия;Это уже quality contract проекта, а не попытка угадать алгоритм Google.
Уровень №2: тестируем готовый HTML
SEO-генератор может возвращать правильные данные.
Но template может вывести их неправильно.
Например генератор создал:
Разработка CRMа HTML содержит сразу два:
<h1>Разработка CRM</h1>
...
<h1>Разработка CRM</h1>Такое легко появляется после изменения layout.
Поэтому следующий слой тестов работает уже с итоговым HTML.
Пример проверки H1
В нашем проектном контракте можно принять:
у индексируемой статьи
ровно один основной H1.Важно: это именно архитектурное правило конкретного шаблона, а не утверждение, что поисковая система технически запрещает несколько <h1>.
Тест:
const h1 = document.querySelectorAll('h1');
expect(h1.length).toBe(1);
expect(h1[0].textContent.trim())
.toBe(article.h1);Так ловится не только отсутствие заголовка.
Но и дубль после случайного изменения шаблона.
То же самое делаем с canonical
Например:
const canonical =
document.querySelector(
'link[rel="canonical"]'
);
expect(canonical).not.toBeNull();
expect(canonical.href)
.toBe(expectedCanonical);Дополнительно:
один canonical;
absolute HTTPS URL;
production host;
нет fragment;
нет tracking params.Очень полезно проверять canonical на самого себя
Для обычной уникальной статьи:
GET /expert/article-aожидаем:
canonical =
https://example.ru/expert/article-aА не:
/expertи не:
http://localhost:3000/expert/article-aПоследняя ошибка особенно легко проходит локальное тестирование, если production base URL подставляется неправильно.
JSON-LD нужно проверять как данные
Визуально пользователь JSON-LD обычно вообще не видит.
Именно поэтому ошибки в нём легко переживают несколько релизов.
Например:
<script type="application/ld+json">
{
"@type": "Article",
"headline": "...",
}
</script>Лишняя запятая — JSON уже невалиден.
Поэтому самый первый тест:
const schema =
JSON.parse(jsonLdText);Если parsing падает, release должен падать вместе с ним.
Но синтаксически правильного JSON недостаточно
Можно получить:
{
"@type": "Article",
"headline": "Разработка CRM",
"url": "https://wrong-domain.example/article"
}JSON валиден.
SEO-смысл — нет.
Поэтому мы проверяем семантику.
Например:
expect(schema['@type'])
.toBe('Article');
expect(schema.headline)
.toBe(visibleH1);
expect(schema.url)
.toBe(canonical);Google рекомендует проверять structured data специальным Rich Results Test и после deployment смотреть страницу через URL Inspection; это полезный внешний слой проверки, но внутри CI имеет смысл дополнительно тестировать собственный JSON-LD и его соответствие реальному контенту.
Особенно важно проверять выдуманные рейтинги
Очень соблазнительный SEO-шаблон:
{
"aggregateRating": {
"ratingValue": "5",
"reviewCount": "127"
}
}Выглядит красиво.
Но откуда взялись:
127 отзывов?Если их нет, разметка описывает не существующий на странице факт.
Google прямо запрещает fake reviews в review structured data и требует, чтобы отмеченный отзывами/рейтингом контент действительно был доступен пользователю на самой странице. Для Organization/LocalBusiness существуют также отдельные ограничения на self-serving reviews.
Поэтому в SEO-тестах полезно иметь отрицательное правило.
Например:
expect(schema.aggregateRating)
.toBeUndefined();для страниц, у которых нет настоящей системы отзывов.
Ещё лучше связать rating с реальными данными
Если когда-нибудь в продукте появятся настоящие отзывы:
reviewCountдолжен происходить из реального источника.
Например:
if (verifiedReviews.length > 0) {
generateAggregateRating();
}Тест может проверить:
JSON-LD reviewCount
===
количество реальных опубликованных отзывовИ одновременно:
rating виден пользователю на странице.Так structured data перестаёт быть SEO-декорацией и становится отражением настоящих данных продукта.
Уровень №3: sitemap тестируется как индекс страниц
sitemap.xml — особенно хороший кандидат на автоматические проверки.
Yandex требует, чтобы Sitemap был доступен с HTTP 200, указывает лимит в 50 000 URL на файл и рекомендует включать canonical URL соответствующих страниц.
Но просто проверить:
/sitemap.xml открываетсянедостаточно.
Мы хотим проверить каждый URL
Для каждого:
<loc>...</loc>можно выполнить запрос.
Ожидаем:
200а затем проверить:
страница indexable;
canonical совпадает;
нет noindex;
URL не redirect;
Content-Type соответствует странице.Получается:
sitemap
↓
URL #1 → PASS
URL #2 → PASS
URL #3 → 301 → FAIL
URL #4 → 404 → FAILЭто намного сильнее проверки XML syntax.
Sitemap не должен становиться кладбищем старых URL
Представим статья была:
/expert/old-slugпосле переименования:
/expert/new-slugСтарый URL правильно возвращает:
301
→ /expert/new-slugНо generator продолжает добавлять оба адреса в sitemap.
Теперь sitemap сам предлагает поисковому роботу URL, который немедленно перенаправляет его дальше.
Проектный тест может установить простое правило:
каждый URL sitemap должен непосредственно возвращать конечный 200, а не redirect.
То же касается 404
Если статья удалена:
/expert/deleted-articleеё больше не должно быть в sitemap.
Это можно проверить автоматически при каждой сборке.
Уровень №4: robots.txt тоже является кодом
Очень опасно относиться к:
robots.txtкак к статическому файлу, который один раз написал разработчик несколько лет назад.
Особенно когда проект имеет:
production;
staging;
development.Для staging может быть:
Disallow: /Для production — совсем другие правила.
Одна ошибка environment способна отправить production в релиз со staging robots.
Значит robots.txt должен иметь тест
Например production:
expect(robots)
.toContain('Sitemap: https://example.ru/sitemap.xml');
expect(robots)
.not.toContain('Disallow: /');Если проект требует закрытые технические paths:
expect(robots)
.toContain('Disallow: /admin/');Но robots.txt и noindex — не одно и то же
Это особенно важный тестовый сценарий.
robots.txt управляет crawling.
noindex управляет индексированием страницы после того, как crawler смог увидеть соответствующую директиву.
Google отдельно указывает: robots meta и X-Robots-Tag могут учитываться только тогда, когда crawler имеет доступ к ресурсу. То есть схема:
Disallow: /private-pageи одновременно:
<meta name="robots" content="noindex">не должна автоматически восприниматься как гарантированный способ передать Google noindex, потому что crawler может не получить саму страницу и не увидеть эту директиву.
SEO-tests полезно использовать и для поиска таких противоречивых правил.
Clean-param можно тестировать отдельно для Яндекса
Допустим страница:
/catalog?ref=partner-aи:
/catalog?ref=partner-bпоказывает одинаковый контент.
Параметр нужен аналитике, но не меняет страницу.
Yandex поддерживает директиву:
Clean-paramименно для указания параметров, которые не влияют на содержание URL. Это помогает роботу объединять такие варианты и не тратить лишнее сканирование на дубли.
Например:
User-agent: Yandex
Clean-param: refНо Clean-param нельзя генерировать вслепую
Пусть есть:
?sort=priceЕсли параметр действительно меняет содержание страницы, объявить его «грязным» только потому, что он query parameter, будет ошибкой.
Поэтому generator должен иметь явную классификацию:
utm_source
→ tracking
ref
→ tracking
category
→ content-changing
page
→ content-changingТест проверяет только ожидаемые параметры.
Yandex также указывает, что некоторые распространённые analytics-параметры может удалять автоматически, однако собственные правила Clean-param всё равно должны соответствовать фактической логике конкретного сайта.
Особенно полезен тест самого robots.txt через HTTP
Статический unit-test может показать:
robots string valid.Но Nginx в production способен сделать что-то другое.
Например:
GET /robots.txt
→ 302 /loginили:
Content-Type: text/htmlпотому что запрос попал в SPA fallback.
Yandex прямо рекомендует проверять реальный server response для robots.txt и указывает, что файл должен находиться в корне сайта; для штатного файла ожидается HTTP 200.
Поэтому нужен следующий уровень.
Уровень №5: тестируем настоящий HTTP, а не только код
Это один из самых важных принципов всего подхода.
Unit tests видят:
что хотел сделать разработчик.HTTP-tests видят:
что реально получает поисковый робот.Между ними находятся:
Nginx
routing
SSR
cache
redirect rules
headers
compression
CDNИ каждый слой способен изменить результат.
Например приложение правильно возвращает 404
Внутри Node.js:
res.status(404);Но Nginx настроен:
error_page 404 /index.html;и в результате:
HTTP/1.1 200 OKплюс красивая страница:
Страница не найденаПользователь визуально понимает ошибку.
Поисковый робот получает 200.
Мы создали классический кандидат на soft 404.
Google отдельно рекомендует отдавать 404 или 410 для действительно удалённых URL и указывает, что soft 404 продолжает расходовать crawling resources.
Поэтому тест должен смотреть статус, а не текст страницы
Например:
const response =
await fetch(
`${baseUrl}/definitely-not-existing-page`,
{ redirect: 'manual' }
);
expect(response.status)
.toBe(404);Вот это SEO-test.
Не:
На экране написано «404».
То же самое с redirect
Есть старый URL:
/uslugi/crmНовый:
/services/crmОжидаем:
301
Location: /services/crmТест:
const response = await fetch(oldUrl, {
redirect: 'manual'
});
expect(response.status).toBe(301);
expect(response.headers.get('location'))
.toBe('/services/crm');Google рекомендует при переносе URL настраивать server-side redirects со старых адресов на новые; постоянные redirects являются одним из сигналов смены URL.
И стоит проверять отсутствие цепочек
Плохо:
/old
↓ 301
/older
↓ 301
/newЛучше:
/old
↓ 301
/newДаже если цепочка технически работает, она добавляет лишние переходы и усложняет crawling.
Тест может пройти redirect вручную и поставить ограничение:
redirect hops <= 1для известных миграционных URL.
MIME тоже является частью HTTP-контракта
Допустим:
/sitemap.xmlвозвращает XML.
Но из-за SPA fallback сервер отвечает:
Content-Type: text/htmlа внутри вообще находится index.html.
Если проверять только:
статус 200,
ошибка не обнаружится.
Поэтому мы проверяем ещё и представление ресурса.
Например:
HTML pages
→ text/html
robots.txt
→ plain text
sitemap.xml
→ XML response
JSON endpoints
→ application/jsonЭто не попытка придумать новый ranking factor.
Это обычная проверка, что сервер действительно выдаёт тот тип ресурса, который мы ожидаем.
Google требует, чтобы robots.txt был текстовым UTF-8-файлом; если crawler вместо него получает HTML, он пытается извлечь допустимые robots-правила и игнорирует остальное, что явно не является хорошей конфигурацией production.
X-Robots-Tag тоже нужно проверять через HTTP
Особенно для файлов, где нет HTML <head>.
Например PDF:
/private/report.pdfне должен появляться в поиске.
Для HTML можно использовать:
<meta name="robots" content="noindex">Но для PDF Google поддерживает HTTP:
X-Robots-Tag: noindexи прямо рекомендует этот механизм для non-HTML ресурсов.
Значит тест выглядит просто
const response =
await fetch(pdfUrl);
expect(
response.headers.get('x-robots-tag')
).toContain('noindex');Это особенно полезно после изменения Nginx.
Потому что приложение может вообще не участвовать в выдаче PDF.
SEO regression иногда находится только на уровне Nginx
Например до релиза:
.pdf
→ X-Robots-Tag: noindexПосле рефакторинга location block исчез.
Код приложения не менялся.
Все unit tests приложения зелёные.
Но SEO-поведение production изменилось.
Именно поэтому нужны реальные HTTP-tests.
Можно выделить четыре слоя SEO-тестирования
Мы используем примерно такую модель:
SEO Generator Tests
↓
Rendered HTML Tests
↓
Cross-artifact Tests
↓
Real HTTP TestsПервый отвечает:
Правильно ли работает логика генератора?
Второй:
Правильно ли она оказалась в HTML?
Третий:
Не противоречат ли друг другу HTML, sitemap и robots?
Четвёртый:
Что фактически отдаёт production-server?
Например одна статья проходит весь путь
Есть:
/expert/seo-kak-kodНа уровне generator проверяем:
title
description
canonical
JSON-LDНа HTML:
title присутствует
H1 ровно один
canonical один
JSON-LD parseableНа cross-check:
canonical === sitemap URL
JSON-LD URL === canonical
headline === visible H1На HTTP:
200
text/html
нет X-Robots-Tag: noindexТеперь уверенность значительно выше.
Особенно сильный тест — пройти весь sitemap
Это уже почти SEO-интеграционный тест.
Алгоритм:
GET /sitemap.xml
↓
Parse XML
↓
For each URL
↓
HTTP request
↓
Check status
↓
Parse HTML
↓
Check canonical
↓
Check robotsЕсли sitemap небольшой, можно выполнять это перед каждым production release.
Если URL десятки тысяч — проверять весь набор реже, а в CI использовать детерминированную выборку плюс отдельную полную проверку.
Можно проверять отсутствие неожиданных внешних canonical
Например:
expect(
new URL(canonical).host
).toBe('example.ru');Это поймает очень неприятную ошибку:
canonical:
https://staging.example.ru/...или вообще canonical на чужой host из ошибочной настройки environment.
Полезно тестировать production hostname отдельно
Потому что локальный snapshot:
localhostничего не скажет о переменной:
PUBLIC_BASE_URLв настоящем deployment.
HTTP smoke-test после запуска способен проверить именно production-конфигурацию.
Страница 200 ещё не обязательно индексируемая
Например:
200 OKно HTML содержит:
<meta
name="robots"
content="noindex"
/>Или HTTP:
X-Robots-Tag: noindexДля публичной SEO-страницы это regression.
Поэтому интеграционный тест должен проверять все три слоя:
status
meta robots
X-Robots-TagИ наоборот: приватная страница не должна случайно стать indexable
Например:
/admin
/account
/client/project/1842тесты могут утверждать:
не присутствует в sitemapи проверять нужную indexing policy проекта.
Это уже SEO-security boundary.
Мы явно знаем, какие URL предназначены для поиска, а какие — нет.
Очень полезно хранить единый реестр типов страниц
Например:
{
type: 'expert_article',
indexable: true,
inSitemap: true,
requiresCanonical: true,
structuredData: 'Article'
}И:
{
type: 'admin',
indexable: false,
inSitemap: false,
structuredData: null
}Теперь SEO-политика не размазана по шаблонам.
Она стала конфигурацией.
Тесты генерируются из той же политики
Например:
expert_articleавтоматически получает проверки:
200
canonical
title
description
H1
Article JSON-LD
sitemapА:
adminполучает совсем другой contract.
Это особенно мощно для генераторов страниц.
Но тесты не должны генерироваться из того же бага
Есть тонкая проблема.
Если один алгоритм создаёт canonical и тот же самый алгоритм вычисляет expected canonical в тесте, ошибка может повториться с двух сторон.
Например:
const expected =
buildCanonical(page);и production тоже использует:
buildCanonical(page);Если функция неправильная, тест всё равно зелёный.
Поэтому критичные ожидания полезно фиксировать независимо
Например fixture:
{
path: '/expert/test-article',
canonical:
'https://example.ru/expert/test-article'
}Теперь test oracle не зависит от генератора.
Это обычный принцип качественного тестирования, но для SEO он особенно важен.
Snapshot tests полезны, но недостаточны
Можно сохранить весь <head> как snapshot.
После изменения:
snapshot changedразработчик увидит diff.
Это удобно.
Но snapshot легко обновить:
Update snapshotsи вместе с ним принять ошибочный canonical.
Поэтому для критичных SEO-свойств нужны semantic assertions.
Например:
canonical host === production hostили:
sitemap never contains noindex pagesТакие условия нельзя бездумно «обновить».
Особенно интересны property-based проверки
Представим генератор создаёт тысячи SEO URL.
Можно проверить общие свойства:
slug никогда не пустой;
canonical всегда HTTPS;
canonical никогда не содержит // после host;
нет двух страниц с одинаковым canonical;
нет двух sitemap entries с одинаковым loc.Это сильнее проверки нескольких вручную выбранных примеров.
Уникальность canonical можно проверить глобально
Например:
const canonicals =
pages.map(buildCanonical);
expect(
new Set(canonicals).size
).toBe(canonicals.length);Если две разные страницы неожиданно указывают на один canonical, CI останавливает релиз.
То же самое с slug
Можно обнаружить collision:
/articles/apiсоздали две сущности.
CMS может позволить это на уровне данных.
SEO-тест должен поймать конфликт ещё до публикации.
Metadata тоже стоит проверять глобально
Не потому, что два одинаковых TITLE автоматически являются поисковым нарушением.
А потому что в редакционной системе массовый дубль часто означает ошибку шаблона.
Например после релиза 200 статей получили:
<title>
Практика разработки — WebRuta
</title>вместо собственных title.
Тест:
доля одинаковых TITLEили полный uniqueness-contract для конкретного типа страниц быстро обнаружит проблему.
Ошибки SEO часто являются массовыми
В этом их особенность.
Ошибка в одной статье:
1 page affected.Ошибка общего template:
500 pages affected.Поэтому автоматическое тестирование SEO особенно выгодно именно в динамических системах.
Один тест защищает сотни будущих URL.
Например тест JSON-LD защищает не одну статью
Если общий генератор Article schema перестал выводить:
datePublishedили начал вставлять:
undefinedручная проверка одной публикации может это пропустить.
Test suite ловит regression при каждом изменении.
Проверяем реальные HTTP status всех специальных ресурсов
Отдельный набор smoke-tests можно сделать для:
/
/robots.txt
/sitemap.xml
/known-page
/old-redirect
/non-existent-page
/private-file.pdfОжидания:
/ → 200
/robots.txt → 200
/sitemap.xml → 200
/known-page → 200
/old-redirect → 301
/non-existent-page → 404Уже такой небольшой набор ловит удивительно много production-дефектов.
Почему реальные HTTP-тесты особенно важны после deployment
Потому что SEO behaviour создаёт не один компонент.
Например:
Applicationсоздаёт HTML.
Nginxсоздаёт redirect и headers.
CDNможет кешировать старую версию.
Service Workerвлияет на пользовательский frontend.
Environmentопределяет production hostname.
Только реальный запрос проходит почти тот же путь, что и crawler.
Production smoke test можно сделать частью updater
Например:
Deploy
↓
/api/ready
↓
SEO HTTP tests
↓
Application smoke tests
↓
Release PASSЕсли:
/sitemap.xml → 500release не считается успешным.
Если:
/old-url → 200вместо 301, release получает fail.
Если:
canonical → staging domaindeployment останавливается или выполняется rollback.
SEO стало quality gate.
Это совсем другой подход по сравнению с проверкой после релиза
Старая модель:
Release
↓
ждём
↓
смотрим Webmaster/Search Console
↓
через несколько дней замечаем ошибкуНовая:
Change
↓
Tests
↓
Deploy
↓
HTTP verification
↓
только после PASS release принятПоисковый робот больше не является первым автоматизированным тестировщиком вашего SEO.
Search Console и Яндекс Вебмастер всё равно нужны
Автотесты не заменяют поисковые системы.
Они не могут гарантировать:
индексацию;
позицию;
выбор Google canonical;
rich result.Даже canonical является для Google сигналом, а итоговый URL поисковая система выбирает сама.
Search Console и Яндекс Вебмастер нужны для проверки фактического поведения crawler и index.
Но их роль меняется.
Мы используем поисковые консоли для внешнего наблюдения
А CI — для предотвращения технических regression.
Условно:
Tests:
«Мы выдаём правильные сигналы?»Search Console / Webmaster:
«Как поисковая система их восприняла?»Это два разных уровня.
Нельзя написать тест «страница будет в топ-10»
SEO-тестирование имеет пределы.
Мы можем проверить:
canonical;
status;
metadata;
structured data;
robots;
sitemap.Но нельзя написать:
expect(page.rank).toBeLessThan(10);и считать это unit-test.
Поисковая выдача зависит от огромного количества внешних факторов.
SEO as Code относится прежде всего к технической корректности и согласованности сигналов.
Контент тоже нельзя полностью свести к unit-тестам
Тест способен обнаружить:
пустой H1;но не всегда:
Этот заголовок действительно полезен читателю?
Он обнаружит:
description отсутствует;но не определит надёжно:
Это хорошее описание?
Поэтому automated tests дополняют редакционную проверку.
Не заменяют её.
Зато машины великолепно проверяют то, что люди постоянно пропускают
Например:
не тот статус;
не тот header;
сломанный JSON;
дубль canonical;
URL в sitemap ведёт на redirect;
staging hostname;
noindex на production;
неправильный MIME;
fake AggregateRating;
два H1 по ошибке шаблона;Именно сюда стоит направлять автоматизацию.
Как может выглядеть SEO test suite проекта
Например:
seo/
├── metadata.test
├── canonical.test
├── headings.test
├── structured-data.test
├── sitemap.test
├── robots.test
├── redirects.test
├── status-codes.test
├── headers.test
└── production-http.testЭто уже полноценная subsystem.
Не пара случайных assertions.
Для редакционной системы можно тестировать публикацию целиком
Например создаём fixture-статью:
SEO как кодПубликуем в test environment.
После этого автоматически проверяем:
public route существует;
200;
TITLE правильный;
description правильный;
один H1;
canonical правильный;
Article JSON-LD;
нет fake rating;
есть в sitemap;
не заблокирована robots;Затем удаляем fixture.
Это уже настоящий E2E SEO-test.
Отдельно стоит проверить удаление материала
Статья существовала:
/expert/article-aПосле удаления policy проекта говорит:
404или:
301 → replacement articleТест должен проверить именно выбранную политику.
Не позволить приложению случайно вернуть:
200 + empty template.Пагинация тоже заслуживает отдельного набора тестов
Например:
/expert
/expert?page=2
/expert?page=3Нужно заранее определить:
какие страницы indexable;
какой canonical;
какие URL попадают в sitemap;
что происходит с page=9999.После этого правила превращаются в assertions.
Особенно полезно тестировать:
?page=9999чтобы пустая пагинация не генерировала бесконечное количество indexable 200 страниц.
То же самое с query-параметрами
Например:
/expert?sort=date
/expert?utm_source=...
/expert?filter=...Для каждого класса параметров должна существовать политика:
меняет контент?
indexable?
canonical куда?
Clean-param нужен?Если этой политики нет, генератор URL постепенно создаёт огромное пространство технических дублей.
SEO-tests становятся особенно важными при programmatic SEO
Представим generator создаёт:
1000 региональных страниц.Ручной просмотр всех URL невозможен.
Поэтому до публикации проверяется набор инвариантов.
Например:
нет пустого контента;
slug уникален;
canonical уникален;
H1 соответствует региону;
metadata не содержит placeholder;
JSON-LD соответствует странице;
URL присутствует в sitemap.После этого выборочная ручная проверка оценивает уже смысл и качество контента.
Тесты могут ловить thin-template ошибки, но не оценивать полезность полностью
Например generator случайно создаёт:
Разработка CRM в Москве
Разработка CRM в Казани
Разработка CRM в Омскеа тело всех трёх страниц полностью одинаковое.
Автоматически можно найти:
extreme text similarityили одинаковый content hash.
Но окончательный вопрос:
Есть ли реальная самостоятельная ценность у каждой страницы?
остаётся редакционным.
Таким образом SEO pipeline имеет два разных gate
TECHNICAL SEO GATEпроверяет машину.
CONTENT QUALITY GATEпроверяет смысл.
Это намного полезнее попытки заставить один AI-score решать всё.
Fake rating test — хороший пример такого разделения
Машина отлично способна проверить:
Нет review data
→ не должно быть AggregateRating.Но машина не должна придумывать:
ratingValue = 4.9«для SEO».
Structured data должно описывать реальный контент, а не желаемый вид поискового сниппета. Google отдельно предупреждает, что нарушение structured-data policies может привести к manual action.
Самый полезный regression test иногда занимает пять строк
Например когда-то произошёл дефект:
после публикации TITLE
повторялся внутри H1Исправили.
Теперь обязательно оставляем тест.
Не просто:
Мы это починили.
А:
Этот класс ошибки
больше не должен вернуться.Так production-инцидент превращается в permanent knowledge проекта.
Это основной смысл regression suite
Каждый найденный SEO-дефект должен по возможности закончиться двумя изменениями:
1. Fix
2. Test preventing recurrenceБез второго через полгода другой refactoring способен вернуть ту же проблему.
SEO-generator особенно хорошо подходит к такому подходу
Потому что generator — это детерминированный код.
На входе:
page dataНа выходе:
SEO artifacts.Если правила формализуемы, они тестируемы.
Практический production checklist для SEO as Code
Перед принятием релиза мы бы автоматически проверяли хотя бы следующее:
- публичные SEO-страницы возвращают реальный
200, а отсутствующие — настоящий404, а не soft 404; - известные перенесённые URL возвращают ожидаемый
301непосредственно на конечный адрес; - у индексируемых HTML-страниц присутствуют валидные
title, description и проектный основной H1; - canonical существует, уникален там, где это требуется, использует production HTTPS-host и не содержит случайных tracking-параметров;
- canonical, URL в sitemap и URL в JSON-LD согласованы между собой;
- JSON-LD парсится и описывает фактический контент страницы;
AggregateRatingи review-разметка не создаются без реальных отзывов и отображаемых пользователю данных;- индексируемые страницы не получают
noindexчерез meta robots илиX-Robots-Tag; - приватные non-HTML ресурсы получают нужный
X-Robots-Tag, если это предусмотрено политикой проекта; robots.txtреально доступен в production, содержит правильный sitemap и не наследует stagingDisallow: /;- правила
Clean-param, если они используются, соответствуют только параметрам, не меняющим реальный контент; sitemap.xmlвозвращает200, корректно разбирается как sitemap и содержит только допустимые canonical URL;- каждый sitemap URL можно получить напрямую без redirect и 404;
- специальные ресурсы имеют ожидаемое представление/MIME, а SPA fallback не подменяет robots, sitemap и error pages HTML-страницей;
- staging/test domains никогда не появляются в production canonical, sitemap или structured data;
- после deployment выполняется отдельный HTTP smoke-test настоящего production-домена.
Если такой suite выполняется перед каждым release, значительная часть технических SEO-ошибок перестаёт зависеть от внимательности разработчика.
Что происходит при ошибке
Например CI сообщает:
SEO CONTRACT FAILED
URL:
/expert/api-integration
Expected canonical:
https://example.ru/expert/api-integration
Actual:
https://staging.example.ru/expert/api-integrationТеперь release просто не проходит.
Это значительно лучше сценария:
релиз
↓
Google crawl
↓
индексация
↓
через неделю замечаем проблему
↓
новый релиз
↓
ждём повторного обходаТо же самое для sitemap
SITEMAP FAILED
URL:
/expert/old-article
HTTP:
301
Expected:
200 canonical pageРазработчик понимает проблему до публикации sitemap поисковым системам.
И для structured data
STRUCTURED DATA FAILED
Page:
/services/crm
aggregateRating exists
Verified reviews:
0Такой тест особенно полезен, потому что визуально страница может выглядеть совершенно нормально.
SEO становится частью Definition of Done
Раньше задача:
Добавить новую публичную страницу.
считалась завершённой, если:
дизайн готов;
API работает;
mobile работает.В системе с SEO as Code Definition of Done расширяется:
route;
metadata;
canonical;
indexing policy;
structured data;
sitemap;
HTTP behaviour;
tests.То есть SEO перестаёт быть финальной процедурой:
Перед запуском попросим специалиста посмотреть.
Оно становится свойством самой функции.
Это особенно полезно при работе нескольких разработчиков
Не каждый frontend-разработчик обязан помнить десятки SEO-правил.
Он меняет общий layout.
Test suite говорит:
Duplicate H1Backend-разработчик меняет routing.
Tests:
Expected 404
Actual 200DevOps меняет Nginx.
Tests:
X-Robots-Tag missingПравило находится не только в голове SEO-специалиста.
Оно находится в репозитории.
Именно поэтому SEO как код масштабируется лучше ручной инструкции
Можно написать документ:
Не забывать проверять canonical после каждого релиза.
Через три месяца кто-нибудь забудет.
А тест:
canonical must use production hostне устает и не торопится домой в пятницу вечером.
При этом документация всё равно нужна
Тест говорит:
FAILДокументация объясняет:
Почему это правило существует?
Например:
Sitemap содержит только конечные canonical URL,
потому что redirect URL не являются
целевыми страницами для индексации.Так архитектурное решение остаётся понятным будущему разработчику.
Хороший SEO-test должен объяснять ошибку
Плохо:
SEO test failed.Хорошо:
SEO_CANONICAL_HOST_MISMATCH
Path:
/expert/article
Expected host:
example.ru
Received:
localhost:4173Или:
SEO_SITEMAP_NON_200
URL:
/expert/deleted
Status:
404Такие ошибки легко исправлять.
Метрики можно вывести и в админ-панель
Например:
SEO pages:
184
Canonical conflicts:
0
Sitemap URLs:
184
Broken sitemap URLs:
0
Structured-data errors:
0
Last production SEO check:
PASSАдминистратор видит не только:
Sitemap существует.
А состояние всей SEO-подсистемы.
Но CI должен оставаться главным gate
Админ-панель показывает состояние после запуска.
CI предотвращает плохой запуск.
Это разные роли.
Можно добавить scheduled production audit
Даже если release не происходил.
Например раз в сутки проверять:
robots
sitemap
несколько критичных pages
redirects
headers.Почему?
Потому что SEO может измениться не только из-за приложения.
Например:
Nginx configuration;
CDN;
DNS;
external proxy;
CMS data.Regression может появиться между releases.
Особенно полезно мониторить sitemap
Например scheduled test видит:
sitemap URLs:
183 → 0Это явно подозрительно.
Или:
404 in sitemap:
0 → 27Можно поднять alert раньше, чем проблема станет заметной в поисковых консолях.
Search engine validators остаются внешней контрольной точкой
После deployment полезно выборочно проверять structured data через инструменты Google и следить за Search Console.
Yandex предоставляет собственные инструменты анализа robots.txt и sitemap; документация рекомендует валидировать эти файлы и проверять их доступность для робота.
Но ручной validator хорошо работает как дополнительная проверка.
Он не заменяет regression suite в репозитории.
В итоге SEO становится похожим на любой другой production-контур
Для платежей мы не говорим:
После релиза глазами посмотрим, не создалось ли два платежа.
Мы пишем тест идемпотентности.
Для прав доступа:
Наверное, клиент не увидит чужой проект.
Мы пишем negative authorization test.
Для backup:
Файл вроде создаётся.
Мы делаем restore-test.
И для SEO логика должна быть той же.
Не:
Откроем исходный код страницы после релиза и посмотрим.
А:
Что является инвариантом?
↓
Как это проверить автоматически?
↓
На каком слое ошибка может возникнуть?
↓
Как остановить релиз до production?Вместо вывода
Техническое SEO веб-продукта состоит не только из:
TITLE;
description;
keywords;
H1.В реальной системе поисковый робот видит целую цепочку:
DNS
↓
HTTP
↓
redirect
↓
status code
↓
headers
↓
robots
↓
HTML
↓
canonical
↓
structured data
↓
sitemapЕсли один слой противоречит другому, визуально идеальная страница может передавать поисковой системе совсем не те сигналы, которые ожидал разработчик.
Поэтому для динамического сайта особенно полезно перестать воспринимать SEO как набор ручных действий после разработки.
Если SEO-страницы создаются программно — тестируем generator.
Если canonical создаётся программно — проверяем canonical.
Если сайт строит sitemap — тестируем каждый его URL.
Если сервер должен выполнить 301 — проверяем настоящий HTTP response.
Если несуществующая страница должна быть 404 — не доверяем тексту «Страница не найдена», а смотрим status code.
Если structured data заявляет rating — проверяем, существуют ли реальные отзывы.
Если PDF не должен индексироваться — проверяем X-Robots-Tag.
Если для Яндекса используются параметры, не меняющие содержание, — контролируем Clean-param.
Если всё это работает локально — всё равно делаем короткий HTTP smoke-test после deployment.
В результате процесс меняется:
SEO requirements
↓
Code
↓
Automated tests
↓
Build
↓
Deploy
↓
Real HTTP tests
↓
Production
↓
Search Console / Webmaster monitoringИ поисковый робот перестаёт быть первым, кто сообщает нам об ошибке.
Это и есть главный смысл SEO as Code:
правила, влияющие на индексирование сайта, должны быть такими же проверяемыми и воспроизводимыми, как API, права доступа, платежи или резервное копирование.
Тогда SEO перестаёт быть финальным чек-листом перед публикацией.
Оно становится частью архитектуры самого веб-продукта.