I link Articolo Successivo/Precedente possono essere ordinati per ordine menu o per meta key?
Ho una serie di articoli che sono ordinati per un valore meta_key. Potrebbero anche essere disposti per ordine menu, se necessario.
I link articolo successivo/precedente (generati da next_post_link
, previous_post_link
, o posts_nav_link
) navigano tutti in ordine cronologico. Mentre capisco questo comportamento predefinito, non capisco come modificarlo. Ho scoperto che viene mappato attraverso adjacent_post_link in link-template.php, ma poi sembra piuttosto hard-coded. È consigliabile riscriverlo da zero per sostituirlo, o esiste una soluzione migliore.

Comprendere gli interni
L'"ordine" di "ordinamento" dei post adiacenti (successivo/precedente) non è realmente un "ordine" di ordinamento. Si tratta di una query separata per ogni richiesta/pagina, ma ordina la query in base al post_date
- o al genitore del post se si dispone di un post gerarchico come oggetto attualmente visualizzato.
Quando si esaminano gli interni di next_post_link()
, si vede che è sostanzialmente un wrapper API per adjacent_post_link()
. Quest'ultima funzione chiama internamente get_adjacent_post()
con l'argomento/flag $previous
impostato su bool(true|false)
per ottenere il link al post successivo o precedente.
Cosa filtrare?
Dopo aver approfondito, si vedrà che get_adjacent_post()
Link alla fonte dispone di alcuni utili filtri per il suo output (ovvero il risultato della query): (Nome Filtro/Argomenti)
"get_{$adjacent}_post_join"
$join // Solo se `$in_same_cat` // oppure: ! empty( $excluded_categories` // e poi: // " 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"; // e se $in_same_cat allora AGGIUNGE: // " 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 stringa vuota se $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"`
Quindi si può fare molto con esso. Si parte dal filtrare la clausola WHERE
, così come la tabella JOIN
e l'istruzione ORDER BY
.
Il risultato viene memorizzato nella cache per la richiesta corrente, quindi non aggiunge query aggiuntive se si chiama quella funzione più volte su una singola pagina.
Costruzione automatica della query
Come @StephenHarris ha sottolineato nei commenti, esiste una funzione di core che potrebbe tornare utile nella costruzione della query SQL: get_meta_sql()
- Esempi nel Codex. Fondamentalmente questa funzione viene utilizzata solo per costruire l'istruzione SQL meta che viene utilizzata in WP_Query
, ma in questo caso (o altri) è possibile utilizzarla anche. L'argomento che si passa è un array, lo stesso che si aggiungerebbe a un WP_Query
.
$meta_sql = get_meta_sql(
$meta_query,
'post',
$wpdb->posts,
'ID'
);
Il valore restituito è un array:
$sql => (array) 'join' => array(),
(array) 'where' => array()
Quindi è possibile utilizzare $sql['join']
e $sql['where']
nel callback.
Dipendenze da tenere a mente
Nel tuo caso la cosa più semplice sarebbe intercettarlo in un piccolo (mu)plugin o nel file functions.php del tema e modificarlo in base alla variabile $adjacent = $previous ? 'previous' : 'next';
e alla variabile $order = $previous ? 'DESC' : 'ASC';
:
I nomi effettivi dei filtri
Quindi i nomi dei filtri sono:
get_previous_post_join
,get_next_post_join
get_previous_post_where
,get_next_post_where
get_previous_post_sort
,get_next_post_sort
Racchiuso in un plugin
...e il callback del filtro sarebbe (ad esempio) qualcosa come il seguente:
<?php
/** Plugin Name: (#73190) Modifica l'ordine di ordinamento del link al post adiacente */
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 per informazione, (@magnakai) se stai facendo qualcosa di simile per le meta query, dai un'occhiata a get_meta_sql()

+1 a te @StephenHarris ! Non l'avevo mai visto prima. Breve domanda: Come ho letto dal codice sorgente, devi passare un oggetto query completamente qualificato, come faresti questo con i filtri menzionati sopra? Per quanto posso vedere vengono passate solo stringhe di query, dato che la query viene eseguita dopo i filtri.

no, $meta_query
è semplicemente l'array che passeresti a WP_Query
per l'argomento meta_query
: In questo esempio: $meta_sql = get_meta_sql( $meta_query, 'post', $wpdb->posts, 'ID');
- questo genera le parti JOIN
e WHERE
della query che dovrebbero essere aggiunte.

@StephenHarris, sto avendo problemi ad applicare l'output di get_meta_sql() - puoi aiutarmi a collegare i puntini?

@Magnakai è un array di $sql => (array) 'join' => array(), 'where' => array()
. Quindi semplicemente prendi $sql['join'];
o $sql['where']
.

