3

I want to achieve something like this, I don't know if it is possible and what would be the best way to do it:

enter image description here

The way I query the posts is like this:

<div class="post">
    <?php global $wp_query;
    query_posts( array('post_type' => array( 'lecturas-post' ),'showposts' => 15, 'paged'=>$paged, 'order' => 'DESC' ));?>
    <?php while ( $wp_query->have_posts() ) : $wp_query->the_post(); ?>
        <div><?php the_title() ?></a>
    <?php endwhile; // end of the loop. ?>
</div>

Anyone can give me a tip on how or best way to do it?

2
  • 1
    To achieve this structure you'd need multiple queries. First you'd need to identify the months to display, and then you'd loop through the results and grab the relevent posts. I will attempt an answer shortly...
    – David Gard
    Commented Jan 27, 2015 at 11:47
  • You can achieve this in one query without using query_posts Commented Jan 27, 2015 at 13:18

3 Answers 3

11

As said in a comment, you can do this in one query. The principle here is to only display the date heading if the post date's month of the previous post does not match that of the previous post

FEW NOTES

Before I start, a few notes:

  • Never use query_posts, except if you really need to break everything on a page. It not just reruns the main query and also breaks it, it messes up pagination and your globals, and also messes with your queried object functions. If you really have to run a custom query, use WP_Query or get_posts

  • showposts was dropped years ago in favor of posts_per_page

  • No need to make use of the global $wp_query. query_posts messes that up anyway

THE PLAN

Post are served up in chronological order with the newest post first, and the oldest post last, so they are already in the correct order. It is just a matter of displaying the date in the appropriate place.

To do this, all you need to do is to get the current month and year from the current post's post date, and then comparing that with the previous post's post date month and then either displaying the date if the months don't match or not display it if they match

As explanation, I will use the main loop with the main query.

To accomplish this, you need to:

  • Get the month from the current post's post date. To achieve this, use get_the_date( 'F' )

  • Get the previous post in the loop with $wp_query->posts['this will be current post -1 ']->post.

  • Get and compare the months between the two posts

  • Display or do not display the date according to the comparison

THE CODE

This code goes inside your loop, just after your while() statement.

$current_month = get_the_date('F');

if( $wp_query->current_post === 0 ) { 

   the_date( 'F Y' );

}else{ 

    $f = $wp_query->current_post - 1;       
    $old_date =   mysql2date( 'F', $wp_query->posts[$f]->post_date ); 

    if($current_month != $old_date) {

        the_date( 'F Y' );;

    }

}

CUSTOM QUERY

If you need to run a custom query, try this

$q = new WP_Query( array('post_type' => array( 'lecturas-post' ),'posts_per_page' => 15, 'paged'=>$paged, 'order' => 'DESC' ));

if( $q->have_posts() ) {
    while( $q->have_posts() ) {

        $q->the_post();

        $current_month = get_the_date('F');

        if( $q->current_post === 0 ) { 

           the_date( 'F Y' );

        }else{ 

            $f = $q->current_post - 1;       
            $old_date =   mysql2date( 'F', $q->posts[$f]->post_date ); 

            if($current_month != $old_date) {

                the_date( 'F Y' );;

            }

        }

        the_title();

    }

}
5
  • Good answer, and a similar concept for your custom query section as in my updated answer, just a different way of checking if the title has been output. I guess it's up to the OP if they want to incorporate the date_query or not :)
    – David Gard
    Commented Jan 27, 2015 at 15:37
  • One thing I will say though is that a month may be split across pages. If the OP doesn't care then happy days, just though it worth pointing out.
    – David Gard
    Commented Jan 27, 2015 at 15:39
  • @DavidGard thanks. The date will display on each and every page on the first post regardless. If you don't need that, add the is_paged() condition. Commented Jan 27, 2015 at 15:50
  • @wpuser my pleasure. Enjoy :-) Commented Jan 27, 2015 at 15:50
  • @PieterGoosen - I realise that, just thought it worth mentioning for anyone else who may come across this answer in the future. I know I'm easily confused so I assume everyone else is also!
    – David Gard
    Commented Jan 27, 2015 at 15:56
