Прокладываем скоростной маршрут для поисковиков по всем страницам сайта

Поисковики — как курьеры, которым дали адрес без подъезда и этажа. Технически доедут, но будут долго бродить по двору, если не показать чёткий путь. Хлебные крошки и HTML-карта сайта — тот самый указатель «подъезд 3, домофон работает, код 1234». Google перестаёт гадать, пользователи перестают теряться, а структура сайта наконец обретает смысл, а не просто существует в виде красивой схемы в презентации для инвестора. В этой статье закрепляем иерархию сайта двумя инструментами: автоматическими хлебными крошками и HTML-картой — без JSON-LD, без циклических ссылок, без лишней магии.

Структурируем контент хлебными крошками и HTML-картой сайта

Васисуалий Пупкевич, услышав термин «хлебные крошки», минут пять был уверен, что речь о разделе кулинарного блога. Пришлось объяснять, что здесь крошки ведут не к рецепту, а к странице категории. Он выдохнул и, как обычно в таких случаях, отправил сообщение ребятам dev-team ФОРСАЖ.

Иерархия сайта работает только тогда, когда она видна

По изначальному проекту сайта у нас стандартная и логичная иерархия: Главная → Рубрика → Публикация. Метки (теги) в этой схеме — служебная штука для удобства читателей, не для поисковиков, поэтому их мы закрываем от индексации.

Чтобы эта структура не осталась красивой идеей на бумаге, делаем две вещи:

  1. Хлебные крошки — на главной, странице категории и странице записи. Обычный HTML, без JSON-LD.
  2. HTML-карта сайта — единая страница, где все ссылки на разделы и публикации оформлены якорями на основе заголовков (title).

Хлебные крошки без петель и самоповторов

Есть нюанс, который упускают из виду в девяти случаях из десяти: страница не должна ссылаться сама на себя. Если вы находитесь на странице категории «Кулинария», в крошках должно быть просто слово «Кулинария» текстом, а не бесполезная ссылка саму на себя. Кликать на ссылку, ведущую туда же, где вы уже находитесь, — занятие того же уровня азарта, что стучать в закрытую дверь собственной квартиры.

Ниже — PHP-код, который решает эту задачу полностью: для главной, категорий, меток, статичных страниц (с учётом вложенности) и отдельных записей.

function gp_custom_breadcrumbs_no_self_links() {
    if (is_admin()) {
        return;
    }

    echo '<div class="breadcrumbs grid-container">';

    if (is_front_page() || is_home()) {
        echo '<span class="current">Главная</span>';
    } else {
        echo '<a href="' . esc_url(home_url('/')) . '">Главная</a>';

        if (is_category()) {
            echo ' <span class="sep">/</span> ';
            $category = get_queried_object();
            if ($category) {
                echo '<span class="current">' . esc_html($category->name) . '</span>';
            }
        }
        
        if (is_tag()) {
            echo ' <span class="sep">/</span> ';
            $tag = get_queried_object();
            if ($tag) {
                echo '<span class="current">' . esc_html($tag->name) . '</span>';
            }
        }
        
        if (is_page()) {
            global $post;
            
            if (isset($post->post_parent) && $post->post_parent) {
                $anc = array_reverse(get_post_ancestors($post->ID));
                foreach ($anc as $ancestor) {
                    echo ' <span class="sep">/</span> ';
                    echo '<a href="' . esc_url(get_permalink($ancestor)) . '">' . esc_html(get_the_title($ancestor)) . '</a>';
                }
            }
            
            echo ' <span class="sep">/</span> ';
            echo '<span class="current">' . wp_strip_all_tags(get_the_title()) . '</span>';
        }
        
        if (is_single()) {
            echo ' <span class="sep">/</span> ';
            
            global $post;
            $primary_cat = false;

            if (class_exists('WPSEO_Primary_Term')) {
                $wpseo_primary_term = new WPSEO_Primary_Term('category', $post->ID);
                $primary_cat_id = $wpseo_primary_term->get_primary_term();
                if ($primary_cat_id) {
                    $primary_cat = get_term($primary_cat_id, 'category');
                }
            }

            if ((!$primary_cat || is_wp_error($primary_cat)) && class_exists('RankMath')) {
                $rank_math_cat_id = get_post_meta($post->ID, 'rank_math_primary_category', true);
                if ($rank_math_cat_id) {
                    $primary_cat = get_term($rank_math_cat_id, 'category');
                }
            }

            if (!$primary_cat || is_wp_error($primary_cat)) {
                $cats = get_the_category();
                if (!empty($cats) && isset($cats[0])) {
                    $primary_cat = $cats[0];
                }
            }

            if ($primary_cat && !is_wp_error($primary_cat)) {
                $cat_ancestors = get_ancestors($primary_cat->term_id, 'category');
                if (!empty($cat_ancestors)) {
                    $cat_ancestors = array_reverse($cat_ancestors);
                    foreach ($cat_ancestors as $ancestor_id) {
                        $ancestor_term = get_term($ancestor_id, 'category');
                        if ($ancestor_term && !is_wp_error($ancestor_term)) {
                            echo '<a href="' . esc_url(get_category_link($ancestor_term->term_id)) . '">' . esc_html($ancestor_term->name) . '</a>';
                            echo ' <span class="sep">/</span> ';
                        }
                    }
                }

                echo '<a href="' . esc_url(get_category_link($primary_cat->term_id)) . '">' . esc_html($primary_cat->name) . '</a>';
                echo ' <span class="sep">/</span> ';
            }

            echo '<span class="current">' . wp_strip_all_tags(get_the_title()) . '</span>';
        }
    }

    echo '</div>';
}

