Make WordPress Core

Opened 4 weeks ago

Closed 4 weeks ago

#65868 closed defect (bug) (duplicate)

Sticky posts appear twice: at the top of page 1 and again in date order on a later page

Reported by: sanket.parmar Owned by:
Priority: normal Milestone:
Component: Query Version: 7.1
Severity: normal Keywords:
Cc: Focuses:

Description

When a sticky post is old enough to fall on a later page of a paginated posts index, it is rendered twice: once pinned at the top of page 1, and again in its natural chronological position on the later page.

Steps to reproduce

On a clean install with the default block theme (Twenty Twenty-Five) and the posts index as the front page:

wp option update posts_per_page 5

# 12 posts, one per day going backwards
wp eval 'for ($i = 1; $i <= 12; $i++) {
  $d = date("Y-m-d H:i:s", strtotime("-{$i} days 10:00"));
  wp_insert_post([
    "post_title"  => "QL test post {$i}",
    "post_status" => "publish",
    "post_type"   => "post",
    "post_date"   => $d,
  ]);
}'

# make an older post sticky - one that naturally falls on page 3
wp option update sticky_posts '[<ID of QL test post 10>]' --format=json

Then view /, /page/2/ and /page/3/.

Expected

The sticky post is shown once, pinned to the top of page 1.

Actual

With 15 published posts at 5 per page and "QL test post 10" (dated August 3) sticky.

Page 1 - the sticky post is pinned above posts dated August 12, 11 and 10, which is correct. Note also that six posts are rendered on a five-per-page setting:


Page 3 - the same post is rendered a second time, in its natural date position between "QL test post 9" and "QL test post 11":


Not specific to the block editor

This was first reported against the Gutenberg Query Loop block. It is not specific to that block. With every plugin deactivated, including Gutenberg:


page 3 still renders the duplicate:


The theme's Query Loop uses "inherit":true, so the posts come from the main query rather than from any block-specific query code.

Where it comes from

In wp-includes/class-wp-query.php, sticky handling is gated on the first page only:

$sticky_posts = get_option( 'sticky_posts' );
if ( $this->is_home && $page <= 1 && is_array( $sticky_posts ) && ! empty( $sticky_posts ) && ! $query_vars['ignore_sticky_posts'] ) {

That block reorders the current page's results in memory and prepends any sticky post that was not already among them. Nothing removes sticky posts from the queries that run for pages 2 and above, so the post keeps its slot in normal date-ordered pagination and is rendered a second time.

This also accounts for page 1 returning one more item than posts_per_page when the sticky post was not already in that page's result set.

Question

Is the duplication intended, or should sticky posts be excluded from the result set on subsequent pages? The $page <= 1 gate looks deliberate, so this may be long-standing accepted behaviour rather than a regression, in which case documenting it would help.

Environment

  • WordPress 7.0.4
  • Theme: Twenty Twenty-Five (block theme, posts index as front page)
  • All plugins deactivated for the screenshots above
  • PHP 8.3, MariaDB 11.8

Notes

Originally reported at https://github.com/WordPress/gutenberg/issues/80624, where the reporters describe this as a change between 7.0.1 and 7.0.2. I was not able to verify that; I tested only 7.0.4, and the $page <= 1 gate above suggests the behaviour may predate it. I also tested only a block theme, not a classic theme.

Attachments (4)

1-page-1-sticky-pinned.jpg (20.4 KB ) - added by sanket.parmar 4 weeks ago.
2-page-3-same-post-again.jpg (20.4 KB ) - added by sanket.parmar 4 weeks ago.
3-gutenberg-deactivated.jpg (115.3 KB ) - added by sanket.parmar 4 weeks ago.
4-page-3-still-duplicated-without-gutenberg.jpg (20.4 KB ) - added by sanket.parmar 4 weeks ago.

Download all attachments as: .zip

Change History (6)

#1 @khokansardar
4 weeks ago

Confirming the report on current trunk, and adding some ticket history, since the $page <= 1 gate is deliberate and this has come up several times before.

Reproduced on trunk with 23 posts, posts_per_page 10, and stickies at fixture index 2, 14 and 8:

page 1 (11): 2,8,14,0,1,3,4,5,6,7,9
page 2 (10): 10,11,12,13,14,15,16,17,18,19
duplicated across pages 1 and 2: 14

Both symptoms hold: page 1 returns posts_per_page + 1, and a sticky whose natural date position falls on a later page is rendered a second time there.

Prior art

The gate dates back to the original sticky posts implementation in WordPress 2.7 (#7457), and the trade-off has been argued before:

  • #9300 — page 1 returning posts_per_page + 1 when a sticky falls outside the page. Closed wontfix. In comment 5, Denis-de-Bernardy sets out the cost: stickies would have to be resolved first and the LIMIT adjusted, and "we could end up stripping stickies from all non singular queries where they might appear, but the performance impact is not benign".
  • #27282 — the same underlying issue, still open, milestoned Future Release with 2nd opinion. bendoh attached trac-27872-on-31418.diff, which folds sticky handling into the SQL query, and notes "paging will work correctly with any number of sticky posts". That patch addresses exactly the duplication reported here and has not been reviewed.
  • #11950 — "Sticky Posts are too aggressive", reopened, has-patch.
  • #23336 — sticky posts lack sanity bounding, with the performance angle.

An existing test overstates its coverage

tests/phpunit/tests/query/stickies.php already has a test that reads as though it covers this:

<?php
public function test_stickies_should_not_be_included_on_pages_other_than_1() {
      $this->go_to( '/?paged=2' );
      $found = wp_list_pluck( $GLOBALS['wp_query']->posts, 'ID' );
      $this->assertNotContains( self::$posts[2], $found );
}

self::$posts[2] is the third-newest post in that fixture, so it sits on page 1 naturally and could never appear on page 2 — the assertion passes either way. The sticky that does land on page 2 in the same fixture, self::$posts[14], is never checked. The name claims coverage the test does not provide, which may be part of why this went unnoticed.

Question

Is this worth pursuing on its own, or should it be closed as a duplicate of #27282? I have not attached a patch: excluding stickies from later pages moves found_posts, max_num_pages and the composition of every page of the main home query on every site, and that decision looks like it belongs with #27282 and the wontfix on #9300 rather than being settled here.

Use of AI Tools

AI assistance: Yes
Used for: reproducing the report against trunk, and researching the related tickets and test coverage cited above. All findings were reviewed and verified by me.

#2 @wildworks
4 weeks ago

  • Milestone Awaiting Review
  • Resolutionduplicate
  • Status newclosed

I think the root cause of this ticket is the same as #27282, so I am closing this as a duplicate.

Note: See TracTickets for help on using tickets.