4

Updated Answer

After thinking about the comment from @PieterGoosen below I've added a method of achieving the you goal by using a single query.

I benchmarked the methods and the single query is faster, with the multiple query method being about 15% slower. Not a hugh margin, but every little helps, and to be honest the method can probably be refined further.

I've left the multiple query method in the answer for prosperity, but I recommend that the single query method be used.

Single Query Method

$time_start = microtime(true);

/** Set up a date interval object for 6 monts ago (you can change as required) */
$interval = new DateInterval('P6M');
$interval->invert = 1;

/** Grab the date as it was 6 months ago */
$date = new DateTime(date('Y-m-d'));
$date->add($interval);

/** Query the database for all posts newer than the the given date interval */
$args = array(
    'nopaging'          => true,
    'posts_per_page'    => -1,
    'post_type'         => 'post',
    'post_status'       => 'publish',
    'order_by'          => 'date',
    'date_query'        => array(
        'after' => $date->format('Y-m-d')
    )
);
$month_query = new WP_Query($args);

/** Check to ensure that there are articles for this month... */
if($month_query->have_posts()) :

    $month_titles = array();
    $close_ul = false;
    
    
    //echo '<ul style="padding-left: 250px;" id="monthly-posts">';
    
    /** Set the attributes for displaying the title as an attribute */
    $title_attribute_args = array(
        'before'    => 'Visit article \'',
        'after'     => '\' ',
        'echo'      => false
    );      
    
    /** Loop through each post for this month... */
    while($month_query->have_posts()) : $month_query->the_post();
    
        /** Check the month/year of the current post */
        $month_title = date('F Y', strtotime(get_the_date('Y-m-d H:i:s')));
        
        /** Maybe output a human friendly date, if it's not already been output */
        if(!in_array($month_title, $month_titles)) :
        
            if($close_ul) echo '</ul>';                                                             // Check if the unordered list of posts should be closed (it shouldn't for the first '$monthe_title')
            echo '<h1 style="padding-left: 250px;" id="monthly-title">' . $month_title . '</h1>';   // Output the '$month_title'
            echo '<ul style="padding-left: 250px;" id="monthly-posts">';                            // Open an unordered lists for the posts that are to come
            $month_titles[] = $month_title;                                                         // Add this `$month_title' to the array of `$month_titles` so that it is not repeated
            $close_ul = true;                                                                       // Indicate that the unordered list should be closed at the next oppurtunity
            
        endif;
        
        /** Output each article for this month */
        printf(
            '<li id="monthly-post-%1$s">%2$s <a href="%3$s" title="%4$s">%3$s</a></li>',
            get_the_ID(),                               /** %1$s - The ID of the post */
            get_the_title(),                            /** %2$s - The article title */
            get_permalink(get_the_ID()),                /** %3$s - The article link */
            the_title_attribute($title_attribute_args)  /** %4$s - The title for use as an attribute */
        );
        
    endwhile;
    
    if($close_ul) echo '</ul>'; // Close the last unordered list of posts (if there are any shown)
    
endif;

/** Reset the query so that WP doesn't do funky stuff */
wp_reset_query();

Original Answer

Give this a try. I've set it up so that only the last 6 months are chosen and only the last 5 posts from each month, but you can alter that as you please.

Essentially the code will first check which months have posts in them and then output the last five posts from that month, along with a link.

Multiple Query Method

global $wpdb, $wp_locale;
    
/** Query the individual months to display (I've chosen the last 6 months) */
$query = $wpdb->prepare('
    SELECT DISTINCT YEAR(`%1$s`.`post_date`) AS year, MONTH(`%1$s`.`post_date`) AS month
    FROM `%1$s`
    WHERE `%1$s`.`post_type` = "post"
    ORDER BY `%1$s`.`post_date` DESC
    LIMIT 6',
    $wpdb->posts
);
$months = $wpdb->get_results($query);

