Sort categories by custom field in WordPress admin

For some reason the add_action( 'pre_get_posts', 'category_custom_orderby' ); hook isn’t working.

Yes, and it’s because on the Categories admin page (where you can see the Categories list table), the list table uses WP_Term_Query and not WP_Query, hence the above hook (pre_get_posts) is not fired.

So instead of the above add_action(), try using the parse_term_query hook like so:

  • If all of your categories have the sort_order meta,

    add_action( 'parse_term_query', 'my_parse_term_query' );
    function my_parse_term_query( $query ) {
        $args =& $query->query_vars;
    
        if ( is_admin() && 'sort_order' === $args['orderby'] ) {
            $args['meta_key'] = 'sort_order';
            $args['orderby']  = 'meta_value_num';
        }
    }
    
  • If there are categories that do not have the sort_order meta,

    add_action( 'parse_term_query', 'my_parse_term_query' );
    function my_parse_term_query( $query ) {
        $args =& $query->query_vars;
    
        if ( is_admin() && 'sort_order' === $args['orderby'] ) {
            $args['meta_query'] = isset( $args['meta_query'] ) ?
                (array) $args['meta_query'] : array();
    
            $args['meta_query'][] = array(
                'relation'      => 'OR',
                'sort_order'    => array( // include categories that have the meta
                    'key'  => 'sort_order',
                    'type' => 'NUMERIC',
                ),
                'no_sort_order' => array( // and categories that don't have the meta
                    'key'     => 'sort_order',
                    'compare' => 'NOT EXISTS',
                ),
            );
        }
    }
    

And note that I used the $args['orderby'] = 'meta_value_num' and 'type' => 'NUMERIC' because I presumed the meta value is a number?

Also, the =& as in $args =& $query->query_vars means that we’re referencing the original $query_vars property in the WP_Term_Query class. So if we modify the $args variable, it means we’re actually modifying the original $query_vars property.