Можно ли упорядочить ссылки на следующий/предыдущий пост по порядку меню или по мета-ключу?
У меня есть серия постов, которые упорядочены по значению meta_key. При необходимости их также можно расположить по порядку меню.
Ссылки на следующий/предыдущий пост (генерируемые с помощью next_post_link
, previous_post_link
или posts_nav_link
) все навигируют по хронологии. Хотя я понимаю это поведение по умолчанию, я не понимаю, как его изменить. Я обнаружил, что это связано с adjacent_post_link в link-template.php, но затем это начинает казаться довольно жестко закодированным. Рекомендуется ли переписать это с нуля для замены, или есть лучшее решение.
Понимание внутренней работы
"Сортировка" соседних (следующих/предыдущих) записей на самом деле не является порядком сортировки. Это отдельный запрос для каждой страницы, но он сортирует результаты по post_date
— или по родительской записи, если у вас иерархический тип записи и он отображается в текущий момент.
Если заглянуть внутрь функции next_post_link()
, то можно увидеть, что это по сути обёртка API для adjacent_post_link()
. Последняя функция вызывает get_adjacent_post()
с аргументом $previous
, установленным в bool(true|false)
, чтобы получить ссылку на следующую или предыдущую запись.
Что можно фильтровать?
При более глубоком изучении становится видно, что get_adjacent_post()
Источник содержит несколько полезных фильтров для результата запроса: (Название фильтра/Аргументы)
"get_{$adjacent}_post_join"
$join // Только если `$in_same_cat` // или: ! empty( $excluded_categories` // и тогда: // " INNER JOIN $wpdb->term_relationships AS tr // ON p.ID = tr.object_id // INNER JOIN $wpdb->term_taxonomy tt // ON tr.term_taxonomy_id = tt.term_taxonomy_id"; // а если $in_same_cat, то добавляется: // " AND tt.taxonomy = 'category' // AND tt.term_id IN (" . implode(',', $cat_array) . ")"; $in_same_cat $excluded_categories
"get_{$adjacent}_post_where"
$wpdb->prepare( // $op = $previous ? '<' : '>'; | $current_post_date "WHERE p.post_date $op %s " // $post->post_type ."AND p.post_type = %s " // $posts_in_ex_cats_sql = " AND tt.taxonomy = 'category' // AND tt.term_id NOT IN (" . implode($excluded_categories, ',') . ')'; // ИЛИ пустая строка, если $in_same_cat || ! empty( $excluded_categories ."AND p.post_status = 'publish' $posts_in_ex_cats_sql " ", $current_post_date, $post->post_type ) $in_same_cat $excluded_categories
"get_{$adjacent}_post_sort"
"ORDER BY p.post_date $order LIMIT 1"`
Таким образом, возможности очень широки: можно фильтровать условие WHERE
, таблицу JOIN
и оператор ORDER BY
.
Результат кэшируется в памяти на время текущего запроса, поэтому повторные вызовы функции не приводят к дополнительным SQL-запросам.
Автоматическое построение запроса
Как отметил @StephenHarris в комментариях, в ядре WordPress есть полезная функция для построения SQL-запросов: get_meta_sql()
— примеры в Codex. Эта функция обычно используется для формирования SQL-условий метаполей в WP_Query
, но её можно применять и в других случаях. Аргументом служит массив, аналогичный тому, что передаётся в WP_Query
.
$meta_sql = get_meta_sql(
$meta_query,
'post',
$wpdb->posts,
'ID'
);
Функция возвращает массив:
$sql => (array) 'join' => array(),
(array) 'where' => array()
Таким образом, в своём обработчике можно использовать $sql['join']
и $sql['where']
.
Важные зависимости
В вашем случае проще всего перехватить запрос через небольшой (mu)плагин или в файле functions.php темы и изменить его в зависимости от переменных $adjacent = $previous ? 'previous' : 'next';
и $order = $previous ? 'DESC' : 'ASC';
.
Фактические названия фильтров
Имена фильтров следующие:
get_previous_post_join
,get_next_post_join
get_previous_post_where
,get_next_post_where
get_previous_post_sort
,get_next_post_sort
Пример реализации в виде плагина
Обработчик фильтра может выглядеть, например, так:
<?php
/** Plugin Name: (#73190) Изменение порядка сортировки соседних записей */
function wpse73190_adjacent_post_sort( $orderby )
{
return "ORDER BY p.menu_order DESC LIMIT 1";
}
add_filter( 'get_previous_post_sort', 'wpse73190_adjacent_post_sort' );
add_filter( 'get_next_post_sort', 'wpse73190_adjacent_post_sort' );

+1. Просто для информации, (@magnakai), если нужно сделать что-то подобное для мета-запросов, посмотрите get_meta_sql()

+1 тебе, @StephenHarris! Раньше не видел эту функцию. Короткий вопрос: как я понял из исходного кода, нужно передавать полностью сформированный объект запроса, как это сделать с упомянутыми выше фильтрами? Насколько я вижу, там передаются только строки запросов, так как сам запрос выполняется после фильтров.

нет, $meta_query
- это просто массив, который ты передаешь в WP_Query
для аргумента meta_query
: В этом примере: $meta_sql = get_meta_sql( $meta_query, 'post', $wpdb->posts, 'ID');
- это генерирует части запроса JOIN
и WHERE
, которые нужно добавить.

@StephenHarris Идеальный момент, чтобы отредактировать один (мой) ответ.

@StephenHarris, у меня не получается применить результат get_meta_sql() - поможешь соединить точки?

@Magnakai это массив $sql => (array) 'join' => array(), 'where' => array()
. Так что просто возьми $sql['join'];
или $sql['where']
.

Ответ Kaiser'а великолепен и детален, однако просто изменить предложение ORDER BY недостаточно, если ваш menu_order
не соответствует хронологическому порядку.
Я не могу приписать себе заслугу этого решения, но я нашел следующий код в этом gist'е:
<?php
/**
* Настройка порядка соседних ссылок на записи
*/
function wpse73190_gist_adjacent_post_where($sql) {
if ( !is_main_query() || !is_singular() )
return $sql;
$the_post = get_post( get_the_ID() );
$patterns = array();
$patterns[] = '/post_date/';
$patterns[] = '/\'[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\'/';
$replacements = array();
$replacements[] = 'menu_order';
$replacements[] = $the_post->menu_order;
return preg_replace( $patterns, $replacements, $sql );
}
add_filter( 'get_next_post_where', 'wpse73190_gist_adjacent_post_where' );
add_filter( 'get_previous_post_where', 'wpse73190_gist_adjacent_post_where' );
function wpse73190_gist_adjacent_post_sort($sql) {
if ( !is_main_query() || !is_singular() )
return $sql;
$pattern = '/post_date/';
$replacement = 'menu_order';
return preg_replace( $pattern, $replacement, $sql );
}
add_filter( 'get_next_post_sort', 'wpse73190_gist_adjacent_post_sort' );
add_filter( 'get_previous_post_sort', 'wpse73190_gist_adjacent_post_sort' );
Я изменил названия функций для WP.SE.
Если вы измените только предложение ORDER BY, запрос всё равно будет искать записи с датой больше или меньше текущей. Если ваши записи не в хронологическом порядке, вы не получите правильную запись.
Этот код изменяет условие WHERE для поиска записей, где menu_order больше или меньше menu_order текущей записи, в дополнение к изменению порядка сортировки.
Также предложение orderby не должно быть жестко закодировано с использованием DESC, так как оно должно меняться в зависимости от того, получаете ли вы следующую или предыдущую ссылку.

Одно замечание: Условие WHERE
ожидает формат 'YYYY-mm-dd HH:mm:ss'
. Если этот формат не соблюдён, условие не сработает. Поскольку значение устанавливается не базой данных, а приложением, вам нужно сначала проверить соответствие этому формату при построении регулярного выражения.

Если вам нужно сортировать по post_title
, вы можете заменить все вхождения menu_order
в приведённом выше коде, и всё должно работать. Обратите внимание на второй элемент массива $replacements
— мне пришлось заключить его в одинарные кавычки, чтобы он работал: $replacements[] = '\'' . $the_post->post_title . '\'';

Пытался подключить хук без успеха. Возможно, это просто проблема моей конфигурации, но для тех, у кого не получается заставить хук работать, вот самое простое решение:
<?php
$all_posts = new WP_Query(array(
'orderby' => 'menu_order',
'order' => 'ASC',
'posts_per_page' => -1
));
foreach($all_posts->posts as $key => $value) {
if($value->ID == $post->ID){
$nextID = $all_posts->posts[$key + 1]->ID;
$prevID = $all_posts->posts[$key - 1]->ID;
break;
}
}
?>
<?php if($prevID): ?>
<span class="prev">
<a href="<?= get_the_permalink($prevID) ?>" rel="prev"><?= get_the_title($prevID) ?></a>
</span>
<?php endif; ?>
<?php if($nextID): ?>
<span class="next">
<a href="<?= get_the_permalink($nextID) ?>" rel="next"><?= get_the_title($nextID) ?></a>
</span>
<?php endif; ?>

после нескольких часов попыток заставить get_previous_post_where
, get_previous_post_join
и get_previous_post_sort
корректно работать с пользовательскими типами записей и сложной сортировкой, включающей мета-поля, я сдался и использовал это. Спасибо!

То же самое, мне нужно было сортировать не только по Menu Order, но и искать записи с определёнными meta_key и meta_value, поэтому этот метод оказался лучшим. Единственное изменение, которое я сделал - обернул это в функцию.

@eballeste, если ты имеешь в виду получение первого поста при нахождении на последнем и последнего при нахождении на первом, смотри мой ответ ниже

function wpse73190_gist_adjacent_post_sort( $sql ) {
$pattern = '/post_date/';
$replacement = 'menu_order';
return preg_replace( $pattern, $replacement, $sql );
}
add_filter( 'get_next_post_sort', 'wpse73190_gist_adjacent_post_sort' );
add_filter( 'get_previous_post_sort', 'wpse73190_gist_adjacent_post_sort' );

Кстати, вот как можно отсортировать по menu_order
для определенного типа записи:
/**
* Настройка порядка соседних записей
*/
add_filter('get_next_post_sort', function($order) {
if (is_singular('my_custom_post_type')) {
return 'ORDER BY p.menu_order ASC LIMIT 1';
}
return $order;
}, 10);
add_filter('get_previous_post_sort', function($order) {
if (is_singular('my_custom_post_type')) {
return 'ORDER BY p.menu_order DESC LIMIT 1';
}
return $order;
}, 10);
add_filter('get_next_post_where', function() {
if (is_singular('my_custom_post_type')) {
global $post, $wpdb;
return $wpdb->prepare("WHERE p.menu_order > %s AND p.post_type = %s AND p.post_status = 'publish'", $post->menu_order, $post->post_type);
}
}, 10);
add_filter('get_previous_post_where', function() {
if (is_singular('my_custom_post_type')) {
global $post, $wpdb;
return $wpdb->prepare("WHERE p.menu_order < %s AND p.post_type = %s AND p.post_status = 'publish'", $post->menu_order, $post->post_type);
}
}, 10);
Надеюсь, это поможет кому-то еще!

На основе ответа @Szabolcs Páll я создал этот вспомогательный класс с методами для получения записей типа по порядку меню, а также для получения следующей и предыдущей записи по порядку меню. Дополнительно я добавил проверки, является ли текущая запись первой или последней, чтобы в таком случае возвращать последнюю или первую запись соответственно.
Например:
// $currentPost является первой по порядку меню
getPreviousPostByMenuOrder($postType, $$currentPost->ID)
// возвращает => последнюю запись по порядку меню
// $currentPost является последней по порядку меню
getPreviousPostByMenuOrder($postType, $$currentPost->ID)
// возвращает => первую запись по порядку меню
Полный класс:
class PostMenuOrderUtils {
public static function getPostsByMenuOrder($postType){
$args =[
'post_type' => $postType,
'orderby' => 'menu_order',
'order' => 'ASC',
'posts_per_page' => -1
];
$posts = get_posts($args);
return $posts;
}
public static function getNextPostByMenuOrder($postType, $postID){
$posts = self::getPostsByMenuOrder($postType);
$nextPost = null;
foreach($posts as $key => $value) {
if($value->ID == $postID){
$nextPost = $posts[$key] !== end($posts) ? $posts[$key + 1] : $posts[0];
break;
}
}
return $nextPost;
}
public static function getPreviousPostByMenuOrder($postType, $postID){
$posts = self::getPostsByMenuOrder($postType);
$prevPost = null;
foreach($posts as $key => $value) {
if($value->ID == $postID){
$prevPost = $key !== 0 ? $posts[$key - 1] : end($posts);
break;
}
}
return $prevPost;
}
}

На основе ответа от @Szabolcs Páll и статьи bbloomer о добавлении кнопок "следующий/предыдущий" на странице товара WooCommerce, я создал этот код.
Он сортирует все товары по мета-ключу и добавляет кнопки "предыдущий/следующий" сверху и снизу товара.
(Мета-ключом может быть и поле ACF!)
/**
* @snippet Добавление кнопок "следующий/предыдущий" с сортировкой по мета-ключу или полю ACF @ WooCommerce Single Product Page
* @testedwith WooCommerce 4.8.0
* @source Elron : https://wordpress.stackexchange.com/a/365334/98773
* @thanks bbloomer : https://businessbloomer.com/?p=20567
* @thanks Szabolcs Páll : https://wordpress.stackexchange.com/a/284045/98773
*/
add_action('woocommerce_before_single_product', 'elron_prev_next_product');
// а если хотите их и внизу...
add_action('woocommerce_after_single_product', 'elron_prev_next_product');
function elron_prev_next_product()
{
global $post;
echo '<div class="prev-next-buttons">';
$all_posts = new WP_Query(
array(
'post_type' => 'product',
'meta_key' => 'the_meta_key_or_acf_field', // <-- ИЗМЕНИТЕ ЭТО
'orderby' => 'meta_value',
'order' => 'DESC',
'posts_per_page' => -1
)
);
foreach ($all_posts->posts as $key => $value) {
if ($value->ID == $post->ID) {
$nextID = $all_posts->posts[$key + 1]->ID;
$prevID = $all_posts->posts[$key - 1]->ID;
break;
}
}
if ($prevID) : ?>
<a href="<?= get_the_permalink($prevID) ?>" rel="prev" class="prev" title="<?= get_the_title($prevID) ?>"><?= esc_attr__('Предыдущий товар') ?></a>
<?php endif; ?>
<?php if ($nextID) : ?>
<a href="<?= get_the_permalink($nextID) ?>" rel="next" class="next" title="<?= get_the_title($nextID) ?>"><?= esc_attr__('Следующий товар') ?></a>
<?php endif; ?>
<?php
echo '</div>';
}
Если вам нужен дополнительный scss-файл, который я использовал: _prev-next-buttons.scss
.prev-next-buttons {
background: $lightpurple;
padding: 2em;
text-align: center;
a {
opacity: 0.7;
border-radius: 0.5em;
border: $white 1px solid;
color: $white;
display: inline-block;
padding: 0.5em 0.8em;
text-decoration: none;
margin: 0 0.1em;
&:hover, &:focus {
opacity: 1;
}
}
.prev {
&:before {
content: " ";
}
}
.next {
&:after {
content: " ";
}
}
}
.rtl {
.prev-next-buttons {
.prev {
&:before {
content: " ";
}
}
.next {
&:after {
content: " ";
}
}
}
}

Спасибо за ценную информацию. Я использовал её с моим пользовательским типом записи, у которого также есть пользовательское поле даты. Работает отлично на WordPress 5.9.3. Также могу подтвердить, что это работает с пользовательскими типами записей и полями, созданными с помощью сторонних плагинов, таких как ACF.

Ни один из ответов, представленных здесь или в интернете в целом, которые я смог найти на момент написания этого текста, не предлагал достаточно простого/элегантного решения для вывода ссылок на Следующую/Предыдущую запись, отсортированных по мета-ключу. Этот вариант отлично работает для меня и легко адаптируется. Наслаждайтесь!
add_filter( 'get_previous_post_where', function( $where ) {
return get_adjacent_post_where( $where, false) ;
});
add_filter( 'get_next_post_where', function ( $where ) {
return get_adjacent_post_where( $where, true );
});
function get_adjacent_post_where( $where, $is_next ) {
global $post;
/* Укажите ваш тип записи -> */
$post_type = "_my_post_type_";
if ($post_type == $post->post_type){
global $wpdb;
$show_private = current_user_can( 'read_private_pages', $post->ID );
/* Укажите название вашего мета-ключа -> */
$meta_key = '_my_meta_key_name_';
$meta_value = get_post_meta($post->ID,$meta_key,true);
$operand = $is_next?">":"<";
$direction = $is_next?"ASC":"DESC";
$sub_query = "(SELECT m.post_id FROM `" . $wpdb->postmeta . "` AS m JOIN `" . $wpdb->posts . "` as p1 ON m.post_id = p1.ID "
. "WHERE m.meta_key = '$meta_key' AND m.meta_value $operand '$meta_value' "
. "AND (p1.post_status = 'publish'" . ($show_private?" OR p1.post_status = 'private') ":") ")
. "ORDER BY m.meta_value $direction LIMIT 1)";
/* Вложенный подзапрос обходит текущие ограничения mysql/mariadb */
$where = "WHERE p.post_type = '$post_type' AND p.ID IN (SELECT * FROM $sub_query as sq)";
}
return $where;
}

В вашем коде есть ссылки на таблицы с префиксами, например kc_wppostmeta
и kc_wpposts
. Лучшей практикой является использование глобального объекта $wpdb
для указания этих таблиц, например $wpdb->posts
вместо kc_wpposts
.
Также стоит отметить, что это предполагает получение публично или приватно опубликованного контента в вашем подзапросе. В основном коде есть целый раздел условий, который обрабатывает это: https://github.com/WordPress/wordpress-develop/blob/8338c630284124bbe79dc871822d6767e3b45f0b/src/wp-includes/link-template.php#L1893

Я нахожу этот небольшой плагин действительно удобным: http://wordpress.org/plugins/wp-query-powered-adjacent-post-link/
WP_Query Powered Adjacent Post Link — это плагин для разработчиков. Он добавляет в WordPress функцию
wpqpapl();
, которая может возвращать информацию о предыдущей и следующей записях относительно текущей. Функция принимает аргументы для использования в классеWP_Query
.

Это сработало для меня:
add_filter( 'get_previous_post_where', 'so16495117_mod_adjacent_bis' );
add_filter( 'get_next_post_where', 'so16495117_mod_adjacent_bis' );
function so16495117_mod_adjacent_bis( $where ) {
global $wpdb;
return $where . " AND p.ID NOT IN ( SELECT post_id FROM $wpdb->postmeta WHERE ($wpdb->postmeta.post_id = p.ID ) AND $wpdb->postmeta.meta_key = 'archive' AND $wpdb->postmeta.meta_value = 1 )";
}

У меня тоже были с этим проблемы. Волшебным образом всё заработало вот так:
- Убедитесь, что ваш тип записи поддерживает иерархические записи.
- Затем используйте простой плагин для сортировки произвольных типов записей.
https://wordpress.org/plugins/simple-custom-post-order/
Помимо простой сортировки перетаскиванием, этот плагин гарантирует правильную работу ссылок "предыдущая" и "следующая" через порядок меню. К сожалению, он может работать в обратном порядке (DSC вместо ASC). Чтобы это исправить, мы можем создать функцию обратной навигации по записям. - Используйте функцию обратной навигации. Я нашёл одну на gist. (добавьте в файл functions.php вашей темы) https://gist.github.com/jaredchu/3e3bcb866240d1d32a3b4ae55905b135#file-the_reverse_post_navigation
И мне не пришлось писать ни строчки кода самому :)

Я отредактировал код Szabolcs Páll выше, чтобы сортировать по пользовательскому meta_key и в рамках определенной категории, а также попытался добавить условия для первого и последнего постов.
В оригинальном коде для первого и последнего поста не отображались правильные ссылки next/prev, показывалась только ссылка на текущий ID поста, на котором я находился.
Ниже приведенный код сработал для меня, но я не уверен, есть ли в нем потенциальные проблемы. (Я не самый продвинутый кодер)
<?php
$all_posts = new WP_Query(array(
'taxonomy' => 'category',
'category_name' => 'projects',
'meta_key' => 'grid_number_projects',
'orderby' => 'meta_value',
'order' => 'ASC',
'posts_per_page' => -1
));
foreach($all_posts->posts as $key => $value) {
if($value->ID == $post->ID){
$nextID = isset($all_posts->posts[$key + 1]) ? $all_posts->posts[$key + 1]->ID : null;
$prevID = isset($all_posts->posts[$key - 1]) ? $all_posts->posts[$key - 1]->ID : null;
break;
}
}
?>
<div class="project-nav-prev">
<?php if($prevID): ?>
<a href="<?= get_the_permalink($prevID) ?>" rel="prev"><span class="arrow">←</span> ПРЕДЫДУЩИЙ ПРОЕКТ </br><?= get_the_title($prevID) ?></a>
<?php endif; ?>
</div>
<div class="project-nav-next">
<?php if($nextID): ?>
<a href="<?= get_the_permalink($nextID) ?>" rel="next">СЛЕДУЮЩИЙ ПРОЕКТ <span class="arrow">→</span> </br><?= get_the_title($nextID) ?></a>
<?php endif; ?>
</div>

Я нашел гораздо более простой способ реализации навигации по записям на основе мета-ключа, без необходимости изменять functions.php.
Мой пример: У вас есть products.php и вы хотите переключаться между товарами. Предыдущий товар - следующий по дешевизне, следующий товар - следующий по дороговизне.
Вот мое решение для single.php:
<div class="post_navigation">
<?php
// Подготавливаем цикл
$args = array(
'post_type' => 'products',
'post_status' => 'publish',
'meta_key' => 'price',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'posts_per_page' => -1
);
query_posts($args);
// Инициализируем массив, в котором будут храниться ID всех товаров
$posts = array();
// ... и запускаем цикл
while ( have_posts() ) : the_post();
$posts[] += $post->ID;
endwhile;
// Сбрасываем запрос
wp_reset_query();
// Определяем позицию текущего товара в массиве $posts
$current = array_search(get_the_ID(), $posts);
// Получаем ID предыдущего товара
$prevID = $posts[$current-1];
// Получаем ID следующего товара
$nextID = $posts[$current+1];
// Ссылка "предыдущий товар"
if (!empty($prevID)) { ?>
<a href="/?p=<?php echo $prevID; ?>">предыдущий товар</a>
<?php }
// Ссылка "следующий товар"
if (!empty($nextID)) { ?>
<a href="/?p=<?php echo $nextID; ?>">следующий товар</a>
<?php } ?>

-10 за этот ответ. Как это может быть лучшим решением, если вы используете query_posts
, когда в кодексе указано, что его не следует применять.

@KentMiller, на странице кодекса есть информативная диаграмма, а также вам может быть полезен этот вопрос. Определенно стоит ознакомиться с этими соглашениями.