/** Count the number of months */
$month_count = count($months);

/** Ensure that there are months to display... */
if($month_count || ($month_count === 1 && $months[0]->month > 0)) :

    /** Loop through each month... */
    foreach($months as $month) :

        if($month->year === 0) :
            continue;
        endif;
        
        /** Grab the individual month and year, and construct a human friendly date (for the title) */
        $m = zeroise($month->month, 2);
        $y = $month->year;
        $human_date = sprintf(__('%1$s %2$d'), $wp_locale->get_month($m), $y);
        
        /** Grab any posts for this month (I've chosedn only the last 5 posts) */
        $args = array(
            'nopaging'          => true,
            'posts_per_page'    => 5,
            'post_type'         => 'post',
            'post_status'       => 'publish',
            'order_by'          => 'date',
            'year'              => $y,
            'monthnum'          => $m
        );
        $month_query = new WP_Query($args);
        
        /** Check to ensure that there are articles for this month... */
        if($month_query->have_posts()) :

            /** Output a human friendly date */
            echo '<h1 id="monthly-title">' . $human_date . '</h1>';
            echo '<ul id="monthly-posts">';
            
            /** Set the attributes for displaying the title as an attribute */
            $title_attribute_args = array(
                'before'    => 'Visit article \'',
                'after'     => '\' ',
                'echo'      => false
            );      
            
            /** Loop through each post for this month... */
            while($month_query->have_posts()) : $month_query->the_post();
            
                /** Output each article for this month */
                printf(
                    '<li id="monthly-post-%1$s">%2$s <a href="%3$s" title="%4$s">%3$s</a></li>',
                    get_the_ID(),                               /** %1$s - The ID of the post */
                    get_the_title(),                            /** %2$s - The article title */
                    get_permalink(get_the_ID()),                /** %3$s - The article link */
                    the_title_attribute($title_attribute_args)  /** %4$s - The title for use as an attribute */
                );
                
            endwhile;
            
            echo '</ul>';
            
        endif;
        
        /** Reset the query so that WP doesn't do funky stuff */
        wp_reset_query();
        
    endforeach;

endif;
7
  • You can do this in one query :-) Commented Jan 27, 2015 at 12:33
  • Really? What are you thinking? I did think about GROUP BY, but the OP want's headings for the month/year, so my tiny brain can't see past multiple queries :-/
    – David Gard
    Commented Jan 27, 2015 at 12:36
  • Will see if I can post a solution later. Just a bit tied up :-) Commented Jan 27, 2015 at 13:18
  • Hmm, I'm intrigued… I suppose you could grab all the posts newer than X using date_query, and then break them out in to a multi-dimensional array. I may have a go at that, but I’m looking forward to a super-simple answer from you later ;-)
    – David Gard
    Commented Jan 27, 2015 at 13:29
  • @PieterGoosen I've had a bash at a single query method, but I'd be grateful for any (constructive) criticisms that you may have!
    – David Gard
    Commented Jan 27, 2015 at 14:39
1

This is a function I made to use for general needs, to retrieve post or custom post types data before or after a certain year/month, or current year, in whichever order you need:

// you could change the name in case it collides with some other plugin
function get_posts_by_date($user_options = array()){

  $options = array(
    'year_limit' => '1980'
    ,'month_limit' => '01'
    ,'operator' => '>=' // date comparison operator
    ,'current_year' => true // limit data to current year
    ,'post_type' => 'post'
    ,'year_order' => 'DESC'
    ,'month_order' => 'DESC'
    ,'post_ids_order' => 'DESC'
    ,'raw_output' => false
  );

  extract(array_merge($options, $user_options));

  global $wpdb;

  if($operator == '>=' || $operator == '=='){
    $day = "01";
  } elseif($mode == '<='){
    $day = "31";
  }

  if($current_year){ // will be after [previous year]/12/31
    $year_limit = date('Y', strtotime('-1 year'));
    $month_limit = '12';
    $day = "31";
    $operator == '>=';
  }

  // warning: if your parameters come from user input/forms, 
  // pass them using $wpdb::prepare()
  // https://developer.wordpress.org/reference/classes/wpdb/prepare/
  $results = $wpdb->get_results("
    SELECT tbl.y year, group_concat(month_posts ORDER BY tbl.m " . $month_order . " SEPARATOR '-') months
      FROM (
        SELECT YEAR(p.post_date) y, MONTH(p.post_date) m, concat(MONTH(p.post_date), ':', group_concat(p.id ORDER BY p.post_date " . $post_ids_order . " )) month_posts
        FROM $wpdb->posts p
        WHERE (p.post_status = 'publish' OR p.post_status = 'future')
          AND p.post_type = '" . $post_type . "'
          AND p.post_date " . $operator . " DATE('" . $year_limit . "-" . $month_limit . "-" . $day . " 00:00:00')
        GROUP BY y, m
      ) tbl
    GROUP BY tbl.y
    ORDER BY tbl.y " . $year_order
  );

  if($raw_output) return $results;

  global $wp_locale;

  foreach ($results as $data){
    $months_data = explode('-',$data->months);
    $months = array();
    $data->count = 0; // year count

    foreach ($months_data as $month_data){
      $month_obj = new stdClass;

      $splitted = explode(':',$month_data);
      $raw_month = $splitted[0];
      $month_obj->number = $raw_month;
      $month_obj->name = $wp_locale->get_month($raw_month);
      $month_obj->posts = array();
      $post_ids = explode(',',$splitted[1]);
      $data->count += count($post_ids);

      foreach($post_ids as $post_id){
        $month_obj->posts[] = get_post($post_id);
        $months[$raw_month] = $month_obj;
      }// foreach
    }// foreach

    $data->months = $months;
  }// foreach

  return $results;
}// get_posts_by_date

Usage example:

$posts_by_date = get_posts_by_date(array(
  'year_limit' => '2016'
  ,'operator' => '<='
  ,'current_year' => false
  ,'post_type' => 'product'
  ,'month_order' => 'ASC'
  ,'raw_output' => true
));

If raw_output option is true, the default output will be something like this:

array(2) {
  [0]=>
  object(stdClass)#6645 (2) {
    ["year"]=>
    string(4) "2017"
    ["months"]=>
    string(65) "8:386,401-7:406,373,380,377,408,399,362-6:1,391,404-5:367,397,394"
  }
  [1]=>
  object(stdClass)#6644 (2) {
    ["year"]=>
    string(4) "2016"
    ["months"]=>
    string(5) "6:429"
  }
}

The "months" string contains values formatted as:

month:[post ids]-month:[post ids]-ecc

If raw_output option is false, you get a list of posts like this:

array (array of objects)
  object
    -> year (ex. '2017')
    -> count (total year's posts)
    -> months (array of objects)
        month
          -> number (of month)
          -> name (localized)
          -> posts (array of post objects)

Happy coding... :)

4
  • Watch out for possible SQL injections and avoid extract (harder to debug) and not use general function names to avoid possible name collisions (or use namespace).
    – birgire
    Commented Aug 15, 2017 at 13:45
  • @birgire: bah... I know all of this, but it's up to the developer to evaluate the name of the function in relation of his environment, and eventually sanitize user/form input. This is only to share usefull code, I honestly think this function will help a lot. Then, I really hope that any developer -- (only) if needed -- will be perfectly able to change the name of the function, and/or to sanitize user input or add a prepare statement instead of direct writing... Commented Aug 16, 2017 at 13:05
  • Hopefully they will do that, but I fear that many just copy/paste code from this site and forget about it, if it worked the first time ;-)
    – birgire
    Commented Aug 16, 2017 at 14:52
  • 1
    Ok thank you I added security suggestions, I think it's enough, the original question is not about security issues :) Commented Aug 16, 2017 at 16:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.