Aunque no soy partidario de paginar loops secundarios, salvo casos muy excepcionales y luego explicaré por qué, es una pregunta tan frecuente que le voy a dedicar un post a explicar como se hace. Vamos a ello.

Para crear un listado paginado necesitamos:

  • el parámetro post_per_page de WP_Query para establecer el número de posts que se muestran en cada página.
  • el parámetro paged para establecer el número de la página actual (o page si estamos un static front page).
  • las funciones previous_posts_link()/next_posts_link() para mostrar un enlace a la página anterior/siguiente o la función paginate_links() para mostrar un set enlaces a la primera/última página, a la siguiente/anterior y a un conjunto de páginas intermedias.

Imaginemos que tenemos este loop secundario para obtener “eventos” próximos según una fecha almacenada en un meta field:

$args = array(
  'post_type'  => 'event',
  'meta_key'   => 'event_start_date',
  'order'      => 'ASC',
  'orderby'    => 'meta_value',
  'meta_query' => array(
     array(
       'key'      => 'event_start_date',
       'value'    => date( "Y-m-d" ),
       'compare'  => '>=',
       'type'     => 'DATE'
     )
   )
 
);

$events_query = new WP_Query( $args );

if( $events_query->have_posts() ) { ?>

  <h1><?php _e( 'Upcoming events', 'textdomain' ); ?></h1>

  <?php while( $events_query->have_posts() ) {
    $events_query->the_post(); ?>

    <h2><?php the_title(); ?></h2>

  <?php }

  wp_reset_postdata();

} else {

  _e( 'No results found', 'textdomain'  );

}

Si queremos paginarlo:

$current_page = get_query_var( 'paged' ) ? get_query_var( 'paged' ): 1;

$args = array(
  'post_type'    => 'event',
  'meta_key'     => 'event_start_date',
  'order'        => 'ASC',
  'orderby'      => 'meta_value',
  'meta_query'    => array(
     array(
       'key'      => 'event_start_date',
       'value'    => date( "Y-m-d" ),
       'compare'  => '>=',
       'type'     => 'DATE'
     )
  ),
  // Paginación
  'posts_per_page' => get_option('posts_per_page'),
  'paged'         => $current_page,
);

$events_query = new WP_Query( $args );

if( $events_query->have_posts() ) { ?>

  <h1><?php _e( 'Upcoming events', 'textdomain' ); ?></h1>

  <?php while( $events_query->have_posts() ) {
    $events_query->the_post(); ?>

    <h2><?php the_title(); ?></h2>

  <?php }

  echo paginate_links( array(
        'current' => $current_page,
        'total' => $events_query->max_num_pages
  ) );

  wp_reset_postdata();

} else {

  _e( 'No results found', 'textdomain'  );

}

¿Qué hemos hecho?

  1. Hemos obtenido la página actual con get_query_var( 'paged' ) y su valor lo hemos puesto como argumento paged en nuestro WP_Query. (Recuerda utilizar page si estás en un static front page).
  2. Hemos establecido el número de posts por página en el argumento post_per_page. Lo hemos puesto según el valor configurado para todo el sitio en Ajustes->Lectura pero puedes poner el número que quieras.
  3. Finalmente hemos utilizado la función paginate_links() y le hemos pasado los valores de la página actual y, aquí viene lo más importante, el número de páginas de nuestro query.

El punto tres es dónde más confusión suele haber. De forma predeterminada la función paginate_links() utiliza los datos del query principal pero nosotros estamos paginando un query secundario, así que tenemos que pasar los datos de este query secundario a la función:

$current_page = get_query_var( 'paged' ) ? get_query_var( 'paged' ): 1;
$args = array(
  // ....
  'posts_per_page' => get_option('posts_per_page'),
  'paged'         => $current_page
);
$mi_query = new WP_Query( $args );
// ...
echo paginate_links( array(
    'current' => $current_page,
    'total'   => $mi_query->max_num_pages
) );

