Предыстория

Я занимаюсь созданием сайтов с начала 00-х. Я просто интересовался этой темой. Веб‑дизайн и разработка сайтов никогда не были моей профессией. Я заканчивал институт, работал в ночных клубах танцором и сделал для себя сайт‑визитку: это было намного проще, чем раздавать агентам фотографии сценических образов в конвертике. Позже на этом сайте появились другие артисты, а самим сайтом начинали потихоньку пользоваться ивент‑агентства. Позднее с годами я уже вёл несколько сайтов дружеских коллективов, а мой первый маленький сайт превратился в каталог артистов с самостоятельной регистрацией и возможностью за плату продвигать свою анкету.

Как и писал ранее — сайты — это моё хобби. Иногда я прибегал к помощи сторонних веб‑дизайнеров, что‑то делал сам. Такое поверхностное занятие сайтами с годами привело к тому, что сайты устарели на десятилетие в плане дизайна и UX. В этом году в бизнесе артистов для корпоративов наступило затишье и появилось много свободного времени. Было принято решение обновить сайты!

Скриншот главной страницы сайта headinspect.ru по состоянию на август 2026 года.
Скриншот главной страницы сайта headinspect.ru по состоянию на август 2026 года.

Почему неудобно пользоваться бесплатными сервисами постранично

За пару месяцев я уже обновил несколько своих сайтов с точки зрения дизайна, что‑то подверглось даже переделке в плане структуры сайтов. Но, когда сайт уже готов, начинается техническая работа, которая видна поисковым роботам, а не человеку. К SEO с их robots.txt, sitemap, крошками и meta‑тегами в последние годы добавились ещё Open Graph, Schema.org и даже экспериментальные файлы типа llms.txt.

Верстать красивые картинки, подбирать шрифты, бесконечно доводить всё до идеала — моё любимое занятие. Но доводить до идеала внутрянку SEO — нет. Мои сайты (за исключением пары крупных) обычно имеют количество страниц от 1 до 100. Но даже такое количество проверить — стало проблемой.

Я покопался в сети, но абсолютное большинство сайтов иностранные и проблема с ними даже не английский язык — с этим проблем нет — а то, что они платные. Я за такое не осуждаю, но платить сейчас на иностранных сайтах неудобно, да и цены скорее для профессионалов, которые штампуют сайты на заказ. Плагины в CMS есть, но они тоже либо с ограниченным бесплатным функционалом, либо от сайта к сайту и от версии к версии сильно различаются. А часть моих сайтов на стареньких WordPress. Ещё есть инструменты в самих Яндекс Вебмастер и Google Search Console, но они мне показались также не очень удобными.

В итоге я выбрал для себя несколько сайтов, которые работают как надо:

  • Rich Results Test от Google

  • OpenGraph.xyz

  • Involta.ru

Involta отдельный случай. Не знаю, кто сделал этот сайт, но на нём есть действительно удобные инструменты для теста сайта, облегчившие мою работу, но тоже постраничные. В любом случае шлю благодарность создателям!

Неделями я пользовался этими тремя сайтами, проверяя страницы своих сайтов. И... я устал. Постоянно где‑то что‑то забывается, где‑то og:image не того размера, а где‑то meta‑description больше 160 символов. Вести учёт страниц в Эксель, ставить галочки «сделано» — очень муторно. В итоге мне впервые захотелось сделать сайт не по своей тематике: сайт, который бы залпом проверял все страницы сайта.

При этом важно было потратить как можно меньше денег на этот проект. Для сайта был выбран вариант размещения на бесплатном GitHub Pages. А с доменом мне просто повезло. Оказалось, что никто на планете ещё не зарегистрировал HeadInspect.ru. От такой радости я даже не поскупился и приобрёл ещё и.com. Пусть будет.

Где берутся все страницы сайта

По задумке сайт должен проверять всё без моей помощи. Главное — ввести только домен и один раз, и чтобы результаты были на экране в едином удобном виде. Причём не только на десктопе, но и на смартфоне. Для создания такого сервиса понадобился бэкенд. Им стал самый дешевый VPS у российского провайдера.