add_action('generate_before_main_content', 'gp_custom_breadcrumbs_no_self_links', 15);

Логика простая: везде, где мы находимся прямо сейчас, вместо ссылки выводится обычный текст (<span class="current">). Всё, что ведёт «наверх» по иерархии, остаётся кликабельным.

Важный момент: хук generate_before_main_content привязан к теме GeneratePress. Если у вас другая тема — хук нужно будет подобрать под неё, иначе крошки просто никуда не выведутся, сколько бы кода вы ни вставили.

HTML-карта сайта показывает всё дерево сразу

Хлебные крошки показывают путь от текущей страницы наверх. Карта сайта делает обратное — показывает всё дерево сайта целиком, сверху вниз:

Главная страница
  Рубрика родительская 1
    Рубрика 1
      Запись 1
      Запись N
  Рубрика N
    Запись 1
    Запись N
  Страница 1
  Страница N

Реализуем через шорткод, чтобы карту можно было вставить в контент любой страницы одной строкой.

Рекурсия строит дерево категорий вместе с записями

Вспомогательная функция, которая рекурсивно строит дерево категорий вместе со вложенными записями:


function gp_build_category_sitemap_tree($parent_id, $current_url) {

    $categories = get_categories(array(
        'parent'     => $parent_id,
        'orderby'    => 'name',
        'order'      => 'ASC',
        'hide_empty' => 1
    ));

    if (empty($categories)) {
        return '';
    }

    $html = '<ul>';

    foreach ($categories as $category) {
        $category_link = rtrim(get_category_link($category->term_id), '/');
        
        $html .= '<li class="sitemap-category">';
        if ($current_url === $category_link) {
            $html .= '<span class="sitemap-current">' . esc_html($category->name) . '</span>';
        } else {
            $html .= '<a href="' . esc_url($category_link) . '/">' . esc_html($category->name) . '</a>';
        }

        $posts = get_posts(array(
            'category'    => $category->term_id,
            'numberposts' => -1,
            'post_status' => 'publish'
        ));

        $posts_html = '';
        if (!empty($posts)) {
            foreach ($posts as $post) {
                $primary_cat_id = false;
                
                if (class_exists('WPSEO_Primary_Term')) {
                    $wpseo_primary_term = new WPSEO_Primary_Term('category', $post->ID);
                    $primary_cat_id = $wpseo_primary_term->get_primary_term();
                }
                
                if (!$primary_cat_id && class_exists('RankMath')) {
                    $primary_cat_id = get_post_meta($post->ID, 'rank_math_primary_category', true);
                }

                if ($primary_cat_id && (int)$primary_cat_id !== (int)$category->term_id) {
                    continue;
                }
                
                if (!$primary_cat_id) {
                    $post_cats = get_the_category($post->ID);
                    if (!empty($post_cats) && (int)$post_cats[0]->term_id !== (int)$category->term_id) {
                        continue;
                    }
                }

                $post_link = rtrim(get_permalink($post->ID), '/');
                $posts_html .= '<li class="sitemap-post">';
                if ($current_url === $post_link) {
                    $posts_html .= '<span class="sitemap-current">' . esc_html(get_the_title($post->ID)) . '</span>';
                } else {
                    $posts_html .= '<a href="' . esc_url($post_link) . '/">' . esc_html(get_the_title($post->ID)) . '</a>';
                }
                $posts_html .= '</li>';
            }
        }

        $child_categories_html = gp_build_category_sitemap_tree($category->term_id, $current_url);

        if (!empty($posts_html) || !empty($child_categories_html)) {
            $html .= '<ul>';
            $html .= $posts_html;
            if (!empty($child_categories_html)) {
                $child_categories_html = preg_replace('/^<ul>/', '', $child_categories_html);
                $child_categories_html = preg_replace('/<\/ul>$//', '', $child_categories_html);
                $html .= $child_categories_html;
            }
            $html .= '</ul>';
        }

        $html .= '</li>';
    }

    $html .= '</ul>';
    return $html;
}