La risposta di Kaiser è fantastica e approfondita, tuttavia modificare solo la clausola ORDER BY non è sufficiente a meno che il tuo menu_order
non corrisponda all'ordine cronologico.
Non posso prendermi il merito, ma ho trovato il seguente codice in questo gist:
<?php
/**
* Personalizza l'ordine dei link ai post adiacenti
*/
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' );
Ho modificato i nomi delle funzioni per WP.SE.
Se modifichi solo la clausola ORDER BY, la query continua a cercare post con date maggiori o minori della data del post corrente. Se i tuoi post non sono in ordine cronologico, non otterrai il post corretto.
Questo codice modifica la clausola WHERE per cercare post dove il menu_order è maggiore o minore del menu_order del post corrente, oltre a modificare la clausola orderby.
Inoltre, la clausola orderby non dovrebbe essere hardcoded per usare DESC poiché dovrà cambiare in base a se stai ottenendo il link al post successivo o precedente.

Una nota: La clausola WHERE
cerca il formato 'YYYY-mm-dd HH:mm:ss'
. Se questo formato non viene rispettato, non funzionerà. Poiché il valore non è impostato dal database, ma dall'Applicazione, dovrai verificare prima questo formato quando costruisci l'espressione regolare.

Se vuoi ordinare per post_title
puoi sostituire tutte le istanze di menu_order
nel codice sopra e dovrebbe funzionare bene. Fai attenzione però al secondo elemento nell'array $replacements
- ho dovuto racchiuderlo tra apici singoli per farlo funzionare, cioè $replacements[] = '\'' . $the_post->post_title . '\'';

Ho provato ad agganciarmi senza successo. Potrebbe essere solo un problema della mia configurazione, ma per coloro che non riescono a far funzionare l'hook, ecco la soluzione più semplice:
<?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; ?>

dopo alcune ore di tentativi per far funzionare get_previous_post_where
, get_previous_post_join
e get_previous_post_sort
con tipi di post personalizzati e un ordinamento complesso che include meta key, ho rinunciato e ho usato questo. Grazie!

Anche io, non solo volevo ordinare per Menu Order, ma anche cercare post con una specifica meta_key e meta_value, quindi questo era il metodo migliore. L'unica modifica che ho fatto è stata di racchiuderlo in una funzione.

@eballeste, se ti riferisci a ottenere il primo post quando sei sull'ultimo e l'ultimo quando sei sul primo, guarda la mia risposta qui sotto

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' );

Per quello che vale, ecco come puoi ordinare per menu_order
per un specifico custom post type:
/**
* Personalizza l'ordine dei post adiacenti
*/
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);
Spero che questo possa aiutare qualcun altro!

Basandomi sulla risposta di @Szabolcs Páll ho creato questa classe di utilità con metodi helper per ottenere i post di un tipo ordinati per menu order e per ottenere anche il post successivo e precedente in base all'ordine del menu. Ho inoltre aggiunto condizioni per verificare se il post corrente è il primo o l'ultimo post per ottenere rispettivamente l'ultimo o il primo post.
Ad esempio:
// $currentPost è il primo per menu order
getPreviousPostByMenuOrder($postType, $$currentPost->ID)
// restituisce => l'ultimo post per menu order
// $currentPost è l'ultimo per menu order
getPreviousPostByMenuOrder($postType, $$currentPost->ID)
// restituisce => il primo post per menu order
La classe 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;
}
}

Basandomi sulla risposta di @Szabolcs Páll e sul post di bbloomer su come aggiungere pulsanti next/prev nella pagina singola del prodotto WooCommerce, ho creato questo codice.
Ordina tutti i prodotti per meta key e aggiunge pulsanti prev/next sopra e sotto il prodotto.
(La meta key può essere anche un campo ACF!)
/**
* @snippet Aggiungi pulsanti next/prev ordinati per meta key o campo ACF @ Pagina Singola Prodotto WooCommerce
* @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');
// e se li vuoi anche in fondo...
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', // <-- MODIFICA QUESTO
'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__('Prodotto precedente') ?></a>
<?php endif; ?>
<?php if ($nextID) : ?>
<a href="<?= get_the_permalink($nextID) ?>" rel="next" class="next" title="<?= get_the_title($nextID) ?>"><?= esc_attr__('Prodotto successivo') ?></a>
<?php endif; ?>
<?php
echo '</div>';
}
Se vuoi il file scss aggiuntivo che ho usato: _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: " ";
}
}
}
}

Grazie per le preziose informazioni. Le ho utilizzate con il mio tipo di post personalizzato che ha anche un campo data personalizzato. Ha funzionato perfettamente su WordPress 5.9.3. Posso anche confermare che ha funzionato con i tipi di post e i campi personalizzati che sono stati creati utilizzando plugin di terze parti come ACF.

Nessuna delle risposte elencate qui o su Internet in generale che ho potuto trovare al momento della stesura sembrava offrire una soluzione ragionevolmente semplice/elegante per presentare i link Post Successivo/Precedente ordinati per meta key. Questa soluzione funziona bene per me ed è facile da adattare. Buon utilizzo!
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;
/* Inserisci il tuo post type -> */
$post_type = "_my_post_type_";
if ($post_type == $post->post_type){
global $wpdb;
$show_private = current_user_can( 'read_private_pages', $post->ID );
/* Inserisci il nome della tua meta key personalizzata -> */
$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 subquery annidata aggira le attuali limitazioni di mysql/mariadb */
$where = "WHERE p.post_type = '$post_type' AND p.ID IN (SELECT * FROM $sub_query as sq)";
}
return $where;
}

