Estoy pasando la web de un cliente desde Joomla a WordPress y tiene una sección tipo glosario con navegación alfabética. Estaba utilizando categorías «a, b, c, etc» para obtener los términos de cada letra. ¿Por qué asignar manualmente una categoría «c» para un post cuyo título comienza por «c» y es un término del glosario? Me dije, tiene que haber una mejor opción. Y me topé con esta pregunta en WPSE: How to build a directory with indexes in WordPress. Una de las respuestas, la que utiliza un taxonomía (i.e. categoría) «escondida» me convenció definitivamente.

He tomando esta idea y la he desarrollado un poco. El proceso consiste en crear una taxonomía que no se mostrará en la pantalla de edición del post (está «escondida») y sus términos (a, b, c, etc) se asignarán de forma automática cuándo se guarda el post. En combinación con un custom post type el resultado es excelente para crear un directorio tipo glosario en el que se listan las entradas alfabéticamente.

1

Registro del custom post type

Creamos un tipo de post para gestionar los términos del glosario alfabético de forma separada y que no se mezcle con el resto de entradas, por ejemplo del blog. He llamado a este tipo de post «Terms» (Términos).

add_action( 'init', 'cybglossary_init' );
function cybglossary_init() {
     
    $labels = array(
        'name'               => _x( 'Terms', 'post type general name', 'cybglossary' ),
        'singular_name'      => _x( 'Term', 'post type singular name', 'cybglossary' ),
        'add_new_item'       => __( 'Add New Term', 'cybglossary' ),
        'new_item'           => __( 'New Term', 'cybglossary' ),
        'edit_item'          => __( 'Edit Term', 'cybglossary' ),
        'view_item'          => __( 'View Term', 'cybglossary' ),
        'all_items'          => __( 'All Term', 'cybglossary' ),
        'search_items'       => __( 'Search Terms', 'cybglossary' ),
        'not_found'          => __( 'No terms found.', 'cybglossary' ),
        'not_found_in_trash' => __( 'No terms found in Trash.', 'cybglossary' )
    );
 
    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'rewrite'            => array( 'slug' => _x('terms', 'post type rewrite slug', 'cybglossary' ) ),
        'capability_type'    => 'post',
        'has_archive'        => true,
        'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' )
    );
 
    register_post_type( 'term', $args );
    
}
2

Crear una taxonomía escondida

En este paso creamos una taxonomía «escondida» con el argumento show_ui => false, es decir, que no se va a mostrar un área de administración para la taxonomía ni se va a mostrar el área de selección en la pantalla de edición del post. La asignación de cada entrada del glosario con el elemento de la taxonomía se hará de forma automáctica en el siguiente paso. El siguiente código se añade dentro del action init del paso anterior.

if( ! taxonomy_exists( 'glossary' ) ) {
    $args = array(
        'show_ui'    => false,
        'rewrite'    => _x('glossary', 'taxonomy URL slug', 'cybglossary')
     );
     register_taxonomy( 'glossary', array('term'), $args );
}
3

Asignar la letra del abecedario automáticamente

Aunque pueda parecer que asignar de forma manual el post a una letra del alfabeto es algo trivial, si nos lo ahorramos mejor. Existe una tendencia global de ahorrar clics y elementos a la hora de escribir y publicar contenido; y tiene su lógica. Es realmente agradable que se simplifiquen los pasos y que el proceso editorial se haga cada vez más fácil y simple.

add_action('save_post','cyb_set_first_letter');
function cyb_set_first_letter( $post_id ){
 
    // skip autosave
    if( defined('DOING_AUTOSAVE') && DOING_AUTOSAVE ) {
         return;
    }
 
    // limit to term post type
    if( isset($_POST['post_type']) && 'term' != $_POST['post_type'] ) {
         return;
    }
    
     // Check permissions
    if( !current_user_can('edit_post', $post_id ) ) {
        return;
    }
 
    //Assign first letter of the title as glossary term
    if( isset($_POST['post_title']) ) {
        //TODO: skip no-sense words, characters, etc
    	$glossary_term = mb_strtolower( mb_substr( $_POST['post_title'], 0, 1 ) );
    	wp_set_post_terms(
		$post_id,
		$glossary_term,
		'glossary'
        );
    }
 
    //delete the transient that is storing the alphabet letters
    delete_transient('cyb_archive_alphabet');
 
}
4

Cambiar el orden de las páginas de archivo

Las páginas de archivo, dónde se muestra el listado de posts, el «blog» del tipo de post estándar para que te hagas una día, ordena los posts según la fecha de publicación en orden descendiente, del más nuevo al más antiguo.

En un glosario de términos nos interesa más ordenar alfabéticamente según el título, ¿no?. Así que vamos a utilizar el action pre_get_posts para cambiar el orden del query principal cuándo estemos en el archivo de nuestro custom post type, que le habíamos llamado term. También lo haremos para las páginas de archivo de la taxonomía glossary que hemos asociado al custom post type:

// Set ascendent order by title in "terms" post type
// and glossary taxomony archives
add_action( 'pre_get_posts', function( $query ) {
  if( ! $query->is_admin && $query->is_main_query()
    && (
      $query->is_tax( 'glossary' ) || $query->is_post_type_archive( 'term' )
    )
  ) {
    $query->set( 'order', 'ASC' );
    $query->set( 'orderby', 'title' );
  }
} );
5

