SEO и контент

Как тестировать SEO как код, а не проверять глазами после релиза

У веб-проекта может быть отличный 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 → 500

release не считается успешным.

Если:

/old-url → 200

вместо 301, release получает fail.

Если:

canonical → staging domain

deployment останавливается или выполняется 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 и не наследует staging Disallow: /;
  • правила 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 H1

Backend-разработчик меняет routing.

Tests:

Expected 404
Actual 200

DevOps меняет 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 перестаёт быть финальным чек-листом перед публикацией.

Оно становится частью архитектуры самого веб-продукта.

Есть похожая задача?

Опишите продукт, интеграции и ограничения. До разработки зафиксируем объём, риски и критерии приёмки.

Обсудить проект →