wpnotes.ru wordpress WP Notes

Как исключить текущую страницу из цикла WordPress в шаблоне и не сломать вывод записей

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

Решается это не одним способом. Иногда достаточно добавить post__not_in в запрос. Иногда нужно исключать текущий термин через term_id и exclude. А иногда проблема вообще не в запросе, а в том, что в шаблоне используется глобальный $post не там, где надо.

Когда проблема действительно в цикле

Сначала стоит проверить, что именно дублируется. Если текущая запись появляется в списке, который строится через WP_Query, причина почти всегда в том, что запрос не знает, что нужно исключить текущий объект. Если же дубль виден в основном контенте, а не в отдельном блоке, значит, ошибка уже в шаблоне или в фильтрах, которые вмешиваются в основной запрос.

Признаки, что нужен именно post__not_in

  • на странице записи блок «похожие» показывает саму эту запись;
  • в виджете или секции ниже контента повторяется текущий пост;
  • в архиве с кастомным запросом первая карточка совпадает с открытой страницей;
  • после переключения темы проблема остаётся, но меняется только место вывода.

Что проверить до правки кода

  • не используется ли в шаблоне query_posts() — это плохая практика и часто источник побочных эффектов;
  • не переопределяется ли глобальный $post без wp_reset_postdata();
  • не добавляет ли плагин фильтр в pre_get_posts;
  • не кешируется ли старый HTML блока в объектном кеше или transient.

Рабочее решение через WP_Query

Если вам нужно исключить текущую запись из дополнительного списка, самый прямой путь — передать её ID в post__not_in. Это работает стабильно и читается без сюрпризов.

<?php
$current_id = get_the_ID();

$args = array(
    'post_type'           => 'post',
    'posts_per_page'      => 6,
    'post__not_in'        => array( $current_id ),
    'ignore_sticky_posts'  => true,
    'no_found_rows'       => true,
);

$related = new WP_Query( $args );

if ( $related->have_posts() ) :
    echo '<ul class="related-posts">';
    while ( $related->have_posts() ) : $related->the_post();
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_permalink() ),
            esc_html( get_the_title() )
        );
    endwhile;
    echo '</ul>';
endif;

wp_reset_postdata();
?>

Здесь важны две вещи. Во-первых, post__not_in принимает массив ID, даже если исключаете один объект. Во-вторых, после кастомного цикла нужен wp_reset_postdata(), иначе следующий шаблонный код может начать работать с неправильным постом.

Если нужно исключить несколько записей

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

<?php
$exclude_ids = array( get_the_ID() );

if ( ! empty( $featured_id ) ) {
    $exclude_ids[] = (int) $featured_id;
}

$args = array(
    'post_type'      => 'post',
    'posts_per_page' => 8,
    'post__not_in'   => array_unique( array_map( 'intval', $exclude_ids ) ),
);

$query = new WP_Query( $args );
?>

Как исключить текущий объект в архиве или на странице таксономии

На архивах проблема обычно не в текущей записи, а в текущем термине. Например, вы выводите список рубрик на странице рубрики и не хотите показывать саму рубрику в этом же списке. Для этого у get_terms() и WP_Term_Query есть свой набор аргументов.

<?php
$current_term = get_queried_object();

$terms = get_terms( array(
    'taxonomy'   => 'category',
    'hide_empty' => false,
    'exclude'    => array( (int) $current_term->term_id ),
) );

if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
    echo '<ul class="term-list">';
    foreach ( $terms as $term ) {
        printf(
            '<li><a href="%s">%s</a></li>',
            esc_url( get_term_link( $term ) ),
            esc_html( $term->name )
        );
    }
    echo '</ul>';
}
?>

Если у вас кастомная таксономия, логика та же. Главное — брать текущий объект через get_queried_object(), а не гадать по глобальным переменным.

Сравнение подходов: код, фильтр, готовый плагин

Иногда задачу можно решить на уровне шаблона, а иногда удобнее вынести логику в фильтр. Ниже — практическое сравнение без лишней теории.

ПодходКогда использоватьПлюсыМинусы
Код в шаблонеОдин конкретный блок «похожие» или «ещё статьи»Прозрачно, легко проверитьНужно править тему
pre_get_postsНужно менять основной запрос или несколько архивовЦентрализованноЛегко задеть лишние страницы
Плагин для очистки SEO-дублейПараллельно нужно убрать технические дубли, архивы, служебные страницыМеньше ручного кодаНе решает именно логику цикла в шаблоне

