Photo of three warehouse doors

So far Nelio has published several plugins in the WordPress.org repository. Some of them are completely free and offer elegant solutions to common problems, such as, for example, Nelio Maps, which allows you to insert a Google map on your pages or posts created with Gutenberg, and Nelio Compare Images, which allows you to insert a block to, as it names suggests, compare two images side by side. We also published a couple of premium plugins that help us pay the bills: Nelio Content and Nelio A / B Testing.

If you take a look at our website, you will see that our two premium plugins occupy a prominent place, as they both have a few landing and pricing pages and they both have a related knowledge base where users can find the answer to their questions. Today we’ll be talking about how we created the documentation pages for Nelio A/B Testing and Nelio Content and what we had to do to sort questions as we please.

How to create custom post types in WordPress

First, let’s create a custom post type to keep track of all the questions our knowledge base will contain. You can see the result in the following screenshot, where there’s an instance of this new post type for Nelio A/B Testing’s knowledge base:

Nelio A/B Testing's Knowledge base
Example of a question in Nelio A/B Testing’s Knowledge Base.

To create a custom post type, all you have to do is use the WordPress register_post_type function as follows:

function nelio_add_help_type() {
  $labels = array(
    'name'          => __( 'Questions', 'nelio-help' ),
    'singular_name' => __( 'Question', 'nelio-help' ),
    'menu_name'     => __( 'Testing’s Help', 'nelio-help' ),
    'all_items'     => __( 'All Questions', 'nelio_help' ),
    'add_new_item'     => __( 'Add New Question', 'nelio_help' ),
    'edit_item'     => __( 'Edit Question', 'nelio_help' ),
    'view_item'     => __( 'View Question', 'nelio_help' ),
  );
  register_post_type( 'nab_help', array(
    'capability_type' => 'post',
    'labels'          => $labels,
    'map_meta_cap'    => true,
    'menu_icon'       => 'dashicons-welcome-learn-more',
    'public'          => true,
    'show_in_rest'    => true,
    'supports'        => [ 'title', 'editor', 'author' ],
  ) );
}
add_action( 'init', 'nelio_add_help_type' );

and voilà! You may already know there are plugins like Advanced Custom Fields that help you create and customize the interface of a custom post types, but I don’t necessarily recommend it to implement such a simple example.

By the way, if you’re wondering where this snippet should be placed, don’t miss our tutorial on how to customize WordPress with custom plugins.

How to create a new taxonomy to organize content

Our knowledge base is organized in “Topics” and “Keywords,” as you can see in the following screenshot:

Screenshot of the WordPress Dashboard showing our new post type with its two taxonomies: Topics and Keywords.
Screenshot of the WordPress Dashboard showing our new post type Testing’s Help with its two taxonomies: Topics and Keywords.

Topics and Keywords are the equivalent of the Categories and Tags that you have by default in WordPress posts. Let’s just focus on the former, as the latter is exactly the same.

As you can already imagine, a Topic is a custom taxonomy we have created for our custom content type. To add a new taxonomy to a WordPress post type, you simply have to use the register_taxonomy function as follows:

function nelio_add_help_taxonomy() {
  $labels = array(
    'name'          => __( 'Topics', 'nelio-help' ),
    'singular_name' => __( 'Topic', 'nelio-help' ),
    'menu_name'     => __( 'Topics', 'nelio-help' ),
    'all_items'     => __( 'All Topics', 'nelio_help' ),
    /* ... */
  );
  register_taxonomy( 'nab_topic', [ 'nab_help' ], array(
    'hierarchical'      => true,
    'label'             => __( 'Topic', 'nelio-help' ),
    'query_var'         => true,
    'show_admin_column' => false,
    'show_ui'           => true,
    'show_in_rest'      => true,
  ) );
}
add_action( 'init', 'nelio_add_help_taxonomy' );

The function is quite straightforward to use. First, you name the taxonomy (nab_topic in our example). Then you specify the post type(s) it’s related to (we only have one: nab_help). Finally, you add a few more arguments to customize the taxonomy. The result is a new taxonomy with which to create the concrete Topics we need to categorize the questions in our knowledge base:

Screenshot of a custom taxonomy in the WordPress Dashboard.
Screenshot of our custom taxonomy (Topics).

How to sort the posts in a Taxonomy

Now that we have a custom taxonomy and we can add questions to each Topic we create, how do we sort those questions within a specific topic? Well, I’m afraid we’ll have to code a little bit…

Nelio A/B Testing

Native Tests for WordPress

Use your WordPress page editor to create variants and run powerful tests with just a few clicks. No coding skills required.

Taking a look at the database

