WordPress lists posts newest first on the blog page and in every archive. For a course, a story told in parts or a changelog you want the oldest post first, or an order by title.
This snippet changes the WordPress main loop order on the blog page and the archives with pre_get_posts. The theme templates stay untouched, and pagination keeps working.
The snippet
<?php
/**
* Change the order of the main post loop on the blog and archive pages.
*/
function dpc_loop_order() {
// ===== Config =====
// Oldest posts first. Other examples:
// array( 'title' => 'ASC' ) or array( 'menu_order' => 'ASC', 'date' => 'DESC' ).
$orderby = array( 'date' => 'ASC' );
// ===== End of config =====
return (array) apply_filters( 'dpc_loop_order', $orderby );
}
add_action( 'pre_get_posts', 'dpc_change_loop_order' );
function dpc_change_loop_order( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
// Only the blog page and archives, not search results or feeds.
if ( ! ( $query->is_home() || $query->is_archive() ) || $query->is_search() || $query->is_feed() ) {
return;
}
// Keep an order a visitor or another plugin chose, and WooCommerce's shop sorting.
if ( '' !== $query->get( 'orderby' ) || '' !== $query->get( 'wc_query' ) ) {
return;
}
$query->set( 'orderby', dpc_loop_order() );
}
How it works
pre_get_postsruns before WordPress queries the database, so changingorderbythere changes the posts, the page count and the pagination in one go. Running a second query in the template does not.$query->is_main_query()limits it to the page’s own loop. Widgets, related posts and Query Loop blocks with their own settings keep their order.- It applies to the blog page and archives (category, tag, author, date, custom post type and taxonomy archives), not to search results, feeds or wp-admin. The old version ran on every main query, so it also reordered the post lists in wp-admin and the search results.
- If the URL already has an
orderby(?orderby=title) or WooCommerce runs its shop query, the snippet leaves the order alone. - The old version ordered by
menu_order. Posts have no order field in the editor, so every post had 0 and the order was effectively random. The array formarray( 'menu_order' => 'ASC', 'date' => 'DESC' )gives a tie-breaker when you do use it.
Add it with Scripts Organizer
In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty. pre_get_posts fires after Scripts Organizer has loaded the code.
The same code also works unchanged in a small plugin or an mu-plugin.
Customise it
- Alphabetical:
array( 'title' => 'ASC' ). - Only one archive: add a condition such as
if ( ! $query->is_category( 'course' ) ) { return; }or$query->is_post_type_archive( 'event' ). - Custom order for a custom post type: register it with
'supports' => array( 'page-attributes' ), set the Order field per post and usearray( 'menu_order' => 'ASC', 'date' => 'DESC' ). - To restrict which posts appear rather than their order, see Limit search to post type for a
pre_get_postsexample on search.
Gotchas
- Sticky posts are still shown first on the blog page. Unstick them, or set
ignore_sticky_poststotruein the same callback. - Block themes: a Query Loop block with Inherit query from template follows this order, one with its own settings does not.
In the Scripts Organizer library Import it with one click from Scripts Organizer.