Con previous_posts_link()/next_posts_link() ocurre algo similar. previous_posts_link() no necesita nada adicional pero next_posts_link() necesita conocer el número de páginas totales del query:

previous_posts_link( __( 'Previous page', 'textdomain' ) );
next_posts_link( __( 'Next page', 'textdomain' ), $mi_query->max_num_pages );

¿Cuándo paginar un loop secundario?

Prácticamente nunca. Cuándo se crean listados paginados suelen ir en el contenido principal de la página y si lo piensas un segundo te darás cuenta que “contenido principal” y “query secundario” chocan, son contradictorios.

Si te ves en la necesidad de modificar el contenido principal en cualquier parte de WordPress en la que intervengan posts de cualquier tipo, utiliza el action pre_get_posts para modificar el query principal según tus necesidades. Y como se trabaja con el query principal, las funciones de paginación no necesitan ninguna atención especial.

add_action( 'pre_get_posts', 'cyb_set_upcoming_events_query' );
function cyb_set_upcoming_events_query( $query ) {
  if ( $query->is_main_query() && ! is_admin() && alguna_otra_condicion_que_necesites() ) {

    $meta_query = array(
      array(
        'key'      => 'event_start_date',
        'value'    => date( "Y-m-d" ),
        'compare'  => '>=',
        'type'     => 'DATE'
      )
    );

    $query->set( 'post_type', 'event' );
    $query->set( 'meta_key', 'event_start_date' );
    $query->set( 'order', 'ASC' );
    $query->set( 'orderby', 'meta_value' );
    $query->set( 'meta_query', $meta_query );

  }
}

Además, al utilizar el action pre_get_posts le decimos a WordPress la solicitud que debe hacer a la base de datos desde el principio. De lo contrario, WordPress hará la solicitud que tenga que hacer (por ejemplo para obtener una página) y luego descartamos los datos de ese query para hacer nuestro query secundario y ponerlo como contenido principal. Hacemos queries extra sin necesidad.

Por estos motivos, si el query secundario es el contenido principal, modifica el query principal y olvídate del query secundario. Parece obvio, ¿verdad?

  • A mi tampoco me apasiona lo de paginar… además siempre da problemillas de SEO por temas de duplicidad y tal… 🙁

  • Israel Ixon

    Qué rapidez de respuesta. En mi caso en particular, creo que sí seria interesante paginarlo. Quizás no sea lo más aconsejable, pero hay gente que no soporta el scroll muy largo. Esto me exige poner una paginación si se juntan 10 resultados, por ejemplo. Me voy a poner manos a la obra a ver que sale de todo esto. Gracias por la amabilidad de regalar conocimiento porque sí.

    • No estoy seguro de que hayas entendido, o yo no lo he explicado bien, el motivo por el que NO veo apropiado, en general, paginar un query secundario. Da igual que sea un listado largo o corto, con scroll o sin scroll, la pregunta para decidirlo es: ¿el listado es el contenido princpal o no?

      Si la respuesta es afirmativa no deberías paginar el query secundario, ni siquiera deberías hacer un query secundario. Como explico en el post, si es el contenido principal hay que trabajar con el query principal. De lo contario se hacen queries completamente innecesarios.

    • Israel Ixon

      Soy duro de mollera, no te voy a decir que no. Mi problema es que
      seguramente estaré empleano mal toda la consulta que estoy haciendo. En
      mi caso, he utilizado el ejemplo que explicas en tu post “obtener post entre fechas…” para hacer una pequeña agenda. Distingo entre dos resultados, los eventos que están vigentes y los que ya han pasado. ¿Que ocurre?, que los resultados de los eventos ya pasados, se acumulan a través del tiempo, por lo que necesito que vayan paginados. Por eso necesito esta función. Otra cosa, es que yo esté empleando mal los querys para mis cosultas, que no lo descarto, pero eso ya es otra cosa… Es lo que tiene aprender a huevo con lo que otros aportan.