Nel tuo codice ci sono riferimenti a tabelle con prefisso, ad esempio kc_wppostmeta
e kc_wpposts
. La best practice è utilizzare l'oggetto globale $wpdb
per specificare queste tabelle, ad esempio $wpdb->posts
invece di kc_wpposts
.
Penso inoltre valga la pena notare che questo presuppone che tu stia ottenendo contenuti pubblicati pubblicamente o privatamente nella tua sub-query. C'è un'intera sezione di condizioni che gestisce questo aspetto nel codice core: https://github.com/WordPress/wordpress-develop/blob/8338c630284124bbe79dc871822d6767e3b45f0b/src/wp-includes/link-template.php#L1893

Trovo questo piccolo plugin davvero utile: http://wordpress.org/plugins/wp-query-powered-adjacent-post-link/
WP_Query Powered Adjacent Post Link è un plugin per sviluppatori. Aggiunge la funzione
wpqpapl();
a WordPress che può restituire informazioni sul post precedente e successivo a quello corrente. Accetta argomenti per l'utilizzo nella classeWP_Query
.

Questo ha funzionato per me:
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 )";
}

Anch'io ho avuto problemi con questo. Magicamente sono riuscito a farlo funzionare così:
- Assicurati che il tuo post type sia impostato per avere post gerarchici.
- Poi usa il semplice plugin per l'ordinamento dei custom post type.
https://wordpress.org/plugins/simple-custom-post-order/
Oltre a permettere un facile ordinamento con drag and drop, questo plugin assicura che prev e next funzionino in base all'ordine del menu. Sfortunatamente potrebbe funzionare nell'ordine sbagliato con DSC invece di ASC. Per risolvere possiamo creare una funzione di navigazione inversa tra i post. - Usa una funzione di navigazione inversa invece. Ne ho trovata una su gist. (aggiungi al file functions del tuo tema) https://gist.github.com/jaredchu/3e3bcb866240d1d32a3b4ae55905b135#file-the_reverse_post_navigation
E non ho dovuto scrivere nemmeno una riga di codice :)

Ho modificato il codice di Szabolcs Páll sopra per ordinare secondo una meta_key personalizzata e all'interno di una categoria specifica, ma ho anche cercato di aggiungere delle condizioni per i primi e gli ultimi post.
Sui primi e sugli ultimi post non mostrava il link corretto per il successivo/precedente con il codice originale, mostrando solo un link per l'ID del post corrente su cui mi trovavo.
Quello che segue ha funzionato per me, ma non sono sicuro se ci siano potenziali problemi (non sono un programmatore molto avanzato).
<?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> PROGETTO PRECEDENTE </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">PROGETTO SUCCESSIVO <span class="arrow">→</span> </br><?= get_the_title($nextID) ?></a>
<?php endif; ?>
</div>

Ho trovato un modo molto più semplice per implementare una navigazione tra post basata su meta-key, senza la necessità di modificare functions.php.
Il mio esempio: hai un products.php e vuoi navigare tra i prodotti. Il prodotto precedente è quello più economico, il prodotto successivo è quello più costoso.
Ecco la mia soluzione per single.php:
<div class="post_navigation">
<?php
// Prepara il loop
$args = (
'post_type' => 'products',
'post_status' => 'publish',
'meta_key' => 'price',
'orderby' => 'meta_value_num',
'order' => 'ASC',
'posts_per_page' => -1
);
query_posts($args);
// Inizializza l'array in cui verranno memorizzati gli ID di TUTTI i post prodotti
$posts = array();
// ... e ora avviamo il loop
while ( have_posts() ) : the_post();
$posts[] += $post->ID;
endwhile;
// Resetta la Query
wp_reset_query();
// Identifica la posizione del prodotto corrente nell'array $posts
$current = array_search(get_the_ID(), $posts);
// Identifica l'ID del prodotto precedente
$prevID = $posts[$current-1];
// Identifica l'ID del prodotto successivo
$nextID = $posts[$current+1];
// Link "prodotto precedente"
if (!empty($prevID)) { ?>
<a href="/?p=<?php echo $prevID; ?>">prodotto precedente</a>
<?php }
// Link "prodotto successivo"
if (!empty($nextID)) { ?>
<a href="/?p=<?php echo $nextID; ?>">prodotto successivo</a>
<?php } ?>

-10 per questa risposta. Come può essere una soluzione migliore se stai usando query_posts
quando il codex afferma che non dovrebbe essere utilizzato.

@KentMiller, c'è un diagramma informativo nella pagina del codex, e potresti anche trovare utile questa domanda. Vale davvero la pena familiarizzare con queste convenzioni.
