¿Pueden los enlaces Next/Prev Post ordenarse por orden de menú o por meta key?
Tengo una serie de entradas que están ordenadas por un valor meta_key. También podrían organizarse por orden de menú, si fuera necesario.
Los enlaces de entrada siguiente/anterior (generados por next_post_link
, previous_post_link
, o posts_nav_link
) todos navegan por cronología. Si bien entiendo este comportamiento predeterminado, no entiendo cómo cambiarlo. Descubrí que se conecta a adjacent_post_link en link-template.php, pero luego parece estar bastante codificado de forma rígida. ¿Se recomienda reescribir esto desde cero para reemplazarlo, o hay una mejor solución?
Entendiendo los internos
El "orden" de clasificación de las publicaciones adyacentes (siguiente/anterior) no es realmente un "orden" de clasificación. Es una consulta separada en cada solicitud/página, pero ordena la consulta por post_date
- o por el padre de la publicación si tienes una publicación jerárquica como objeto mostrado actualmente.
Cuando echas un vistazo a los internos de next_post_link()
, verás que básicamente es un envoltorio de API para adjacent_post_link()
. Esta última función llama internamente a get_adjacent_post()
con el argumento/bandera $previous
establecido en bool(true|false)
para obtener el enlace de la publicación siguiente o anterior.
¿Qué filtrar?
Después de profundizar en ello, verás que get_adjacent_post()
Enlace fuente tiene algunos filtros interesantes para su salida (también conocido como resultado de consulta): (Nombre/Argumentos del Filtro)
"get_{$adjacent}_post_join"
$join // Solo si `$in_same_cat` // o: ! empty( $excluded_categories` // y luego: // " 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"; // y si $in_same_cat entonces AGREGA: // " 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, ',') . ')'; // O cadena vacía si $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"`
Así que puedes hacer mucho con esto. Eso comienza con filtrar la cláusula WHERE
, así como la tabla JOIN
y la declaración ORDER BY
.
El resultado se almacena en caché en la memoria para la solicitud actual, por lo que no agrega consultas adicionales si llamas a esa función varias veces en una sola página.
Construcción automática de consultas
Como @StephenHarris señaló en los comentarios, hay una función central que podría ser útil al construir la consulta SQL: get_meta_sql()
- Ejemplos en el Codex. Básicamente, esta función solo se usa para construir la declaración SQL de metadatos que se usa en WP_Query
, pero puedes usarla en este caso (u otros) también. El argumento que le pasas es una matriz, exactamente la misma que agregarías a un WP_Query
.
$meta_sql = get_meta_sql(
$meta_query,
'post',
$wpdb->posts,
'ID'
);
El valor de retorno es una matriz:
$sql => (array) 'join' => array(),
(array) 'where' => array()
Así que puedes usar $sql['join']
y $sql['where']
en tu devolución de llamada.
Dependencias a tener en cuenta
En tu caso, lo más fácil sería interceptarlo en un pequeño plugin (mu) o en el archivo functions.php de tu tema y modificarlo dependiendo de la variable $adjacent = $previous ? 'previous' : 'next';
y la variable $order = $previous ? 'DESC' : 'ASC';
:
Los nombres reales de los filtros
Así que los nombres de los filtros son:
get_previous_post_join
,get_next_post_join
get_previous_post_where
,get_next_post_where
get_previous_post_sort
,get_next_post_sort
Envuelto como un plugin
...y la devolución de llamada del filtro sería (por ejemplo) algo como lo siguiente:
<?php
/** Plugin Name: (#73190) Alterar el orden de clasificación de enlaces de publicaciones adyacentes */
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. Solo para información, (@magnakai) si vas a hacer algo como esto para metaqueries, echa un vistazo a get_meta_sql()

+1 para ti @StephenHarris ! No había visto esto antes. Pregunta rápida: Según leo en el código fuente, tienes que pasar un objeto de consulta completamente calificado, ¿cómo harías esto con los filtros mencionados anteriormente? Hasta donde puedo ver, solo se pasan cadenas de consulta, ya que la consulta se ejecuta después de los filtros.

no, $meta_query
es simplemente el array que pasarías a WP_Query
para el argumento meta_query
: En este ejemplo: $meta_sql = get_meta_sql( $meta_query, 'post', $wpdb->posts, 'ID');
- esto genera las partes JOIN
y WHERE
de la consulta que necesitarían ser añadidas.

@StephenHarris, estoy teniendo problemas para aplicar el resultado de get_meta_sql() - ¿puedes ayudarme a conectar los puntos?

@Magnakai es un array de $sql => (array) 'join' => array(), 'where' => array()
. Así que simplemente toma $sql['join'];
o $sql['where']
.

