How to Create CPT Archives with Custom Ordering Logic
A Custom Post Type (CPT) archive often needs ordering that goes beyond “latest first”.
Common requirements include:
- Sort by a numeric custom field (priority, price, ranking)
- Sort by multiple fields (featured first, then date)
- Apply different ordering rules per taxonomy term
- Show “pinned” posts first, then normal posts
This article shows WordPress-friendly, code-based patterns to build CPT archives with custom ordering,
using pre_get_posts, safe meta queries, and SQL-order customization when needed.
Decide Where the Ordering Should Live
There are two common approaches:
- Use
pre_get_poststo modify the main archive query (recommended) - Use a custom
WP_Queryinside a template (only if you need a fully custom loop)
For a standard CPT archive URL like /events/ or /news/, modifying the main query is usually best.
Baseline: Target Only the CPT Archive Main Query
Always scope your logic carefully to avoid impacting admin queries or other loops.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() ) {
return;
}
if ( ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'event' ) ) {
return;
}
// Custom ordering goes here.
} );
1) Sort a CPT Archive by a Numeric Custom Field
A common pattern: sort by a numeric meta key like event_start_ts or priority.
Use meta_value_num for numeric sorting.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'event' ) ) {
return;
}
$query->set( 'meta_key', 'event_start_ts' );
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'order', 'ASC' );
} );
This produces a chronological event archive (soonest first).
2) Sort by Multiple Rules (Featured First, Then Date)
To prioritize featured items, you can order by a meta value first, then date.
Use an orderby array.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'news' ) ) {
return;
}
$query->set( 'meta_key', 'is_featured' );
$query->set( 'orderby', array(
'meta_value_num' => 'DESC',
'date' => 'DESC',
) );
} );
This works well if is_featured is consistently set.
However, missing meta values can cause unexpected ordering.
Make Featured Sorting Stable When Meta Is Missing
If most posts do not have the meta key, add a meta query with an OR clause so missing values are treated as 0.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'news' ) ) {
return;
}
$query->set( 'meta_query', array(
'relation' => 'OR',
array(
'key' => 'is_featured',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'is_featured',
'value' => '1',
'compare' => '=',
),
array(
'key' => 'is_featured',
'value' => '0',
'compare' => '=',
),
) );
$query->set( 'meta_key', 'is_featured' );
$query->set( 'orderby', array(
'meta_value_num' => 'DESC',
'date' => 'DESC',
) );
} );
This increases query complexity, so use it only if ordering stability matters.
3) Pinned Posts First (Using post__in + Orderby)
If you maintain a list of pinned IDs (via an option, ACF, or admin UI),
you can force them to the top of the archive and keep normal pagination.
Because post__in overrides ordering, you typically do this as a two-step approach:
- Page 1: pinned posts first, then normal posts
- Page 2+: normal posts only (avoid repeating pinned items)
Example: Pinned on Page 1 Only
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'news' ) ) {
return;
}
$pinned_ids = get_option( 'news_pinned_ids', array() );
$pinned_ids = array_filter( array_map( 'absint', (array) $pinned_ids ) );
$paged = max( 1, (int) $query->get( 'paged' ) );
if ( $paged === 1 && ! empty( $pinned_ids ) ) {
// Include pinned posts in the query, prioritize them via posts_orderby.
$query->set( 'post__in', $pinned_ids );
$query->set( 'orderby', 'post__in' );
$query->set( 'order', 'ASC' );
return;
}
// Page 2+ should exclude pinned posts to prevent repeats.
if ( ! empty( $pinned_ids ) ) {
$query->set( 'post__not_in', $pinned_ids );
}
$query->set( 'orderby', 'date' );
$query->set( 'order', 'DESC' );
} );
Note: post__in returns only those posts. If you want “pinned + normal” in one page,
you need a merge approach (two queries) or a custom SQL ordering.
4) True “Pinned + Normal” in One Archive Page (Two Queries Merge)
If you want pinned posts at the top and fill the rest with normal posts,
you can run two queries and merge IDs safely.
This works well when:
- The pinned list is short
- You want full control over ordering
- You accept a bit more code complexity
<?php
function wpct_get_news_archive_ids( int $paged, int $per_page ): array {
$pinned_ids = get_option( 'news_pinned_ids', array() );
$pinned_ids = array_values( array_filter( array_map( 'absint', (array) $pinned_ids ) ) );
if ( $paged < 1 ) {
$paged = 1;
}
// Page 1: include pinned, then fill with normal posts.
if ( $paged === 1 && ! empty( $pinned_ids ) ) {
$remaining = max( 0, $per_page - count( $pinned_ids ) );
$normal = new WP_Query( array(
'post_type' => 'news',
'posts_per_page' => $remaining,
'fields' => 'ids',
'post__not_in' => $pinned_ids,
'orderby' => 'date',
'order' => 'DESC',
'no_found_rows' => true,
) );
return array_values( array_unique( array_merge( $pinned_ids, $normal->posts ) ) );
}
// Page 2+: normal posts only, offset by pinned count on page 1.
$offset = 0;
if ( ! empty( $pinned_ids ) ) {
$offset = max( 0, $per_page - count( $pinned_ids ) );
}
$normal_paged = $paged;
$normal_offset = 0;
if ( $paged > 1 && $offset > 0 ) {
// For page 2, we already displayed (per_page - pinned_count) normal posts on page 1.
// So we need to skip that many.
$normal_offset = $offset + ( $per_page * ( $paged - 2 ) );
$normal_paged = 1;
}
$normal = new WP_Query( array(
'post_type' => 'news',
'posts_per_page' => $per_page,
'fields' => 'ids',
'post__not_in' => $pinned_ids,
'orderby' => 'date',
'order' => 'DESC',
'offset' => $normal_offset,
'no_found_rows' => true,
) );
return $normal->posts;
}
This is powerful, but pagination totals become custom work because you’re no longer using a single main query.
For most sites, a simpler approach is better.
5) Conditional Ordering Based on Taxonomy Term
Sometimes each archive “sub-view” needs different ordering.
For example, event_type=upcoming sorts ascending by start date,
while other terms sort by publish date.
<?php
add_action( 'pre_get_posts', function ( $query ) {
if ( is_admin() || ! $query->is_main_query() ) {
return;
}
if ( ! $query->is_post_type_archive( 'event' ) && ! $query->is_tax( 'event_type' ) ) {
return;
}
if ( $query->is_tax( 'event_type', 'upcoming' ) ) {
$query->set( 'meta_key', 'event_start_ts' );
$query->set( 'orderby', 'meta_value_num' );
$query->set( 'order', 'ASC' );
return;
}
$query->set( 'orderby', 'date' );
$query->set( 'order', 'DESC' );
} );
Performance Notes (Ordering by Meta Can Be Expensive)
- Prefer numeric timestamps stored as a single meta key for ordering (e.g.,
event_start_ts) - Avoid
LIKEmeta comparisons for archive sorting - Keep meta query conditions minimal on large archives
- Consider adding an indexed custom column only if you truly need it (advanced)
Common Mistakes
- Forgetting to scope to
is_main_query()and breaking secondary loops - Applying ordering changes in admin screens
- Using
meta_valuefor numeric fields (should bemeta_value_num) - Expecting
post__into return “pinned + normal” without extra logic
Summary
- Use
pre_get_poststo customize CPT archive ordering reliably - For numeric sorting, set
meta_keyand usemeta_value_num - For multi-rule ordering, use
orderbyarrays (and handle missing meta carefully) - Pinned behavior can be simple (exclude on page 2+) or advanced (merge queries)
- Always scope and test to avoid unintended query side effects
Custom ordering is one of the most valuable improvements you can make to CPT archives.
Done correctly, it keeps archives meaningful for users while remaining maintainable and performant.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.