Filter next_post_link() and previous_post_link() by meta_key?

I have a page with two sections, each uses a different WP_Query() to pull in events, which are a custom post type. Each WP_Query() queries a meta_key for the event date so that Section 1 only displays upcoming events and Section 2 displays past events.

The upcoming events in Section 1 display all relevant information on my page, so clicking them is not possible.

Read More

The past events in Section 2 only display the event title and are clickable. When users click a past event they link to a custom single-event.php template for the past event.

I want to display Previous/Next navigation in the single-event.php template, but the navigation should only point to past events.

I tried using next_post_link() and previous_post_link() but these will link to upcoming events too, which I do not want. I can probably setup a new WP_Query() on my single-event.php and loop through it to get the Prev/Next IDs, but repeating the query seems like a drastic step.

I would really appreciate some insight on a way to filter out upcoming events from my Previous/Next post links. I’ve seen this question but I would prefer not to use a plugin.

Related posts

Leave a Reply

2 comments

  1. I managed to get this working using nothing but WordPress filters, thanks to @Milo’s hint.

    Just note that these are pretty specific to my case but you shouldn’t have a problem modifying them for your own use. I am using Advanced Custom Fields with a Date Picker field called date and Prev/Next links only point to events with date fields set to any day before today.

    I created 5 filters:

    • 1 to modify JOIN (to add wp_postmeta)
    • 1 to modify WHERE for the Previous link
    • 1 to modify WHERE for the Next link
    • 1 to modify SORT for the Previous link
    • 1 to modify SORT for the Next link

    Here’s what I came up with, it seems to be working but if anyone spots any issues I would love feedback:

    function get_adjacent_past_events_join($join) {
      if(is_singular('event')) {
        global $wpdb;
        $new_join = $join."INNER JOIN $wpdb->postmeta AS m ON p.ID = m.post_id ";
        return $new_join;
      }
      return $join;
    }
    add_filter('get_previous_post_join', 'get_adjacent_past_events_join');
    add_filter('get_next_post_join', 'get_adjacent_past_events_join');
    
    function get_prev_past_events_where($where) {
      if(is_singular('event')) {
        global $wpdb, $post;
        $id = $post->ID;
        $current_event_date = get_field('date', $id);
        $today = date('Ymd');
        $new_where = "WHERE p.post_type = 'event' AND p.post_status = 'publish' AND (m.meta_key = 'date' AND (m.meta_key = 'date' AND CAST(m.meta_value AS CHAR) < '$today')) AND (m.meta_key = 'date' AND (m.meta_key = 'date' AND CAST(m.meta_value AS CHAR) < '$current_event_date'))";
        return $new_where;
      }
      return $where;
    }
    add_filter('get_previous_post_where', 'get_prev_past_events_where');
    
    function get_next_past_events_where($where) {
      if(is_singular('event')) {
        global $wpdb, $post;
        $id = $post->ID;
        $current_event_date = get_field('date', $id);
        $today = date('Ymd');
        $new_where = "WHERE p.post_type = 'event' AND p.post_status = 'publish' AND (m.meta_key = 'date' AND (m.meta_key = 'date' AND CAST(m.meta_value AS CHAR) < '$today')) AND (m.meta_key = 'date' AND (m.meta_key = 'date' AND CAST(m.meta_value AS CHAR) > '$current_event_date'))";
        return $new_where;
      }
      return $where;
    }
    add_filter('get_next_post_where', 'get_next_past_events_where');
    
    function get_prev_past_events_sort($sort) {
      if(is_singular('event')) {
        global $wpdb;
        $new_sort = " GROUP BY p.ID ORDER BY m.meta_value+0 DESC";
        return $new_sort;
      }
      return $sort;
    }
    add_filter('get_previous_post_sort', 'get_prev_past_events_sort');
    
    function get_next_past_events_sort($sort) {
      if(is_singular('event')) {
        global $wpdb;
        $new_sort = " GROUP BY p.ID ORDER BY m.meta_value+0 ASC";
        return $new_sort;
      }
      return $sort;
    }
    add_filter('get_next_post_sort', 'get_next_past_events_sort');
    
  2. i had a pretty similar problem, needed to sort and exclude several posts from prev/next navigation. problem with @cfx’s solution was: its not capable for ajax: the is_singular() function returns false, if you load contents via wp-ajax. so it worked on page load, but didn’t, when content was changed by ajax. global $post; was helping me out here.

    here is my solution:

    /**
      * WP: join postmeta to our sql query, so we can filter for custom fields
      *
      * @param $join
      * @return string
      */
    function jnz_adjacent_work_join( $join ) {
      global $post;
      if ( get_post_type( $post ) == 'work' ) {
        global $wpdb;
        return $join . "INNER JOIN $wpdb->postmeta AS m ON p.ID = m.post_id ";
      }
      return $join;
    }
    add_filter('get_previous_post_join', 'jnz_adjacent_work_join');
    add_filter('get_next_post_join', 'jnz_adjacent_work_join');
    
    
    
    /**
     * WP: Change order of post for prev / next navigation
      * exclude posts with custom field "not_clickable" set to true
      *
      * @param $where
      * @param $operator
      * @return string|void
      */
     function jnz_adjacent_work_where( $where, $operator ) {
       global $post;
       if ( get_post_type( $post ) == 'work' ) :
         global $wpdb;
         $where = $wpdb->prepare("WHERE p.post_title {$operator} '%s' AND p.post_type = 'work' AND p.post_status = 'publish' AND (m.meta_key = 'not_clickable' AND (m.meta_key = 'not_clickable' AND m.meta_value != 1))", $post->post_title );
       endif;
    
       return $where;
     }
    
     $gt = '<';
     $lt = '>';
     add_filter( 'get_next_post_where', function( $where ) use ( $lt ) {
       return jnz_adjacent_work_where( $where, $lt );
     });
     add_filter( 'get_previous_post_where', function( $where ) use ( $gt ) {
       return jnz_adjacent_work_where( $where, $gt );
     });
    

    in this case, the costum field query is: exclude all posts, that have cf not_clickable set to true.

    another problem i encountered: i had some content created and then implemented that custom field afterwards.. so the query also excluded the posts that didn’t even had that field attached to the post, no matter if true or false. just keep that in mind, when using this type of filtering. make sure that every post has a value or consider this in your sql syntax..