La respuesta de Kaiser es increíble y detallada, sin embargo, simplemente cambiar la cláusula ORDER BY no es suficiente a menos que tu menu_order
coincida con tu orden cronológico.
No puedo atribuirme el crédito por esto, pero encontré el siguiente código en este gist:
<?php
/**
* Personalizar el orden de los enlaces de posts adyacentes
*/
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' );
He modificado los nombres de las funciones para WP.SE.
Si solo cambias la cláusula ORDER BY, la consulta sigue buscando posts mayores o menores que la fecha actual del post. Si tus posts no están en orden cronológico, no obtendrás el post correcto.
Esto cambia la cláusula WHERE para buscar posts donde el menu_order sea mayor o menor que el menu_order del post actual, además de modificar la cláusula ORDER BY.
La cláusula ORDER BY tampoco debería estar codificada para usar DESC, ya que necesitará cambiar según si estás obteniendo el enlace del siguiente o del anterior post.

Una nota: La cláusula WHERE
busca el formato 'YYYY-mm-dd HH:mm:ss'
. Si no se cumple ese formato, no funcionará. Como el valor no lo establece la base de datos, sino la aplicación, tendrás que verificar primero ese formato al construir la expresión regular.

Si deseas ordenar por post_title
, puedes reemplazar todas las instancias de menu_order
en el código anterior y debería funcionar bien. Pero ten cuidado con el segundo elemento del array $replacements
- tuve que envolverlo entre comillas simples para que funcione, es decir: $replacements[] = '\'' . $the_post->post_title . '\'';

Intenté engancharlo sin éxito. Puede que sea solo un problema de mi configuración, pero para aquellos que no pueden hacer que el hook funcione, aquí está la solución más simple:
<?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; ?>

después de unas horas intentando que get_previous_post_where
, get_previous_post_join
y get_previous_post_sort
funcionen correctamente con tipos de posts personalizados y ordenamientos complejos que incluyen meta keys, me rendí y usé esto. ¡Gracias!

Lo mismo aquí, no solo quería ordenar por Menu Order, sino también buscar posts con un meta_key y meta_value específicos, por lo que este fue el mejor método. El único cambio que hice fue envolverlo en una función.

@eballeste, si te refieres a obtener la primera publicación cuando estás en la última y la última cuando estás en la primera, mira mi respuesta a continuación

/**
* Modifica la consulta SQL para ordenar posts adyacentes por menu_order en lugar de post_date
*
* @param string $sql La consulta SQL original
* @return string La consulta SQL modificada
*/
function wpse73190_gist_adjacent_post_sort( $sql ) {
$pattern = '/post_date/';
$replacement = 'menu_order';
return preg_replace( $pattern, $replacement, $sql );
}
// Aplicar el filtro tanto para el post siguiente como para el anterior
add_filter( 'get_next_post_sort', 'wpse73190_gist_adjacent_post_sort' );
add_filter( 'get_previous_post_sort', 'wpse73190_gist_adjacent_post_sort' );

Por si sirve de ayuda, aquí está cómo puedes ordenar por menu_order
para un tipo de entrada personalizado específico:
/**
* Personalizar el orden de las entradas adyacentes
*/
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);
¡Espero que esto le sea útil a alguien más!

Basado en la respuesta de @Szabolcs Páll, he creado esta clase de utilidad con métodos auxiliares para poder obtener publicaciones de un tipo por orden de menú y también obtener la publicación siguiente y anterior por orden de menú. Además, he añadido condiciones para verificar si la publicación actual es la primera o la última, para así obtener la última o primera publicación respectivamente.
Por ejemplo:
// $currentPost es la primera por orden de menú
getPreviousPostByMenuOrder($postType, $$currentPost->ID)
// devuelve => última publicación por orden de menú
// $currentPost es la última por orden de menú
getPreviousPostByMenuOrder($postType, $$currentPost->ID)
// devuelve => primera publicación por orden de menú
La clase completa:
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;
}
}

Basado en la respuesta de @Szabolcs Páll y el post de bbloomer sobre agregar botones de anterior/siguiente en la página de producto único de WooCommerce, creé este código.
Ordena todos los productos por clave meta y agrega botones de anterior/siguiente arriba + abajo del producto.
(¡La clave meta puede ser también un campo ACF!)
/**
* @snippet Agregar botones anterior/siguiente ordenados por clave meta o campo ACF @ WooCommerce Página de Producto Único
* @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');
// y si también los quieres al final...
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', // <-- CAMBIA ESTO
'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__('Producto anterior') ?></a>
<?php endif; ?>
<?php if ($nextID) : ?>
<a href="<?= get_the_permalink($nextID) ?>" rel="next" class="next" title="<?= get_the_title($nextID) ?>"><?= esc_attr__('Producto siguiente') ?></a>
<?php endif; ?>
<?php
echo '</div>';
}
Si quieres el archivo scss adicional que usé: _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: " ";
}
}
}
}

Gracias por la valiosa información. Lo he usado con mi tipo de publicación personalizado que también tiene un campo de fecha personalizado. Funcionó perfectamente en WordPress 5.9.3. También puedo confirmar que funcionó con los tipos de publicaciones personalizados y campos que fueron creados usando plugins de terceros como ACF.

Ninguna de las respuestas listadas aquí o en internet en general que pude encontrar al momento de escribir esto parecía ofrecer una solución razonablemente simple/elegante para presentar los enlaces de Siguiente/Anterior Post ordenados por meta key. Esto funciona bien para mí y es fácil de adaptar. ¡Disfrútalo!
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;
/* Ingresa tu tipo de post -> */
$post_type = "_my_post_type_";
if ($post_type == $post->post_type){
global $wpdb;
$show_private = current_user_can( 'read_private_pages', $post->ID );
/* Ingresa el nombre de tu meta key personalizada -> */
$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)";
/* La subconsulta anidada soluciona limitaciones actuales de mysql/mariadb */
$where = "WHERE p.post_type = '$post_type' AND p.ID IN (SELECT * FROM $sub_query as sq)";
}
return $where;
}

