How to Show Posts Without Using setup_postdata()

Quick Summary
- Loop over get_posts() results and read fields straight from each WP_Post object, such as post_title, without calling setup_postdata().
- The example lists hand-picked IDs with post__in, keeps their order with orderby post__in and shows one per page via paged.
- Functions relying on the global post still refer to the page running the template, so pass the post object to has_post_thumbnail() and get_the_post_thumbnail().
get_posts() returns an array of WP_Post objects. The usual pattern, used for showing recent posts with get_posts(), is to call setup_postdata() on each one so template tags work, but you can also skip it and read the data straight from the object. This snippet does that to show a few hand-picked posts, one per page, in a WordPress template.
<?php
$paged = get_query_var( "paged" )?get_query_var( "paged" ):1;
$post_per_page = 1;
$post_ids = array( 12,9,18 );
$_p = get_posts(array(
'posts_per_page' => $post_per_page,
'post__in' => $post_ids,
'orderby' =>'post__in',
'paged' => $paged
));
foreach ($_p as $p) {
//setup_postdata( $post );
?>
<div class="post" <?php post_class( ); ?> >
<div class="container">
<div class="row">
<div class="col-md-12">
<h3><?php echo apply_filters( "the_title",$p->post_title ); ?></h3>
</div>
<div class="col-md-12">
<p>
<?php
if(has_post_thumbnail( )){
the_post_thumbnail( 'large', array( "class"=>"img-fluid single-post-highlight-image" ) );
}
?>
</p>
</div>
</div>
</div>
</div>
<?php }
wp_reset_postdata();
?>How it works
$pagedis the current page number from the URL, or 1 on the first page.post__inlimits the query to the IDs in$post_ids, and'orderby' => 'post__in'keeps them in the order you listed: 12, 9, 18. Replace the IDs with your own.$post_per_pageis 1, so together withpagedeach page shows one of the three posts.get_posts()returns a plain array without a page count, and no page links are printed, so add your own navigation.- The title comes from
$p->post_title. Passing it throughapply_filters( "the_title", ... )applies the filtersthe_title()would. Other fields work the same way, such as$p->IDor$p->post_content.
Watch out for tags that use the global post
Without setup_postdata(), the global $post is still the page running the template, and any function that relies on it ignores $p:
has_post_thumbnail()andthe_post_thumbnail()check and show the featured image of that page, not of the listed post. Pass the post instead:has_post_thumbnail( $p )andget_the_post_thumbnail( $p, 'large', ... ).post_class()prints the page’s classes too, and because the div already hasclass="post", the tag ends up with twoclassattributes, of which browsers use only the first.post_class( 'post', $p )on its own covers both.- The closing
wp_reset_postdata()isn’t strictly needed when nothing changed the global post, but it does no harm.
Related articles
- adding page links to a custom WP_Query: get_posts() has no page count, so switch to WP_Query for pagination
- posts picked in an ACF Relationship field: another way to list chosen posts in your order