WordPress and their relationships with your posts are stored in four different database tables:

WordPress 2.5 added a new numeric field in the wp_term_relationships table, term_order, which allows us to specify the position that a certain element occupies within the taxonomy. This sounds like the candidate we were looking for to sort our questions within our topic… but there’s a catch: as far as I can tell, WordPress doesn’t offer a standard mechanism to (1) define the value of the term_order field and (2) use it to actually sort the posts within a certain taxonomy. Let’s fix this!

How to use the term_order field to sort the posts included in a taxonomy

Let’s suppose we were able to somehow set the desired term_order values we want, as you can see in the following screenshot:

Screenshot of the order we see in the database and the one that actually appears on the web.
The order that we see in the database (and that is what we would expect to see) is different than the one we actually get when browsing our website.

By default, WordPress ignores this field and the posts aren’t sorted in the front-end. That’s why the three questions shown in the screenshot above aren’t properly sorted.

Luckily, we can easily tell WordPress to use the field to sort the posts by it. Just use the posts_orderby filter to tweak the query that retrieves the posts belonging to a certain term so that it includes a sorting request:

add_filter( 'posts_orderby', 'nelio_sort_questions_in_topic', 99, 2 );
function nelio_sort_questions_in_topic( $orderby, $query ) {
  if ( ! nelio_is_topic_tax_query( $query ) ) return;
  global $wpdb;
  return "{$wpdb->term_relationships}.term_order ASC";
}
function nelio_is_topic_tax_query( $query ) {
  if ( empty( $query->tax_query ) ) return;
  if ( empty( $query->tax_query->queries ) ) return;
  return in_array(
    $query->tax_query->queries[0]['taxonomy'],
    [ 'nab_topic' ],
    true
  );
}

All the previous snippet does is to check that we’re querying a certain taxonomy (in our case, nab_topic) and, when we do, we add an ORDER BY clause in the query so that the final result is ordered by the value of the term_order attribute found in the $wpdb->term_relationships table:

Screenshot of the results (ordered) of a taxonomy
Screenshot of (sorted) posts within a taxonomy.

Implementation of a user interface to order the contents of a taxonomy

Finally, we only have one thing left to do. In the previous section, we assumed we were able to somehow set the correct value in the term_order field. But how? Is there a screen or option somewhere to set this value in a user-friendly manner? As far as I know, there isn’t. But we can create one.

In my opinion, the best solution to this problem would be to have a UI that allows me to select the taxonomy that I want to sort, and then drag and drop all the questions to establish their order. Something like this, go:

User interface to easily sort the posts we have in a certain taxonomy.
User interface to easily sort the posts we have in a certain taxonomy.

To achieve the desired effect we must implement the following steps:

  • Register a new page on the WordPress dashboard
  • Implement the function that will render the taxonomy selector and the posts within the selected taxonomy
  • Add a tiny JavaScript snippet so that the UI is responsive
  • Create a new callback to save the resulting order once we’re done sorting the posts

You can make this interface as complicated as you want, using the new WordPress development stack. But today I’m going to outline a quick and dirty solution that gets the job done.

As I was saying, the first thing is to register the page where we will put the interface. We can easily do this with the add_submenu_page function during the admin_menu action:

add_action( 'admin_menu', function() {
  add_submenu_page(
    'edit.php?post_type=nab_help',
    'Sort',
    'Sort',
    'edit_others_posts',
    'nab-help-question-sorter',
    __NAMESPACE__ . '\render_question_sorter'
  )
} );

The render_question_sorter method i also extremely simple:

function render_question_sorter() {
  echo '<div class="wrap"><h1>Sort Questions</h1>';
  $terms = get_terms( 'nab_topic' );
  render_select( $terms );
  foreach ( $terms as $term ) {
    render_questions_in_term( $term );
  }
  render_script();
  echo '</div>';
}

As you can see, we simply retrieve all terms in the nab_topic taxonomy and then we rely on three auxiliary functions to (1) render the selector, (2) render the questions in each category and (3) add a small script to make everything dynamic.

Rendering the selector is as easy as iterating over each term and render an option:

function render_select( $terms ) {
  echo '<select id="topic">';
  foreach ( $terms as $term ) {
    printf(
      '<option value="%s">%s</option>',
      esc_attr( $term->slug ),
      esc_html( $term->name )
    );
  }
  echo '</select>';
}

To render the questions we have in each term of the taxonomy, we launch a query to the database and iterate through all of them:

function render_questions_in_term( $term ) {
  printf(
    '<div id="%s" class="term-block" data-term-id="%d">',
    esc_attr( $term->slug ),
    esc_attr( $term->term_id )
  );
  echo '<div class="sortable">'
  $query = new WP_Query( array(
    'post_type'     => 'nab_help',
    'post_per_page' => -1,
    'tax_query'     => array(
      array(
        'taxonomy' => 'nab_topic',
        'field'    => 'term_id',
        'terms'    => $term->term_id,
        'orderby'  => 'term_order',
      ),
    ),
  ) );
  while ( $query->have_posts() ) {
    $query->the_post();
    global $post;
    printf(
      '<div class="question" data-question-id="%d">%s</div>',
      esc_attr( $post->ID ),
      esc_html( $post->post_title )
    );
  }
  echo '</div>';
  printf(
    '<input type="button" data-term-id="%s" value="%s" />',
    esc_attr( $term->term_id ),
    esc_attr( 'Save' )
  );
  echo '</div>';
}

Note this function renders a div that uses the slug of the current $term as its ID, it groups the questions in yet another div element, and it finally includes a Save button. All these details will be useful when we finally render the script that wires our app.

Once everything’s ready, we simply need to add some JavaScript magic. In particular, this quick and dirty UI requires two scripts (jquery and jquery-ui) to implement the drag-and-drop functionality and then the following JS snippet:

( function() {
  // MAKE QUESTIONS SORTABLE
  $( '.sortable' ).sortable();

  // TERM SELECTOR
  const select = document.getElementById( 'topic' );
  const termBlocks = [ ...document.querySelectorAll( '.term-block' ) ];
  select.addEventListener( 'change', showSelectedTermBlock );
  // Helper function
  function showSelectedTermBlock() { /* ... */ }

  // SAVE BUTTONS
  [ ...document.querySelectorAll( '.term-block input[type="button"]' ) ].forEach( addButtonLogic );

  // Helper function
  function addButtonLogic( button ) {
    const termId = button.getAttribute( 'data-term-id' );
    button.addEventListener( 'click', () => {
      button.disabled = true;
      const selector = `div[data-term-id="${ termId }"] .question`;
      const ids = [ ...document.querySelectorAll( selector ) ]
        .map( ( question ) => question.getAttribute( 'data-question-id' );
      $.ajax( {
        url: ajaxurl,
        method: 'POST',
        data: {
          action: 'nelio_save_tax_sorting',
          objectIds: ids,
          termId,
        },
      } ).always( () => button.disable = false );
    }
  }
} )()

At first glance, the previous snippet might seem complicated. But I promise: it isn’t. First, we make our questions “sortable” by using jQuery UI’s sortable function. Then, we add a listener to our select component that shows/hides the appropriate set of questions using a helper function. Finally, we add the click listeners to our Save buttons so that, when the user clicks on them, the order in which the questions are sorted is stored in the database.

As you can see, saving this new order is done via an AJAX request, which means we need to implement its PHP callback counterpart:

add_action( 'wp_ajax_nelio_save_tax_sorting', __NAMESPACE__ . '\save_tax_sorting' );
function save_tax_sorting() {
  $term_id = isset( $_POST['termId'] ) ? absint( $_POST['termId'] : 0 );
  if ( ! $term_id ) die();
  $object_ids = isset( $_POST['objectIds'] ) ? $_POST['objectIds'] : [];
  if ( ! is_array( $object_ids ) || empty( $object_ids ) ) die();
  $object_ids = array_values( array_map( 'absint', $object_ids ) );
  global $wpdb;
  foreach ( $object_ids as $order => $object_id ) {
    $wpdb->update(
      $wpdb->term_relationships,
      array( 'term_order' => $order + 1 ),
      array(
        'object_id'        => $object_id,
        'term_taxonomy_id' => $term_id,
      ),
    );
  }
  die();
}

And that’s it! Quite easy, huh?

In summary

WordPress doesn’t offer a default mechanism to sort posts within a certain taxonomy. But that’s OK! We can implement one ourselves: all we have to do to sort posts in a WordPress taxonomy is set the term_order field in the term_relationships database table and then extend WordPress with the posts_orderby filter so that it uses said field.

Finally, we have outlined a simple UI to easily populate the term_order field by dragging and dropping the posts included in a certain taxonomy.

I hope you liked this tutorial and if you did, please share it with your friends and colleagues. And, as always, if you have any questions, leave them in the comment section below and I’ll be glad to address them.

Featured image by Steve Johnson on Unsplash.

20 responses to “How to Sort Posts within a WordPress Taxonomy”

  1. MB Avatar
    MB

    Thank you for great tutorial. Could you please put your code to file and publish it in a repo? That would really help. Thank you in advance!

    1. David Aguilera Avatar

      Sure! I’ll get back to you once I’ve done it 🙂

  2. Max Avatar
    Max

    Waiting for repo 😉

    1. David Aguilera Avatar

      Oops, I’m sorry, Max – I forgot. Here you have it! Let me know if it works correctly 🙂

      1. Max Avatar
        Max

        Thank you very much! Works like a charm!

      2. Max Avatar
        Max

        I would buy a plugin which will help me to soft my custom posts according to my custom taxonomies. Together with ACF it is a great mix! I also would need a good php code example of how to print my data on the site.

        It is very useful in many cases. Your great article and code examples is a working prototype!

        Thank you a lot!

        1. David Aguilera Avatar

          I’m glad this is helpful 🙂

        2. Kathy Avatar
          Kathy

          Have a look at Post Types Order plugin https://wordpress.org/plugins/post-types-order/ , available free or as an advanced version with additional sorting functionality.

  3. Max Avatar
    Max

    Is it possible to have one question in few different topics?

  4. Luis Escobar Avatar
    Luis Escobar

    Hi David,

    I would like to hire you to implement this functionality on our website.

    Is this possible?

    I’d like to schedule a meeting as soon as possible if you are interested.

    Thank you

    1. David Aguilera Avatar

      Hi, Luis. You can use the code provided in the post to implement this functionality – you don’t need us for that (and it’ll be cheaper 🤗).

      Did you try it? Where did you get stuck?

  5. Paul Avatar
    Paul

    Hi David,

    Thanks for this great tutorial, it works like a charm.
    I used your code for two of my cpt (tickets & packages).
    Unfortunatly, i get stuck with add_filter of one cpt (it works well for packages, but not for tickets, even if the term_order get the right value in the term_relationships table).
    Can this be caused due to a conflict with the first add_filter to posts_orderby ?

    Thanks in advance for your feedback !

    1. David Aguilera Avatar

      Honestly, I don’t know. I’m using this approach on multiple post types and they don’t interfere with each other, but who knows!

      Anyhow, here’s what I’d do: disable “the first add_filter” and look at your tickets. Are they properly sorted now? If they are, your hooks interfere with each other and you’ll have to explore why. You probably need to make sure that each sorting hook only applies to the correct post type. If it doesn’t work, the tickets issue has nothing to do with your packages hooks. Again, look at your hooks and make sure they only apply to the appropriate post type.

  6. Michael Avatar
    Michael

    This is fantastic David – thanks for publishing!
    Using the simple version posts_orderby (in my case I am sorting by a CPT tag) could we THEN sort by date modified ACF?

    So… sort (ASC) by CPT Tag
    Then sort (DESC) by date modified ACF

    Thanks again!

    1. David Aguilera Avatar

      Probably. Is this “date modified ACF” a custom post meta? You can always tweak the SQL query to match your needs.

      1. Michael Avatar
        Michael

        Thanks David – it is a CP meta – just not sure how to tweak it to add the date function. I also realized that using posts_orderby is only working because I happened to add my CPT tags alphabetically. In which case if I add one down the line it will push it to the end of the list. Unless I am reading how posts_orderby works? I could be very wrong with this 🙂

        Also OT – how well does your Nelio Unlocker work going from Elementor to WPBakery – interesting plugin. And how far away are you from implementing multi-step in your forms?

        1. David Aguilera Avatar

          You can basically hook into the posts_clauses_request and add the missing bits to the SQL query so that you can retrieve the data as you need. It’s “low level,” but it’ll get the job done (and it’s probably easier this way).

          Regarding alphabetical CPT tags… yup, that’s a problem when it comes to sort the tags themselves. In our case, we added a number before the tag itself (you can see it in the screenshot) so that “alphabetical order” becomes the order we want.

          Nelio Unlocker converts websites to Gutenberg. You can’t use it to convert a page to WPBakery, sorry.

          Finally, multi-step forms is something we’d like to do in the future… but there’s no ETA at all. We’re currently focused on Nelio A/B Testing and Nelio Content.

  7. rebecca mantel Avatar
    rebecca mantel

    Hello
    I have copied your code but the button save doesnt save the order. Any idea?

    1. David Aguilera Avatar

      I don’t know. If I were you, I’d edit save_tax_sorting and add a few logs (i.e. echo xxxxx; and so on) to check if everything is being properly saved.

Leave a Reply

Your email address will not be published. Required fields are marked *

I have read and agree to the Nelio Software Privacy Policy

Your personal data will be located on SiteGround and will be treated by Nelio Software with the sole purpose of publishing this comment here. The legitimation is carried out through your express consent. Contact us to access, rectify, limit, or delete your data.