Tienes algunas referencias a tablas con prefijos en tu código, por ejemplo kc_wppostmeta
y kc_wpposts
. La mejor práctica es usar el objeto global $wpdb
para especificar estas tablas, por ejemplo $wpdb->posts
en lugar de kc_wpposts
.
También creo que vale la pena mencionar que esto asume que estás obteniendo contenido publicado públicamente o de forma privada en tu subconsulta. Hay una sección completa de condiciones que manejan esto en el código principal: https://github.com/WordPress/wordpress-develop/blob/8338c630284124bbe79dc871822d6767e3b45f0b/src/wp-includes/link-template.php#L1893

Este pequeño plugin me resulta muy útil: http://wordpress.org/plugins/wp-query-powered-adjacent-post-link/
WP_Query Powered Adjacent Post Link es un plugin para desarrolladores. Añade la función
wpqpapl();
a WordPress, la cual puede devolver información sobre el post anterior y siguiente al actual. Acepta argumentos para ser usados en la claseWP_Query
.

Esto funcionó para mí:
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 )";
}

Yo también tuve problemas con esto. Mágicamente logré que funcionara de esta manera:
- Asegúrate de que tu tipo de contenido (post type) esté configurado para tener posts jerárquicos.
- Luego usa el plugin simple custom post type order.
https://wordpress.org/plugins/simple-custom-post-order/
Además de permitir ordenar fácilmente con arrastrar y soltar, este plugin asegura que prev y next funcionen según el orden del menú. Desafortunadamente podría funcionar en orden inverso con DSC en lugar de ASC. Para solucionar esto podemos crear una función de navegación inversa entre posts. - Usa una función de navegación inversa. Encontré una en gist. (agrégala al archivo functions.php de tu tema) https://gist.github.com/jaredchu/3e3bcb866240d1d32a3b4ae55905b135#file-the_reverse_post_navigation
Y no tuve que escribir ni una sola línea de código yo mismo :)

Edite el código de Szabolcs Páll mencionado anteriormente para ordenar por una meta_key personalizada y dentro de una categoría específica, pero también para intentar agregar condicionales para el primer y último post.
En el primer y último post no mostraba el enlace siguiente/anterior correcto con el código original, solo mostraba un enlace para el ID de post actual en el que me encontraba.
Esto funcionó para mí como se muestra a continuación, pero no estoy seguro si hay posibles problemas con ello. (No soy el programador más avanzado)
<?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> PROYECTO ANTERIOR </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">SIGUIENTE PROYECTO <span class="arrow">→</span> </br><?= get_the_title($nextID) ?></a>
<?php endif; ?>
</div>

He encontrado una forma mucho más fácil de lograr una navegación entre publicaciones basada en meta-claves, sin necesidad de modificar functions.php.
Mi ejemplo: Tienes un products.php y quieres navegar entre productos. El producto anterior es el siguiente más barato, el siguiente producto es el siguiente más caro.
Aquí está mi solución para single.php:
<div class="post_navigation">
<?php
// Preparar el loop
$args = array(
'post_type' => 'products',
'post_status' => 'publish',
'meta_key' => 'price',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'posts_per_page' => -1
);
query_posts($args);
// Inicializar array donde se almacenarán los IDs de TODAS las publicaciones de productos
$posts = array();
// ... y ahora iniciemos el loop
while ( have_posts() ) : the_post();
$posts[] += $post->ID;
endwhile;
// Resetear Query
wp_reset_query();
// Identificar la posición del producto actual dentro del array $posts
$current = array_search(get_the_ID(), $posts);
// Identificar ID del producto anterior
$prevID = $posts[$current-1];
// Identificar ID del siguiente producto
$nextID = $posts[$current+1];
// Enlace "producto anterior"
if (!empty($prevID)) { ?>
<a href="/?p=<?php echo $prevID; ?>">producto anterior</a>
<?php }
// Enlace "siguiente producto"
if (!empty($nextID)) { ?>
<a href="/?p=<?php echo $nextID; ?>">siguiente producto</a>
<?php } ?>

-10 por esta respuesta. ¿Cómo puede ser una mejor solución si estás usando query_posts
cuando el codex establece que no debería usarse?

@KentMiller, hay un diagrama informativo en la página del codex, y también puede encontrar esta pregunta bastante útil. Vale la pena familiarizarse con estas convenciones.
