

The Arrange screen: a separate order for each category, shown as cards you can drag, move with buttons or the keyboard, with an ordering preview.
This plugin gives editors one focused screen (Tools Category Post Order) to:
Each term keeps its own order per content type. The same post can be 2nd in “Residential” and 9th in “Commercial”. The global order is stored separately and never mixed with term orders.
jdcpo argument.No external services, tracking, accounts or page builder required.
Enable them in Settings. The main query of an enabled archive uses the saved order unless:
?orderby=title, or a theme’s pre_get_posts sets orderby)Select a Query Loop block and open Curated post order in the block sidebar. Choose Global order or A term’s order… and pick the term. The editor preview and the published page use the same order. (Query Loops set to “Use default query / inherit” follow the archive integration instead.)
Pass the jdcpo argument to WP_Query, or use the helper jdcpo_query_args(), which also sets suppress_filters => false (required for get_posts()).
Global order of a custom post type:
$q = new WP_Query( jdcpo_query_args( array( 'post_type' => 'project', 'posts_per_page' => 12 ), 'global' ) );
One term’s order (the query still decides which posts appear):
$q = new WP_Query( jdcpo_query_args( array(
'post_type' => 'project',
'tax_query' => array( array( 'taxonomy' => 'project_category', 'terms' => 12 ) ),
), array( 'term' => 12 ) ) );
Automatic: the single selected term’s order, or the global order when no term is selected:
$q = new WP_Query( array( 'post_type' => 'project', 'project_category' => 'residential', 'jdcpo' => true ) );
Pinned first: nothing extra in code. Pinned-first vs manual is part of the saved order, so the same query follows whichever mode the editor chose.
Multi-term listing with a chosen ordering term, and a fallback to the global order when that term has no saved order:
'jdcpo' => array( 'term' => 'residential', 'taxonomy' => 'project_category', 'fallback' => 'global' )
Accepted values: true / 'auto', 'global', 'term:123', or an array with term (ID or slug), taxonomy (required for slugs), post_type, fallback ('global' or 'none').
Use the same arguments on every page request. Pagination stays correct because only the ORDER BY changes.
add_action( 'wp_ajax_nopriv_projects_more', 'mytheme_projects_more' );
add_action( 'wp_ajax_projects_more', 'mytheme_projects_more' );
function mytheme_projects_more() {
$page = max( 1, absint( $_POST['page'] ?? 1 ) );
$term = absint( $_POST['term'] ?? 0 );
$query = new WP_Query( jdcpo_query_args( array(
'post_type' => 'project',
'post_status' => 'publish', // admin-ajax runs as "admin": always set the status.
'posts_per_page' => 9,
'paged' => $page,
'tax_query' => $term ? array( array( 'taxonomy' => 'project_category', 'terms' => $term ) ) : array(),
), $term ? array( 'term' => $term ) : 'global' ) );
ob_start();
while ( $query->have_posts() ) { $query->the_post(); get_template_part( 'parts/project-card' ); }
wp_reset_postdata();
wp_send_json_success( array( 'html' => ob_get_clean(), 'more' => $page < $query->max_num_pages ) );
}
Important: requests to admin-ajax.php run with is_admin() === true, where WordPress includes drafts in queries without an explicit status. Always set post_status => 'publish' in public AJAX handlers. That is standard WordPress practice, not something this plugin changes.
jdcpo_explain_query( $query ) returns the same plain-language reason.jdcpo_loaded (action): fires after the plugin has loaded. Add-ons hook in here.jdcpo_order_config (filter): ( array $config, Scope $scope, WP_Query $query ). Adjust pins or positions for one query just before ORDER BY is built. It cannot change which posts are returned.jdcpo_order_saved (action): ( Scope $scope, array $config ). Fires after an order is saved or undone on the Arrange screen.jdcpo_available_post_types (filter): ( WP_Post_Type[] $types ). The content types that can be enabled in Settings.jdcpo_scope_capability (filter): ( string $cap, Scope $scope ). Who can arrange a scope.get_posts() skips query filters by default. Pass suppress_filters => false, or use jdcpo_query_args().jdcpo argument.