Des pages de présentation pour vos archives

Les custom post types de WordPress sont une fonctionnalité qui nous permet de créer, via des thèmes ou des plugins, de nouveaux types de contenu. Ces contenus sont accessibles sur le site via des pages de taxonomie, d’archives ou singulières.

Ma grosse frustration est de ne pas pouvoir nativement ajouter d’introductions sur les pages d’archives des Custom Post Types, comme on peut le faire avec un terme sur une page de taxonomie.

L’objet de cet article est de voir comment créer des pages de présentation d’archives, et comment les exploiter à travers nos templates.

page description archive wordpress
Une page pour décrire vos archives de Custom Post Type

Une metabox pour associer une page et une archive

Pour lier une page de description et une archive, nous allons concevoir une metabox. Elle nous permettra, dans le menu d’édition des pages, d’indiquer que telle page sert à la présentation de tel custom post type. La meta donnée qui fera la liaison aura pour clef _archive_page.

Il faut, pour commencer, ajouter une nouvelle meta box en copiant le code suivant dans le fichier functions.php de votre thème.

// ajout de la metabox
add_action( 'admin_init', 'my_admin_init' );
function my_admin_init(){
    add_meta_box("desc_page", "Archive à présenter", "archive_page", "page", "side", "high");
}

Nous venons d’ajouter une nouvelle boîte, en haut à droite du menu d’administration des pages.

Construire la boîte qui liste les types de contenus

Pour l’instant cette boîte fait tout planter, car nous n’avons pas encore créer la fonction « archive_page » dont elle a besoin pour se construire.

Juste en dessous du code précédent, on ajoute cette nouvelle fonction.

//fonction de la metabox
function archive_page( $post ) {
    $archive_page = get_post_meta( $post->ID, '_archive_page', true );
    ?>
    <select name="archive_page">
        <option value="">Aucune</option>
        <?php
            $post_types = get_post_types( array( 'show_ui' => true, '_builtin' => false ) );
            foreach( $post_types as $post_type )
                echo '<option value="' . esc_attr( $post_type ) . '" ' . selected( $post_type, $archive_page, false ) . '>' . esc_html( $post_type ) . '</option>'; 
        ?>
    </select>
    <p>Choisissez la cible de cette page</p>
    
    <?php
    wp_nonce_field( 'archive_page-save_' . $post->ID, 'archive_page-nonce') ;
}

Cette fonction liste les custom post types de WordPress dans un sélecteur.

Dans get_post_types, les options show_ui et _builtin servent à préciser que l’on ne souhaite pas ressortir les post types natifs de WordPress, ni les types de contenus privés.

Il ne faut pas oublier de rajouter une option « Aucune » avec une valeur vide, pour les pages normales.

La fonction selected servira, après la sauvegarde, à réassigner la valeur enregistrée dans _archive_page à l’option du select correspondante.

Sauvegarder la liaison entre la page et le custom post type

Pour finir la metabox, il ne nous reste plus qu’à sauvegarder la metadonnée.