Cambiar el orden de los links previo/siguiente

Al igual que en las páginas de archivo, los enlaces hacia el post siguiente y el post anterior se seleccionan en base a la fecha de publicación.

Vamos a cambiarlo también por un orden según el título alfabéticametne. Utilizaremos los filtros get_next_post_sort y get_previous_post_sort junto a los filtros get_next_post_where y get_previous_post_where:

// Get next/prev post sorted by title
// See http://wordpress.stackexchange.com/questions/166932/how-to-get-next-and-previous-post-links-alphabetically-by-title-across-post-ty
add_filter('get_next_post_sort', 'filter_next_post_sort');
function filter_next_post_sort($sort) {
    if ( !is_main_query() || !is_singular('term') ) {
    
        return $sort;
        
    }
    $sort = "ORDER BY p.post_title ASC LIMIT 1";
    return $sort;
    
}
add_filter('get_next_post_where',  'filter_next_post_where');
function filter_next_post_where($where) {
    global $wpdb;
    if ( !is_main_query() || !is_singular('term') ) {
    
      return $where;
      
    }
    
    $the_post = get_post( get_the_ID() );
    $where = $wpdb->prepare("WHERE p.post_title > '%s' AND p.post_type = '". $the_post->post_type ."' AND p.post_status = 'publish'",$the_post->post_title);
    return $where;
}
add_filter('get_previous_post_sort',  'filter_previous_post_sort');
function filter_previous_post_sort($sort) {
    if ( !is_main_query() || !is_singular('term') ) {
    
        return $sort;
        
    }
    $sort = "ORDER BY p.post_title DESC LIMIT 1";
    return $sort;
    
}
add_filter('get_previous_post_where', 'filter_previous_post_where');
function filter_previous_post_where($where) {
    global $wpdb;
    if ( !is_main_query() || !is_singular('term') ) {
    
      return $where;
      
    }
    
    $the_post = get_post( get_the_ID() );
    
    $where = $wpdb->prepare("WHERE p.post_title < '%s' AND p.post_type = '". $the_post->post_type ."' AND p.post_status = 'publish'",$the_post->post_title);
    return $where;
    
}
6

Crear el menú alfabético

Con la siguiente función se obtiene un menú con las letras del abecedario enlazadas al archivo de esa letra en la taxonomía «glossary», archivo que contendrá un listado de los posts que comienzan con esa letra.

Para evitar mostrar enlaces a letras que no contenga ningún post, se obtienen las que tienen posts y se almacena el resultado en un transient, transient que no se regenerará hasta que no se guarde/actualice una nueva entrada de nuestro glosario alfabético. Esta solución no me convence mucho y estoy estudiando otras opciones.

function cyb_the_glossary_menu() {

    $taxonomy ='glossary';

    // save the terms that have posts in an array as a transient
    if( false === ( $alphabet = get_transient('cyb_archive_alphabet') ) ) {

        // It wasn't there, so regenerate the data and save the transient
        $terms = get_terms($taxonomy);
        $alphabet = array();

        if($terms){
            foreach($terms as $term){
                 $alphabet[]= $term->slug;
            }
        }

        set_transient('cyb_archive_alphabet', $alphabet );
     }
     ?>

     <ul id="alphabet-menu">
         <?php
         foreach(range('a','z') as $i) {
 
             $current = ($i == get_query_var($taxonomy)) ? "current-menu-item" : "menu-item";
             if(in_array( $i, $alphabet )) {
                 printf('<li class="az-char %s"><a href="%s">%s</a></li>', $current, get_term_link( $i, $taxonomy ), $i );
             } else {
                 printf('<li class="az-char %s">%s</li>', $current, $i );
             }
 
          }
          ?>
     </ul>

<?php
}

Ejecuta la función cyb_the_glossary_menu() en la parte de tu tema dónde quieras que aparezca el menú alfabético.

Creo que la solución planteada en este tutorial es excelente para crear glosarios alfabéticos en WordPress, aunque no es válida para todos los casos. Por ejemplo, no es válido si quieres que posts como «El viento del sur» y «Un viento del sur» aparezcan en la «V», para este caso necesitaría algunos retoques. Lo he puesto todo en forma de plugin que puedes descargar desde Github:

CYB Glossary en Github

Asignar letras del abecedario a posts antiguos

Si tienes antiguos posts que quieres asignar a la nueva taxonomía del glosario, puedes ejecutar este código. Recuerda utilizarlo una sola vez y cambiar el argumento post_type por el valor que necesites.

add_action('init','cyb_run_once');
//create array from existing posts
function cyb_run_once(){

    $taxonomy = 'glossary';

    $alphabet = array();
    //get all posts of "post" type. Change for your needs
    $posts = get_posts( array('numberposts' => -1, 'post_type' => 'post' ) );

    foreach($posts as $p) :  
        //set term as first letter of post title, lower case
        wp_set_post_terms( $p->ID, mb_strtolower(mb_substr($p->post_title, 0, 1)), $taxonomy );
    endforeach;     
}