Если задача шире, чем один шаблон, иногда удобнее подключить инструмент вроде Clearfy Pro: он помогает с технической чисткой сайта и SEO-настройками, но сам по себе не заменяет корректный WP_Query. Ссылка на продукт: Clearfy Pro.

Диагностика: как понять, что запрос собран правильно

Перед тем как считать задачу закрытой, проверьте сам запрос. В WordPress это можно сделать без сторонних инструментов: временно выведите аргументы или посмотрите SQL через отладку. Если у вас включён WP_DEBUG, проще заметить, что запрос вообще не исключает нужный ID.

<?php
$args = array(
    'post_type'     => 'post',
    'post__not_in'   => array( get_the_ID() ),
    'posts_per_page' => 6,
);

$query = new WP_Query( $args );

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
    error_log( print_r( $query->query_vars, true ) );
}
?>

Ещё один практичный способ — временно вывести ID найденных записей и сравнить их с текущим:

<?php
while ( $query->have_posts() ) : $query->the_post();
    echo '<!-- post ID: ' . (int) get_the_ID() . ' -->';
endwhile;
wp_reset_postdata();
?>

Если текущий ID всё равно попадает в список, значит, где-то выше по стеку запрос переписывается фильтром или кеш отдаёт старую версию блока.

Частые ошибки и как их исправить

Используют query_posts()

Это одна из самых частых причин странного поведения. query_posts() перезаписывает основной запрос и может ломать пагинацию, хлебные крошки и условные теги. Для дополнительных списков используйте только WP_Query или get_posts().

Забывают wp_reset_postdata()

После кастомного цикла глобальный $post остаётся в состоянии последнего элемента. В итоге следующий вызов the_title() или the_permalink() может вывести не то, что ожидается. Если в шаблоне несколько вложенных циклов, этот сбой проявляется особенно быстро.

Путают ID записи и ID термина

Для записей нужен post__not_in, для терминов — exclude. Если передать не тот тип ID, WordPress не всегда выдаст явную ошибку, но результат будет неверным.

Не учитывают кеш

Если блок кэшируется через transient, объектный кеш или CDN, вы можете исправить код и всё равно видеть старый дубль. После правки очистите кеш плагина, сервера и, если нужно, CDN. Иначе проверка будет ложной.

Проверка результата после внедрения

После правки откройте страницу, где раньше был дубль, и проверьте три вещи:

  • текущий объект больше не присутствует в списке;
  • ссылки в блоке ведут на другие записи или термины;
  • после обновления страницы и очистки кеша результат не меняется.

Если у вас есть доступ к логам или отладочному выводу, сравните ID текущей страницы с ID элементов в цикле. Для таксономий проверьте, что исключён именно текущий термин, а не соседний.

Хорошая практика — открыть страницу в режиме инкогнито и отдельно проверить её после очистки кеша объекта, если он используется. Так вы исключите ситуацию, когда локально всё уже исправлено, а посетители видят старую версию.

Когда лучше вынести логику в отдельную функцию

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

<?php
function wpnotes_related_posts_args( $current_id, $limit = 6 ) {
    return array(
        'post_type'           => 'post',
        'posts_per_page'      => (int) $limit,
        'post__not_in'        => array( (int) $current_id ),
        'ignore_sticky_posts' => true,
        'no_found_rows'       => true,
    );
}
?>

Такой подход проще сопровождать: если позже нужно добавить ещё один исключаемый ID или поменять тип записи, вы правите одно место, а не пять шаблонов.

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

Для блоков, которые выводятся на каждой странице, не тащите лишние данные. Если вам не нужна пагинация, ставьте no_found_rows в true. Если не нужны sticky-записи, отключайте их обработку. Если запрос строится только ради нескольких карточек, не запрашивайте лишние поля и не делайте тяжёлые JOIN без необходимости.

С точки зрения безопасности не подставляйте в post__not_in сырые данные из GET/POST без приведения к целому числу. Даже если WordPress сам не выполнит опасный SQL в этом аргументе, привычка нормализовать входные данные нужна в любом шаблоне.

Если блок формируется через AJAX или REST API, проверяйте, что текущий ID передаётся только как число и что ответ не кэшируется дольше, чем нужно. Иначе вы получите редкий, но неприятный баг: пользователь открывает страницу, а в подборке всё ещё видит сам себя.

×
Quizle
Получите больше лидов и увеличьте продажи!
-15%

на премиум плагин WordPress

Получить скидку ⋙