После введения домена, сайт самостоятельно обращается к robots.txt, берёт в нём sitemap и пробегается по всем ссылкам, сканируя внутрянку и сортируя всё это в удобочитаемые результаты. Если robots.txt нет или sitemap в нём отсутствует — сайт выдаёт уведомление об этом и пытается найти по стандартным адресам самостоятельно. Если и sitemap нет — то проверка сайта останавливается. Сделать HeadInspect краулером внутренних страниц и ссылок можно, но пока я этого решил не делать. В настоящее время отсутствие robots на сайте не критично, но без sitemap найти страницы сайта невозможно. Загрузи sitemap и проверяй сайт дальше.

Ищем robots.txt:

Код поиска robots.txt
from __future__ import annotations

from urllib.parse import urlsplit, urlunsplit

from .config import MAX_ROBOTS_BYTES
from .fetcher import safe_fetch


def robots_url_for(site_url: str) -> str:
    parts = urlsplit(site_url)
    return urlunsplit((parts.scheme, parts.netloc, "/robots.txt", "", ""))


async def fetch_robots(site_url: str) -> tuple[str, bool, str]:
    robots_url = robots_url_for(site_url)
    try:
        result = await safe_fetch(
            robots_url,
            max_bytes=MAX_ROBOTS_BYTES,
            accepted_content_types=("text/", "application/octet-stream"),
        )
    except Exception:
        return robots_url, False, ""

    if result.status_code != 200:
        return robots_url, False, ""

    return robots_url, True, result.content.decode("utf-8", errors="replace")


def sitemap_urls_from_robots(robots_text: str) -> list[str]:
    found: list[str] = []
    for raw_line in robots_text.splitlines():
        line = raw_line.strip()
        if not line or ":" not in line:
            continue
        key, value = line.split(":", 1)
        if key.strip().lstrip("\ufeff").lower() == "sitemap":
            candidate = value.strip()
            if candidate and candidate not in found:
                found.append(candidate)
    return found

Ищем sitemap в robots.txt. Если его там нет — пробует стандартные адреса. Учитываем и формат, когда sitemap является индексом других карт сайта:

Код поиска sitemap
from __future__ import annotations

from collections import deque
from urllib.parse import urljoin, urlsplit
import xml.etree.ElementTree as ET

from fastapi import HTTPException

from .config import MAX_AUDIT_URLS, MAX_SITEMAP_BYTES, MAX_SITEMAP_DEPTH, MAX_SITEMAPS
from .fetcher import safe_fetch
from .security import validate_public_url


def _local_name(tag: str) -> str:
    return tag.rsplit("}", 1)[-1].lower()


def _parse_sitemap_xml(content: bytes) -> tuple[str, list[str]]:
    try:
        root = ET.fromstring(content)
    except ET.ParseError as exc:
        raise HTTPException(status_code=502, detail="Invalid sitemap XML") from exc

    root_name = _local_name(root.tag)
    locs: list[str] = []

    for node in root.iter():
        if _local_name(node.tag) == "loc" and node.text:
            value = node.text.strip()
            if value:
                locs.append(value)

    if root_name == "sitemapindex":
        return "index", locs
    if root_name == "urlset":
        return "urlset", locs

    raise HTTPException(status_code=502, detail="Unsupported sitemap XML root element")


async def discover_urls(
    site_url: str,
    initial_sitemaps: list[str],
) -> tuple[list[str], list[str], bool]:
    if not initial_sitemaps:
        initial_sitemaps = [
            urljoin(site_url, "/sitemap.xml"),
            urljoin(site_url, "/sitemap_index.xml"),
        ]

    queue = deque((url, 0) for url in initial_sitemaps)
    seen_sitemaps: set[str] = set()
    processed_sitemaps: list[str] = []
    pages: list[str] = []
    seen_pages: set[str] = set()
    limited = False

    site_host = urlsplit(site_url).hostname

    while queue and len(seen_sitemaps) < MAX_SITEMAPS:
        sitemap_url, depth = queue.popleft()
        if depth > MAX_SITEMAP_DEPTH:
            continue

        try:
            sitemap_url = await validate_public_url(sitemap_url)
        except HTTPException:
            continue

        if sitemap_url in seen_sitemaps:
            continue
        seen_sitemaps.add(sitemap_url)

        try:
            result = await safe_fetch(
                sitemap_url,
                max_bytes=MAX_SITEMAP_BYTES,
                accepted_content_types=("xml", "text/plain", "application/octet-stream"),
            )
        except HTTPException:
            continue

        if result.status_code != 200:
            continue

        try:
            kind, locs = _parse_sitemap_xml(result.content)
        except HTTPException:
            continue

        processed_sitemaps.append(result.url)

        if kind == "index":
            for child in locs:
                if len(seen_sitemaps) + len(queue) >= MAX_SITEMAPS:
                    break
                queue.append((child, depth + 1))
            continue

        for page_url in locs:
            try:
                normalized = await validate_public_url(page_url)
            except HTTPException:
                continue

            if urlsplit(normalized).hostname != site_host:
                continue

            if normalized in seen_pages:
                continue

            if len(pages) >= MAX_AUDIT_URLS:
                limited = True
                return pages, processed_sitemaps, limited

            seen_pages.add(normalized)
            pages.append(normalized)

    return pages, processed_sitemaps, limited