Обратите внимание на блок с проверкой Primary category (Yoast SEO / Rank Math). Без него запись, у которой несколько категорий, продублируется на карте несколько раз — по разу в каждой рубрике. С проверкой запись появляется только там, где она назначена основной. Аккуратно и без дублей.

Шорткод собирает главную, дерево и страницы в единую карту


function gp_html_sitemap_shortcode() {
    if (is_admin()) return;

    global $wp;
    $current_url = home_url(add_query_arg(array(), $wp->request));
    $current_url = rtrim($current_url, '/');
    $home_url = rtrim(home_url('/'), '/');

    $output = '<div class="html-sitemap">';
    $output .= '<ul>';

    $output .= '<li class="sitemap-home">';
    if ($current_url === $home_url) {
        $output .= '<span class="sitemap-current">Главная страница</span>';
    } else {
        $output .= '<a href="' . esc_url(home_url('/')) . '">Главная страница</a>';
    }
    
    $categories_tree = gp_build_category_sitemap_tree(0, $current_url);
    if (!empty($categories_tree)) {
        $categories_tree = preg_replace('/^<ul>/', '', $categories_tree);
        $categories_tree = preg_replace('/<\/ul>$//', '', $categories_tree);
        $output .= '<ul>' . $categories_tree . '</ul>';
    }

    $page_on_front = get_option('page_on_front');
    $exclude_ids = array($page_on_front, 124); 

    $pages = get_pages(array(
        'sort_column' => 'post_title',
        'sort_order'  => 'ASC',
        'exclude'     => implode(',', array_filter($exclude_ids))
    ));

    if (!empty($pages)) {
        $output .= '<ul>';
        foreach ($pages as $page) {
            $page_link = rtrim(get_page_link($page->ID), '/');
            $output .= '<li class="sitemap-page">';
            if ($current_url === $page_link) {
                $output .= '<span class="sitemap-current">' . esc_html(get_the_title($page->ID)) . '</span>';
            } else {
                $output .= '<a href="' . esc_url($page_link) . '/">' . esc_html(get_the_title($page->ID)) . '</a>';
            }
            $output .= '</li>';
        }
        $output .= '</ul>';
    }

    $output .= '</li>'; 
    $output .= '</ul>';
    $output .= '</div>';

    return $output;
}
add_shortcode('html_sitemap', 'gp_html_sitemap_shortcode');

Обратите внимание на $exclude_ids = array($page_on_front, 124) — здесь 124 это ID самой страницы карты сайта. Она исключена из списка статичных страниц по той же логике «не ссылаемся сами на себя»: карта не должна включать саму себя как обычный пункт списка.

Вставляем шорткод в контент страницы

Вставляем шорткод [html_sitemap] в контент страницы с ID 124 — и карта сайта готова.

Часто задаваемые вопросы о структуре сайта
Зачем сайту хлебные крошки, если есть меню?
Меню показывает разделы верхнего уровня, а хлебные крошки — точное положение конкретной страницы в иерархии сайта. Это помогает и пользователям (понятно, куда вернуться на шаг назад), и поисковикам, которые используют структуру крошек для понимания вложенности контента и иногда показывают её прямо в сниппете поисковой выдачи.
Почему нельзя использовать готовый плагин для хлебных крошек?
Можно, но многие плагины генерируют лишний JSON-LD, добавляют ненужные скрипты и стили, или ссылают текущую страницу саму на себя — что создаёт бесполезные циклические ссылки. Собственный код даёт полный контроль над разметкой и не тянет лишний вес на страницу.
В чём разница между HTML-картой сайта и XML sitemap для Google?
XML sitemap (обычно генерируется Yoast SEO или Rank Math) — техническая карта только для поисковых роботов, невидимая пользователям. HTML-карта сайта — обычная страница для людей, которая заодно помогает поисковикам быстрее обходить всю структуру сайта через внутренние ссылки.
Почему важно исключать теги (метки) из индексации?
Теги на большинстве сайтов создают дублирующийся контент — одни и те же записи собираются под разными метками. Поисковики не любят дубли и могут занижать позиции страниц с похожим содержимым. Закрывая теги от индексации, вы убираете источник дублей, оставляя чистую иерархию: рубрика → запись.
Что случится, если не следить за primary-категорией записей на карте сайта?
Если у записи несколько категорий и не задана основная (primary), запись при построении дерева попадёт сразу под все свои рубрики. В итоге одна и та же ссылка продублируется в нескольких местах карты — а это лишний шум и для пользователей, и для поисковиков, которые могут расценить структуру как нелогичную.
Сегодня: 10.11.2026
Дата публикации: 22.08.2026
Дата обновления: 06.11.2026

Оставьте комментарий