TL;DR
WordPress 6.9 introduced a fix for adjacent post navigation when posts have identical publication dates. While this resolved a long-standing bug A bug is an error or unexpected result. Performance improvements, code optimization, and are considered enhancements, not defects. After feature freeze, only bugs are dealt with, with regressions (adverse changes from the previous version) being the highest priority., it inadvertently caused infinite loops in some extensions that modify the get_adjacent_post() WHERE clause.
What Changed in WordPress 6.9
In WordPress 6.9 (Trac #8107), a bug fix landed where next/previous post navigation failed when multiple posts shared identical post_date values. This commonly occurred when bulk-publishing draft posts.
The Technical Change
The get_adjacent_post() function’s WHERE clause was modified to include ID-based comparison as a tiebreaker:
Before (WordPress 6.8 and earlier):
WHERE p.post_date > '2024-01-01 12:00:00'
AND p.post_type = 'post'
After (WordPress 6.9):
WHERE (
p.post_date > '2024-01-01 12:00:00'
OR (
p.post_date = '2024-01-01 12:00:00'
AND p.ID > 123
)
)
AND p.post_type = 'post'
This ensures deterministic ordering when posts have identical dates, using the post ID as a secondary sort criterion.
Additionally, the ORDER BY clause was updated:
-- Before
ORDER BY p.post_date DESC
LIMIT 1
-- After
ORDER BY p.post_date DESC,
p.ID DESC
LIMIT 1
The Problem: Infinite Loops in Some Themes/Plugins
As Trac ticket #64390 documents, some plugins and themes modify adjacent post navigation to change behavior. For example, WooCommerce’s Storefront theme navigates between products instead of regular posts. These plugins use the get_{$adjacent}_post_where filter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. to replace the current post’s date with a different post’s date.
Here’s what was happening:
- Plugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party hooks In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same. into
get_previous_post_where filter.
- Plugin does string replacement: replaces
$current_post->post_date with $target_product->post_date.
- Problem: The new
WHERE clause structure includes the post date in TWO places (date comparison AND ID comparison).
- Simple string replacement modified the date comparison but left the ID comparison unchanged.
- Query returns the same post repeatedly → infinite loop The Loop is PHP code used by WordPress to display posts. Using The Loop, WordPress processes each post to be displayed on the current page, and formats it according to how it matches specified criteria within The Loop tags. Any HTML or PHP code in the Loop will be processed on each post. https://codex.wordpress.org/The_Loop..
Real-World Example: Storefront Theme
The fix to the WooCommerce Storefront theme illustrates this issue. They had to add special handling for the ID comparison:
// Replace the post date (works as before)
$where = str_replace( $post->post_date, $new->post_date, $where );
// NEW: Also need to replace the ID comparison (WordPress 6.9+)
if ( strpos( $where, 'AND p.ID ' ) !== false ) {
$search = sprintf( 'AND p.ID %s ', $this->previous ? '<' : '>' );
$target = $search . $post->ID;
$replace = $search . $new->ID;
$where = str_replace( $target, $replace, $where );
}
For Plugin Developers: Detecting and Fixing the Issue
How to Detect If Your Plugin Is Affected
Your plugin is likely affected if it:
- Uses the
get_{$adjacent}_post_where filter.
- Performs string replacement on post dates in the
WHERE clause.
- Changes which post is considered “adjacent” (like navigating between custom post types instead).
To test if your plugin works correctly with WordPress 6.9:
- Create 3-4 posts with identical publication dates (bulk publish drafts).
- Navigate between them using your plugin’s adjacent post functionality.
- Verify that navigation moves to different posts (not the same post repeatedly).
- Check for infinite loops or performance issues.
Quick Fix: Handle the ID Comparison
If you’re doing date replacement in the WHERE clause, you also need to handle the ID comparison. There are numerous ways to tie a knot, but the following example is loosely inspired by Storefront’s recent fix.
add_filter( 'get_next_post_where', 'example_custom_adjacent_post_where', 10, 5 );
add_filter( 'get_previous_post_where', 'example_custom_adjacent_post_where', 10, 5 );
function example_custom_adjacent_post_where( $where, $in_same_term, $excluded_terms, $taxonomy, $post ) {
// IMPORTANT: Replace this with your logic to find the desired adjacent post.
$adjacent_post = example_find_adjacent_post_function( $post );
if ( $adjacent_post instanceof WP_Post ) {
// Replace the date comparison.
$where = str_replace( $post->post_date, $adjacent_post->post_date, $where );
// Replace the post ID in the comparison.
$where = preg_replace(
"/AND p\.ID (<|>) {$post->ID}\)/",
"AND p.ID $1 {$adjacent_post->ID})",
$where
);
}
return $where;
}
You could also add a version_compare( $wp_version, '6.9', '>=' ) test if you’d like to support multiple versions.
Important: don’t forget to first test any code on your site, and customize it according to your needs.
Next time
At the time, this was not a known breaking change, and was classed as a bug fix.
However as @jmdodd points out in the ticket, changes to WP_Query, especially SQL changes should be, by default, communicated more widely. Going forward:
- Reach out to known plugins using the
get_{$adjacent}_post_where filter.
- Include a migration Moving the code, database and media files for a website site from one server to another. Most typically done when changing hosting companies. guidance in the 6.9 field guide The field guide is a type of blogpost published on Make/Core during the release candidate phase of the WordPress release cycle. The field guide generally lists all the dev notes published during the beta cycle. This guide is linked in the about page of the corresponding version of WordPress, in the release post and in the HelpHub version page. (as applicable).
- Test against any known, popular plugins that modify adjacent post queries.
What’s Next and Getting Help
Discussions have started on the ticket about ways to make this more robust in future WordPress versions. If you’re experiencing issues related to this change:
- Check Trac ticket #64390 for the latest updates.
- Ask questions in the #core channel on WordPress Slack Slack is a Collaborative Group Chat Platform https://slack.com/. The WordPress community has its own Slack Channel at https://make.wordpress.org/chat/..
- Review the original fix PR #10394.
Thank you for your patience and understanding. If you maintain a plugin affected by this change, please update it using the guidance above, and don’t hesitate to reach out if you need assistance.
Thanks to @westonruter @isabel_brison @andrewserong @jmdodd for helping to prepare this post.
#6-9, #dev-notes
You must be logged in to post a comment.