А чтобы вводить домен не приходилось несколько раз, мне пришлось изрядно попотеть над UX и сделать так, чтобы при переключении между Schema, meta, sitemap и open graph не терялись результаты. Переходи между ними сколько угодно раз — результаты на месте. Постарался настолько, что теперь даже не сразу понятно, как сбросить результаты теста и перейти к другому сайту, ведь даже логотип слева вверху сохраняет результат текущей проверки. Но решением стала кнопка «Проверить заново» — она и сбрасывает текущий job.

Что уже проверяет HeadInspect

В данный момент сайт уже проверяет Open Graph, Meta, Schema и Sitemap. Вот перечень данных, собираемых для анализа:

  • Meta: title, description, keywords, robots, viewport, lang, charset. Проверяет длину title и description и даёт рекомендации (не мои — взял у плагинов типа All in One SEO и других).

  • Open Graph: og:title, og:description, og:url, og:image, дубли, ошибки и параметры изображений. Изображения — вообще боль, особенно, если они генерируются автоматически.

  • Schema.org: JSON‑LD, типы сущностей, синтаксические ошибки и базовые требования к структурированным данным.

  • Sitemap: само наличие карты, URL внутри неё, HTTP‑коды, конечные URL, редиректы, плюс состояние robots.txt и способ обнаружения карты сайта.

Проверку Canonical и Images планирую скоро запустить. Ещё в планах сделать выгрузку результатов в Эксель. В данный момент кнопка есть, но она не работает.

Сам HeadInspect я довожу до ума самим же HeadInspect

Сперва HeadInspect я намеренно оставил с хаосом в SEO. Мне это помогало искать ошибки в анализаторе. Теперь же я довёл до ума и сам HeadInspect с помощью же него самого. Работа нового сервиса и удобство мне понравились.

Казалось бы, уже полностью вычищенный сайт, но вдруг нашлось meta‑description на 114 символов — оперативно было исправлено. В обычном режиме это описание сохранилось бы на годы.

Для кого я его сделал

Да, сейчас есть уже предложения python‑скриптов, которые анализируют всё то же самое. Но для меня удобнее в браузере на экране компьютера или смартфона. И если уж я взялся за такой проект, то почему бы его не открыть для других, таких же, как и я, инди‑веб‑дизайнеров. Более того, проверки других пользователей помогут выявить недостатки сайта. Очень надеюсь, что в случае неудобства и ошибок пользователи будут писать мне на почту свои предложения.

Почему я ввёл ограничение в 500 страниц

Если инструмент станет востребованным, то это может привести к тому, что моих ресурсов и ресурсов самого сайта может не хватить, а серьёзно вкладываться в проект пока нет мотивации. Поэтому количество проверяемых страниц одного сайта я решил ограничить количеством 500. Предполагаю, что для большинства небольших сайтов этого будет вполне достаточно.

Спасибо, что дочитали мою статью до конца. Я приглашаю воспользоваться моим новым сервисом массового анализа страниц сайта. Пишите мне на почту (она есть в футере) всё что думаете: что удобно, что неудобно, какие ошибки нашли. Отдельно буду благодарен, если подскажете, в каком именно виде лучше сделать выгрузку результатов. И вообще нужна ли она.

Найти сайт можно здесь: headinspect.ru

Исходники: https://github.com/dmitrystarosta/HeadInspect