Поисковики — как курьеры, которым дали адрес без подъезда и этажа. Технически доедут, но будут долго бродить по двору, если не показать чёткий путь. Хлебные крошки и HTML-карта сайта — тот самый указатель «подъезд 3, домофон работает, код 1234». Google перестаёт гадать, пользователи перестают теряться, а структура сайта наконец обретает смысл, а не просто существует в виде красивой схемы в презентации для инвестора. В этой статье закрепляем иерархию сайта двумя инструментами: автоматическими хлебными крошками и HTML-картой — без JSON-LD, без циклических ссылок, без лишней магии.
Структурируем контент хлебными крошками и HTML-картой сайта
Васисуалий Пупкевич, услышав термин «хлебные крошки», минут пять был уверен, что речь о разделе кулинарного блога. Пришлось объяснять, что здесь крошки ведут не к рецепту, а к странице категории. Он выдохнул и, как обычно в таких случаях, отправил сообщение ребятам dev-team ФОРСАЖ.
Иерархия сайта работает только тогда, когда она видна
По изначальному проекту сайта у нас стандартная и логичная иерархия: Главная → Рубрика → Публикация. Метки (теги) в этой схеме — служебная штука для удобства читателей, не для поисковиков, поэтому их мы закрываем от индексации.
Чтобы эта структура не осталась красивой идеей на бумаге, делаем две вещи:
- Хлебные крошки — на главной, странице категории и странице записи. Обычный HTML, без JSON-LD.
- 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 — и карта сайта готова.
Дата публикации: 22.08.2026
Дата обновления: 06.11.2026
