Améliorer les sticky posts

Aujourd’hui je vous propose de découvrir quelques astuces autour d’une fonctionnalité de WordPress trop rarement exploitée : les sticky posts, ou articles à la Une.

Que sont les sticky posts ?

Il s’agit d’un système apparu dans WordPress 2.7 et qui permet de charger les contenus importants de son site en début de liste sur la page de blog. Cela fonctionne un peu de la même façon que lorsqu’on épingle un tweet sur son profil Twitter…

Plus concrètement, il s’agit de cocher une case dans la fenêtre d’édition des articles qu’on souhaite mettre en Une. Cette liste est enregistrée dans une option du site. Lors de la requête principale de la première page du blog, WordPress exécute une sous-requête pour ajouter les sticky posts devant la liste des articles (#inception).

On peut aussi jouer avec les articles à la une directement dans une WP_Query grâce à des paramètres comme 'post__in' => get_option( 'sticky_posts', array() ), ou 'ignore_sticky_posts' => 1,…

Des sticky Custom Post Types ?

Par défaut, cette fonctionnalité est limitée aux articles… cependant il existe de nombreux cas où l’on aimerait en faire bénéficier d’autre types de contenus.

Par exemple :

  • si vous souhaitez appeler les sticky de CPT pour faire des diaporamas en page d’accueil ;
  • si vous souhaitez exploiter un CPT à la place des articles dans vos archives de blogs…

Le hic, c’est que l’option des sticky posts est codée de façon assez spécifique : l’input a cocher est inscrit « en dur » dans la metabox de publication des articles (pas de hook pour ajouter directement la checkbox a cet endroit), mais la valeur est sauvegardée dans une option (au lieu d’une metadonnée) sans trop de vérification.

Pour ajouter la fonctionnalité sticky, il faut donc juste se débrouiller pour ajouter la checkbox !

Nous n’avons pas de hook, alors on va faire « un tunnel entre deux hooks d’actions » pour ajouter le champ où l’on souhaite (une astuce à base de ob_start ,  ob_get_clean et de regex 😉).

  1. Le premier pavé de code est une fonction qui vérifie qu’on peut ajouter l’option : on vérifie les droits de l’utilisateur et on choisit les types de contenus souhaités.
  2. Ensuite je démarre le tampon de sortie PHP sur le hook post_submitbox_minor_actions : plus rien ne sera écrit dans la page metabox à partir de ob_start()
  3. … jusqu’à ce que l’on stop le tampon ligne 25 avec ob_get_clean(). La variable $temp  récupère ce qui avait été écrit par PHP dans la metabox ; on peut alors y injecter nos checkboxes en ciblant l’endroit voulu grâce à une expression rationnelle.
// Does the current user can define sticky posts for this custom post type?
function need_sticky( $post ) {
    $post_type = $post->post_type;
    $post_type_object = get_post_type_object( $post_type );
    $can_publish = current_user_can( $post_type_object->cap->publish_posts );
    return $can_publish 
      // Choose your post types
      && in_array( $post->post_type, array( 'page', 'event' ) );
}

// Start deferring php output
add_action( 'post_submitbox_minor_actions', 'w_post_submitbox_minor_actions' );
function w_post_submitbox_minor_actions( $post ) {
    if ( need_sticky( $post ) ) {
        ob_start();
    }
}

// Get PHP output and print sticky posts metabox
add_action( 'post_submitbox_misc_actions', 'w_post_submitbox_misc_actions' );
function w_post_submitbox_misc_actions( $post ) {
    if ( need_sticky( $post ) ) {
        $temp = ob_get_clean();

        // WP core need two different checkbox: on for authors, and another one for editors
        $sticky_box = '<input type="checkbox" style="display:none" 
            name="hidden_post_sticky" id="hidden-post-sticky" value="sticky" '
            . checked( is_sticky( $post->ID ), true, false ) . ' />';

        if ( current_user_can( 'edit_others_posts' ) ) {
            $sticky_box .= sprintf( '<span id="sticky-span"><input id="super-sticky" 
                name="sticky" type="checkbox" value="sticky" %1$s />
                <label for="super-sticky" class="selectit">%2$s</label><br/></span>',
                checked( is_sticky( $post->ID ), true, false ),
                __( 'Stick this post to the front page' ) );
        }

        $re = '/(<input.*id="visibility-radio-password")/U';
        $output = preg_replace( $re, "{$sticky_box}$1", $temp );
        echo $output;
    }
}

Et voilà ! On peut maintenant épingler nos types de contenus personnalisés 😀

Vous pouvez utiliser la fonctionnalité en appelant directement les contenus à la une dans vos WP_Query(). Si vous souhaitez utiliser le système en natif, il faut juste pensez à autoriser vos types de contenu à apparaitre dans la requête de la page des articles, avec un code du genre :

add_filter( 'pre_get_posts', 'ok_for_cpt_on_sticky' );
function ok_for_cpt_on_sticky( $q ) {
	// we target sticky_post sub request in class-wp-query.php
	$stickies = get_option( 'sticky_posts' );
	if ( $stickies // have_stickies
	  && is_home() // … in home page
	  && $q->get( 'suppress_filters' ) // is get_posts()
	  && $stickies === $q->get( 'post__in' ) ) { //only stikies queried
		$q->set( 'post_type', array( 'page', 'event', 'post' ) );
	}
	return $q;
}
Pirouette

Gérer les sticky_posts avec Advanced Custom Fields

Pour des utilisateurs néophytes, je trouve que le système pour choisir les articles à la une est très discret ; on peut le chercher longtemps sans le trouver 😕

Il m’arrive donc d’utiliser le plugin Advanced Custom Fields pour gérer l’ensemble des sticky_posts sur une seule et même interface. Je créer un champs de type « sélecteur d’articles multiple » et je le place sur une page d’option de l’administration, ou bien directement dans une metabox de la page « racine du blog ».

Sélectionnez les sticky posts à partir de la page du blog

Enfin, je synchronise la lecture et l’enregistrement des valeurs de ce champ sur notre option sticky_posts, avec les snippets ci-dessous.

Si on passe par une metabox de page :

/**
 * If ACF field `sticky_posts` is a page_for_posts metabox
 */
add_filter( 'get_post_metadata', 'w_get_sticky_post_meta', 10, 3 );
function w_get_sticky_post_meta( $check, $object_id, $meta_key ) {
    if ( get_option( 'page_for_posts' ) == $object_id
      && 'sticky_posts' === $meta_key ) {
        return array( get_option( 'sticky_posts' ) );
    }
    return $check;
}

add_action( 'updated_post_meta', 'w_update_sticky_post_meta', 10, 4 );
function w_update_sticky_post_meta( $meta_id, $object_id, $meta_key, $_meta_value ) {
    if ( get_option( 'page_for_posts' ) == $object_id
      && 'sticky_posts' === $meta_key ) {
        update_option( 'sticky_posts', $_meta_value );
    }
}

… ou si on choisit plutôt un champ sur une page d’option :

/**
 * If ACF field `sticky_posts` is on an option page
 */
add_filter( 'pre_option_options_sticky_posts', 'w_get_sticky' );
function w_get_sticky() {
    return get_option( 'sticky_posts' );
}

add_action( 'update_option', 'w_update_sticky', 10, 3 );
function w_update_sticky( $option, $old_value, $value ) {
    if ( 'options_sticky_posts' === $option ) {
        update_option( 'sticky_posts', $value );
    }
}

Et bien sûr, l’ensemble de ces interfaces sont synchronisées ! On peut donc mettre à la fois la checkbox, la page de réglage, et faire une metabox sur la page du blog.

Modification du nombre d’articles par page

Pour finir, il y a une dernière chose qui me dérange avec la façon dont sont implémentés les sticky posts dans WordPress : elle ne tient pas compte de la pagination.

Par exemple si vous avez dix articles par page et quatre article fixés, ce sont quatorze éléments qui apparaitront sur votre home page. Selon la mise en page de votre thème cela peut être gênant…

Je vous propose de créer une « variable de requête personnalisée » qui va nous servir pour indiquer à WordPress que l’on veut que le nombre d’articles par page tienne compte des sticky posts :

// Add a custom query var to force post count
add_filter( 'query_vars', 'sticky_offset_query_var' );
function sticky_offset_query_var( $vars ) {
    $vars[] = 'ppp_include_sticky';
    return $vars;
}

// Add the query var on home page query
add_filter( 'pre_get_posts', 'w_sticky_posts' );
function w_sticky_posts($q) {
    if ( is_home() ) {
        $q->set('ppp_include_sticky',true);
    }
    return $q;
}

Grâce à cette variable on va pouvoir indiquer, juste après que WP_Query ai ajoutée les stickies, que l’on souhaite au maximum autant d’articles que ce qui a été défini par posts_per_page.

// Truncate posts array to adjust post count
add_filter( 'the_posts', 'sticky_count_offset', 10, 2 );
function sticky_count_offset( $posts, $q ) {
    if ( $q->get( 'ppp_include_sticky' )
      && 0 < $q->get( 'posts_per_page' )
      && false == $q->get( 'ignore_sticky_posts' ) ) {
        $posts = array_slice( $posts, 0, $q->get( 'posts_per_page' ) );
    }
    return $posts;
}

Notre page d’accueil contient maintenant exactement le nombre d’éléments souhaités.

Il reste cependant un dernier détail : lorsqu’on accède à la seconde page d’archive du blog, les articles tronqués par le code précédent ont disparus ! Pour ajuster ça, on va jouer sur le décalage des articles…

  1. La première fonction nous sert à tester qu’on est sur une home paginée ;
  2. si tel est le cas alors on s’assure que la requête ne renverra aucun article à la une ;
  3. on décale l’offset du nombre d’article ayant été tronqué, auquel on ajoute la page courante (l’usage d’offset fait sauter la pagination native) ;
  4. enfin on indique à WordPress d’inclure les éléments « fixés » dans le nombre d’articles trouvés (afin de ne pas fausser le nombre de pages total.
// Are we on a paged home with sticky posts ?
function is_paged_home_with_sticky( $q ) {
    return $q->is_home()
      && $q->get( 'ppp_include_sticky' )
      && $q->is_paged()
      && false == $q->get( 'ignore_sticky_posts' );
}

// Define page offset to show posts truncated on previous page
add_filter( 'pre_get_posts', 'sticky_count_on_paged', 20 );
function sticky_count_on_paged( $q ) {
    if ( is_paged_home_with_sticky( $q ) ) {
        $q->set('post__not_in', get_option( 'sticky_posts', array() ) );
        
        $offset = count( get_option( 'sticky_posts', array() ) );
        $page_offset = $q->get( 'posts_per_page', get_option( 'posts_per_page' ) ) 
          * ( $q->get( 'paged' ) -1 ) - $offset;
        $q->set( 'offset', $page_offset );
    }
    return $q;
}

// Adjust found_posts property to display the right number of pages
add_filter( 'found_posts', 'sticky_adjust_offset_pagination', 1, 2 );
function sticky_adjust_offset_pagination( $found_posts, $q ) {
    if ( is_paged_home_with_sticky( $q ) ) {
        $offset = count( get_option( 'sticky_posts', array() ) );
        return $found_posts + $offset;
    }
    return $found_posts;
}

C’est tout pour ce tutoriel, j’espère qu’il vous sera utile. N’hésitez pas à partager vos impressions en commentaires ou sur les réseaux Twitter/Facebook 😬

Soyez le premier à commenter !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Publié le 25 avril 2017
par Willy Bahuaud
Catégorie Développement WordPress