//sauvegarde de la metabox
add_action( 'save_post', 'my_save_post' ); 
function my_save_post( $post_ID ){ 
    // on retourne rien du tout s'il s'agit d'une sauvegarde automatique
    if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE )
        return $post_ID;
        
    if ( isset( $_POST[ 'archive_page' ] ) ) {
        check_admin_referer( 'archive_page-save_' . $_POST[ 'post_ID' ], 'archive_page-nonce' );

        if( isset( $_POST[ 'archive_page' ] ) ) {
            $target = $_POST[ 'archive_page' ];
            global $wpdb;
            $suppr = $wpdb->get_results( $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_archive_page' AND meta_value = '%s'"), $target );
            foreach( $suppr as $s )

On ajoute, grâce au hook save_post, une fonction au moment de la sauvegarde de la page.

S’il s’agit d’une sauvegarde automatique, ou si le nonce n’est pas vérifié on arrête tout… sinon on continue.

La sauvegarde de cette metabox est un peu particulière car il ne faut pas que deux pages présentent le même type de contenu.

On commence donc par rechercher dans notre base de données toutes les autres pages associées à la même archive, et on supprime leur meta _archive_page.

Il ne reste plus qu’à sauvegarder notre meta.

La fonction qui affiche la présentation

Nous avons maintenant tout ce qu’il faut pour afficher le contenu de la page de description sur la page d’archive. Y’a plus qu’à…

Même si vous pouvez directement faire une requête dans vos templates d’archive, je vous conseille plutôt de créer une fonction dans functions.php, et de l’exécuter dans ces templates.

Voici le code :

//présentation de l'archive
function presentation_archive() {
    $post_type_obj = get_queried_object();
    $target = $post_type_obj->name;
    $presentation = new WP_Query( array(
        'post_type' => 'page',
        'meta_query' => array(
            array(
                'key' => '_archive_page',
                'value' => $target,
                'compare' => '='
                )
            )
        ) );
    if( $presentation->have_posts() ) : $presentation->the_post();
        the_title( '<h1 class="h1">', '</h1>' );
        echo '<div class="article-elem">';
        echo the_content();
        echo '</div>';
    endif;
}

On commence par récupérer l’objet appelé dans la requête (un post type). Ensuite on fait une requête avec une meta_query pour récupérer la page liée.

Enfin, on boucle sur cette requête (enfin, on ne boucle pas vraiment puisqu’il n’y a qu’un seul résultat) et on affiche ce que l’on souhaite.

On peut placer la fonction presentation_archive() où l’on veut, tant que c’est sur le template de l’archive.

Réécrire le permalien de la page de description

La page utilisée en tant que description s’affiche bien à présent lorsque l’on visite la page d’archive. Mais cette page continue d’exister, avec sa propre URL, ailleurs sur le site.

Et si on se greffait sur la fonction get_permalink afin de transformer l’URL de la page de présentation en URL de l’archive ?

Nous allons faire d’une pierre deux coups 🙂

  1. En supprimant les éventuelles occurences à l’URL de la page de description, partout sur le site (dans le thème, les menus, les sitemaps, les rss…)
  2. En permettant d’utiliser cette page pour cibler la page d’archive, comme dans les menus par exemple (où l’on ne peut pas nativement cibler une page d’archive)

Utile n’est-ce pas ? Voici le code :

// filtre permalien
add_filter( 'page_link', 'archive_permalink', 10, 2 );
function archive_permalink( $lien, $id ) {
    if( '' != ( $archive = get_post_meta( $id, '_archive_page', true ) ) && ! is_admin() )
        return get_post_type_archive_link( $archive );
    else
        return $lien;
}

Une redirection si l’on essaye d’accéder directement à la page

Le site ne comporte plus de lien vers la page de description de l’archive, mais on peut toujours y accéder directement si l’on connait l’URL.

Pour éviter ça il faut mettre en place une redirection 301, de façon automatique.

// redirect
add_action( 'template_redirect', 'redirect_to_archive' );
function redirect_to_archive() {
    if( is_page() && ! is_admin() ){
        global $post;
        if( '' != ( $archive = get_post_meta( $post->ID, '_archive_page', true ) ) ) {
            wp_redirect( get_post_type_archive_link( $archive ), 301 );
            exit();
        }
    }
}

Au moment du template_redirect, si le contenu chargé est une page, et si elle comporte une meta donnée _archive_page, alors on la redirige. Une redirection 301 est définitive et fera rapidement comprendre aux moteurs de recherche que la page recherchée a changée d’adresse.

Les classes des menus

En substituant l’archive à sa description, les entrées du menu vers la page d’archive cesseront de se voir appliquer les éventuelles classes current-menu-item et current-menu-ancestor.

Pour rétablir ce comportement, il faut appliquer un filtre sur nav_menu_css_class qui est utilisé dans le walker wp_nav_menu.

//filtre classes nav menu
add_filter( 'nav_menu_css_class', 'add_my_archive_menu_classes', 10 , 3 );
function add_my_archive_menu_classes( $classes , $item, $args ) {
    if( '' != ( $archive = get_post_meta( $item->object_id, '_archive_page', true ) ) ) {
        if( is_post_type_archive( $archive ) )
            $classes[] = 'current-menu-item';
        if( is_singular( $archive ) )
            $classes[] = 'current-menu-ancestor';
    }
    return $classes;
}

On y compare l’objet du menu et la page courante. En cas de correspondance on ajoute la classe qui va bien.

Le système dans son intégralité

Voilà, c’est fini ! Revoici le code dans son intégralité :


// ajout de la metabox
add_action( 'admin_init', 'my_admin_init' );
function my_admin_init(){
    add_meta_box("desc_page", "Archive à présenter", "archive_page", "page", "side", "high");
}

//fonction de la metabox
function archive_page( $post ) {
    $archive_page = get_post_meta( $post->ID, '_archive_page', true );
    ?>
    <select name="archive_page">
        <option value="">Aucune</option>
        <?php
            $post_types = get_post_types( array( 'show_ui' => true, '_builtin' => false ) );
            foreach( $post_types as $post_type )
                echo '<option value="' . esc_attr( $post_type ) . '" ' . selected( $post_type, $archive_page, false ) . '>' . esc_html( $post_type ) . '</option>'; 
        ?>
    </select>
    <p>Choisissez la cible de cette page</p>
    
    <?php
    wp_nonce_field( 'archive_page-save_' . $post->ID, 'archive_page-nonce') ;
}

//sauvegarde de la metabox
add_action( 'save_post', 'my_save_post' ); 
function my_save_post( $post_ID ){ 
    // on retourne rien du tout s'il s'agit d'une sauvegarde automatique
    if ( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE )
        return $post_ID;
        
    if ( isset( $_POST[ 'archive_page' ] ) ) {
        check_admin_referer( 'archive_page-save_' . $_POST[ 'post_ID' ], 'archive_page-nonce' );

        if( isset( $_POST[ 'archive_page' ] ) ) {
            $target = $_POST[ 'archive_page' ];
            global $wpdb;
            $suppr = $wpdb->get_results( $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_archive_page' AND meta_value = '%s'"), $target );
            foreach( $suppr as $s )
                delete_post_meta( $s->post_id, '_archive_page' );
            update_post_meta( $_POST[ 'post_ID' ], '_archive_page', $_POST[ 'archive_page' ] );
        }
    }
}

//présentation de l'archive
function presentation_archive() {
    $post_type_obj = get_queried_object();
    $target = $post_type_obj->name;
    $presentation = new WP_Query( array(
        'post_type' => 'page',
        'meta_query' => array(
            array(
                'key' => '_archive_page',
                'value' => $target,
                'compare' => '='
                )
            )
        ) );
    if( $presentation->have_posts() ) : $presentation->the_post();
        the_title( '<h1 class="h1">', '</h1>' );
        echo '<div class="article-elem">';
        echo the_content();
        echo '</div>';
    endif;
}

// filtre permalien
add_filter( 'page_link', 'archive_permalink', 10, 2 );
function archive_permalink( $lien, $id ) {
    if( '' != ( $archive = get_post_meta( $id, '_archive_page', true ) ) && ! is_admin() )
        return get_post_type_archive_link( $archive );
    else
        return $lien;
}

// redirect
add_action( 'template_redirect', 'redirect_to_archive' );
function redirect_to_archive() {
    if( is_page() && ! is_admin() ){
        global $post;
        if( '' != ( $archive = get_post_meta( $post->ID, '_archive_page', true ) ) ) {
            wp_redirect( get_post_type_archive_link( $archive ), 301 );
            exit();
        }
    }
}

//filtre classes nav menu
add_filter( 'nav_menu_css_class', 'add_my_archive_menu_classes', 10 , 3 );
function add_my_archive_menu_classes( $classes , $item, $args ) {
    if( '' != ( $archive = get_post_meta( $item->object_id, '_archive_page', true ) ) ) {
        if( is_post_type_archive( $archive ) )
            $classes[] = 'current-menu-item';
        if( is_singular( $archive ) )
            $classes[] = 'current-menu-ancestor';
    }
    return $classes;
}

Vous pouvez, si vous le souhaitez, prendre ce code et le lâcher dans un fichier du dossier mu-plugin. Il ne restera plus qu’à appeler la fonction dans votre template d’archive.

Une variante…

Ces lignes de code peuvent être insérées au choix dans le fichier functions.php, dans un plugin ou un mu-plugin. Peu importe…

Pour établir la liaison entre une page de contenu et une page d’archive, il y a deux façons de procéder :

Chaque méthode a ses avantages ; pour des raisons de souplesse j’ai préféré utilisé la première.

Mais rien n’empêche d’utiliser l’autre technique ;-)

Contacter l'auteur :

willy bahuaud

Je suis Willy Bahuaud, développeur de sites, de thèmes et d'extensions WordPress. Passionné par ce CMS, j'aime fouiller le code à la recherche des hooks, API et techniques pour optimiser mes développements.
Vous avez un site WordPress en projet ? Demandez mon expertise !

8 commentaires

  1. Par Greg — Il y a 3 années
    Sympa comme idée 🙂

    Une alternative, bien moins pratique, serait d’utiliser le champs « description » de register_post_type(), mais cela pose plein de contraintes.

    Sinon, il ne serait pas plus avisé d’utiliser le paramètre « has_archive » au lieu de « show_ui » pour le get_post_types() ? (ok, ça revient presque au même)

    Dernier point, concernant ta fonction my_save_post(), je note quelques erreurs et failles de sécurité. De plus, il y a bien plus pratique que wpdb + foreach selon moi. Je te propose ça plutôt (non testé) : http://pastebin.com/ZJhFGB2D
    T’en dis quoi ? 🙂

    A++

  2. Par Willy Bahuaud — Il y a 3 années
    Oui tout à faire pour le get_post_types(), je n’avais pas vu regardé le détail de la fonction. Le codex ne le mentionne pas mais on peut lui passer les mêmes paramètres que register_post_type, et la fonction ressortira les post_type correspondants.

    Alors je ne connaissais pas du tout delete_metadata(), et ton code fonctionne très bien ! Pratique pour supprimer les occurrences d’une même meta indifféremment l’ID de l’objet auquel elle est rattachée 🙂 Merci !

    Tu as raison, je devrais vérifier la valeur de _archive_page avant la sauvegarde. Habituellement je le fais à l’affichage, mais pas ici…

    Par contre je n’ai pas compris l’intérêt de tester que $_POST[ 'post_id' ] soit bien un entier. Si on essaye de faire une injection (ou autre) dessus, le check_admin_referer() va stopper le tout, non ?

    Merci pour ce commentaire qui me permet de peaufiner le système !!

  3. Par Julio Potier@Développement WordPress — Il y a 3 années
    Oui le check_admin_referer va le stopper mais tu montres tout de même dans un tuto une requête contenant un $_POST sans traitement. Imagine la personne qui reprends un morceau de ton code ailleurs, BIM merci Willy dit « Willy La faille » XD
  4. Par Willy Bahuaud — Il y a 3 années
    ah bah aussi si les gens reprennent pas le code en entier aussi, moi je ne réponds plus de rien XD

    Nan mais ok, je comprend, je corrige 🙂

  5. Par Greg — Il y a 3 années
    Pour ( $_post_ID = (int) $_POST[ 'post_ID' ] ), le cast est simplement une habitude que j’ai prise (regard accusateur vers ) et sa présence dans le if permet aussi de vérifier que la valeur retournée n’est pas 0.

    PS : il reste une typo (que j’avais corrigée dans mon pastebin :p), le return $post_id; devrait être return $post_ID; ;)

  6. Par sylvain Brunerie — Il y a 3 années
    Bonjour,

    Merci pour se script.

    Mais comment passer par une meta donnée pour « dire » que telle page présente telle archive.

    Car j’ai créé un custom post type dans le back office.
    Quel code à mettre dans la page où je veux afficher les données liés à mon custom post type.

  7. Par Arnaud Le Bihan — Il y a 1 année
    Un grand merci pour ce code !
    C’est exactement ce que je cherchais pour un projet.
    #laclasse #lesbonstuyauxwp
  8. Par Guillaume MESNARD — Il y a 9 mois
    Super Bahuaud, juste ce dont j’avais besoin comme fonctionnalité ;)
    Thx!

Commenter

Les sites qui parlent de cet article