Предыстория

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

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

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

За пару месяцев я уже обновил несколько своих сайтов с точки зрения дизайна, что‑то подверглось даже переделке в плане структуры сайтов. Но, когда сайт уже готов, начинается техническая работа, которая видна поисковым роботам, а не человеку. К 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