How to sort empty values last in WordPress

For the past several days I’ve been hammering my head against a conundrum: how to get WordPress to sort a set of posts in ascending order, but with empty values at the end of the list instead of the beginning.

This seems like it should be a simple option in the query. But MySQL doesn’t offer a straightforward way to do this. There are some fairly simple MySQL tricks that will accomplish it, but there’s no way to apply those tricks within the context of WP_Query because they require manipulating either the SELECT or ORDER BY portions of the SQL query in ways WP_Query doesn’t allow. (I mean, you can write custom SQL for WP_Query, but if you’re trying to alter the output of the main query, good luck.)

I tried everything I could possibly think of yesterday with the pre_get_posts hook, but it all went nowhere, other than discovering a very weird quirk of MySQL that I don’t fully understand and won’t bother explaining here.

Sleep on it

I woke up this morning with an idea! I resigned myself to the fact that this ordering can’t happen before the query runs, but I should be able to write a pretty simple function to do it after the query has run.

Bear one key thing in mind: This is not going to work properly with paginated results. I mean, it’ll sort of work. The empty values will get sorted to the end of the list, but they’ll stay on the same “page” they were on before the query was run. In other words, they’ll be sorted to the bottom of page one, not of the last page. Anyway… consider this most useful in cases where you’re setting posts_per_page to -1 or some arbitrarily large number (e.g. 999).

The function

This simple (and highly compact) function accepts a field name (and a boolean for whether or not it’s a custom field [meta data]), then takes the array of posts in the main query ($wp_query), splits them into two separate arrays — one with the non-empty values for your selected field, one with the empty values — and then merges those arrays back together, with all of the non-empty values first. (Other than shifting empties to the back, it retains the same post order from the original query.)

function sort_empty_last($field, $is_meta=false) {
  global $wp_query;
  if (!$wp_query->is_main_query()) { return; }
  $not_empty = $empty = array();
  foreach ((array)$wp_query->posts as $post) {
    $field_value = !empty($is_meta) ? get_post_meta($post->ID, $field) : $post->{$field};
    if (empty(implode((array)$field_value))) { $empty[] = $post; }
    else { $not_empty[] = $post; }
  }
  $wp_query->posts = array_merge($not_empty,$empty);
}

Calling the function

As I said, this function is designed to work directly on the main query. You just need to call the function right before if (have_posts()) in any archive template where you want it to apply. Because of the way it works — especially the posts_per_page consideration — I thought calling it directly in the template was the most clear-cut way to work with it. Here’s an example of the first few lines of a really basic archive template that uses it, looking for a custom field (meta data) called deadline:

<?php

get_header();

sort_empty_last('deadline', true);

if (have_posts()) {

Super-easy filterable lists/tables with jQuery

I’m working on a page that will potentially have a very long table of information, and I wanted a way to filter the table to only show rows that contain a specific text string.

Fortunately, with jQuery that’s super easy. It took me about 3 minutes to build and test. Let’s take a look!

First, you want to create your table. Give it a class you’ll be able to use to tell jQuery this is what you’re filtering. Something like this:

<table class="filterable">
  <tbody>
    <tr>…</tr>
  </tbody>
</table>

Now let’s put in a little form field for entering the filter string. This is old news now, but HTML5 lets us use one-off form inputs for on-page actions without having to wrap them in a <form> tag, so let’s just stick this into our HTML above the table:

<div>
  Filter: <input type="text" id="list_filter" />
</div>

Now here’s where it gets fun. In jQuery, we’re going to watch for the keyup event on the input. If the input has a value (i.e. is not empty), we’ll do our filtering. If it is empty, we’ll just reveal all of the rows in the table again.

We probably want the filter to be case-insensitive, so we’ll make both the input string and our check of each row’s text all-lowercase with .toLowerCase().

Next we’ll step through each row of the table, check if our filter string is not present (.indexOf() == -1) in the .text() inside that row. If it’s not there, we’ll hide the row. Otherwise, we’ll show it. (This last bit is important because we want to start revealing previously hidden rows if the user deletes characters from the filter input.)

Yeah… that’s pretty much it. And since it’s all jQuery interacting with elements already present on the page, it’s lightning-fast.

<script>
  jQuery(function() {
    jQuery('input#list_filter').on('keyup', function() {
      if (jQuery(this).val() != '') {
        var filter_val = jQuery(this).val().toLowerCase();
        jQuery('table.filterable tbody tr').each(function() {
          if (jQuery(this).text().toLowerCase().indexOf(filter_val) == -1) {
            jQuery(this).hide();
          }
          else {
            jQuery(this).show();
          }
        });
      }
      else {
        jQuery('table.filterable tbody tr').show();
      }
    });
  });
</script>

A few other notes:

  1. The way this is built, you could conceivably have multiple filterable tables on one page, but only one filter input. The filter would automatically get applied to all filterable tables on the same page. There are various ways of changing this by modifying your selector.
  2. I am deliberately only checking for <tr> tags inside the <tbody> tag to allow for a header row inside a <thead> tag that would not be subject to the filter. If you have a header row inside your <tbody> tag, it’s going to get filtered too! Probably not desirable.
  3. You could play around with making the whole thing smoother with .slideUp() and .slideDown() instead of .hide() and .show() but that UX can get messy in a hurry.

Responsive horizontal scrolling tables for phones in CSS with no additional HTML

For some reason I always forget how to do this, and most tutorials out there suggest wrapping your table in a container <div> tag but as long as you’re using <tbody> (and, ideally, not using <thead> or <tfoot>) then it’s easy with a small bit of CSS and no HTML changes at all.

The trick is to display your <table> as a block, and your <tbody> as a table!

Here’s the code. Fit it into whichever breakpoint makes sense for your site. Mine shown here is the standard phone width breakpoint for WordPress (782 pixels):

@media screen and (max-width: 782px) {

  table {
    display: block;
    overflow-x: auto;
    width: 100%;
  }

  table tbody {
    display: table;
    width: auto;
  }

}

Of course, right after I posted this I found that I had already written about it two years ago with a slightly different solution. Never hurts to have a slightly different perspective.

All the Albums of 2019

It’s been an annual tradition here on Underdog of Perfection: Around the end of November, I post a list of all of the albums I’ve purchased that year, and then a couple weeks later, I rank my top 5.

Well… I skipped it altogether last year, because I hadn’t been paying much attention to new music. The trend has only accelerated this year. It’s partly because I’ve been delving deeply into jazz from the ’60s and ’70s, but it’s more because… man, pop music really sucks these days, and I just haven’t been too interested in the more obscure new stuff either.

And of course, who “buys albums” anymore?

So, here then is my complete list of all the new albums I purchased in 2019. Ones I think are especially good are in bold.

Christopher Willits — Sunset
The Claypool Lennon Delirium — South of Reality
Com Truise — Persuasion System
John Coltrane — Blue World*
JV’s Boogaloo Squad — Going to Market
Lizzo — Cuz I Love You
Mario Alberto Silva — Pan-American Sonata
Steve Hauschildt — Nonlin
The Bad Plus – Activate Infinity
The Bird and the Bee — Interpreting the Masters, Volume 2: A Tribute to Van Halen
TOOL — Fear Inoculum
32nd Street Jazz — self-title**

* Technically a new release!

** OK, I didn’t buy this… I made it, along with my bandmates. But hey, it’s a real release… we even got a ton of airplay on Jazz 88 in August and September! (And yes, it is one of my favorite albums of the year.)

And while I’m at it, here’s my complete (and considerably longer, but still short) list of all the new albums I purchased in 2018, favorites in bold.

Aphex Twin — Collapse EP
The Bad Plus — Never Stop II
Brad Mehldau Trip — Seymour Reads the Constitution!
Christian McBride — Christian McBride’s New Jawn
Courtney Barnett — Tell Me How You Really Feel
The Decemberists — I’ll Be Your Girl
Delvon Lamarr Organ Trio — Live at KEXP!
Elvis Costello & The Impostors — Look Now
Geotic — Traversa
Halloween, Alaska — Le Centre
Helios — Veriditas
Joshua Redman, Ron Miles, Scott Colley & Brian Blade — Still Dreaming
Justin Timberlake — Man of the Woods
Kamasi Washington — Heaven and Earth
King Crimson — Meltdown (Live in Mexico 2017)
Myriad3 — Vera
OMEGA Danzer — FUTURA the Album
Optiganally Yours — O.Y. in Hi-Fi
Steve Hauschildt — Dissolvi
X-Altera — self-title
Yellowjackets — Raising Our Voice
Yes — Fly From Here (Return Trip)

Using The SEO Framework with Advanced Custom Fields

I’m going to go out on a limb and guess that I am not the only WordPress developer who in recent days (in the wake of their obnoxious Black Friday dashboard ad) has switched allegiance from Yoast to another SEO plugin, and that many of those who find themselves in a similar boat (to mix metaphors) have switched to The SEO Framework.

I’ve only been using it for a couple of days, but I already love it. It does all of the things I actually used Yoast for, without any of the other stuff I did not use it for. I mean honestly, maybe readability scores and “cornerstone content” do provide an SEO boost, but I barely understand how to use these tools, so good luck explaining them to my clients in a meaningful way. I suppose they’re more of a tool for full-time SEO consultants who need to pad out their billable hours. (Sorry not sorry. My opinion on the business of SEO hasn’t changed all that much since 2011.)

It wasn’t until the Black Friday ad that I really admitted to myself how much I don’t like Yoast. It does a lot of important things, and does them very well. But it’s obnoxious as hell about it. Pushing features you don’t really want or need into every page of the WordPress admin, and plastering its own over-designed admin screens with tons of garish ads promoting its “premium” features.

Yuck.

The SEO Framework encapsulates all of the key features I liked about Yoast into a single configuration screen, which kindly adheres to the standard WordPress admin UI design language instead of infusing its own brand style into every button and metabox border. It’s refreshingly boring to look at. And it just has the stuff I actually use, like title and description, OpenGraph tags, sitemap XML, the basic elements of SEO that unequivocally matter and can be a pain to build and maintain on your own.

But enough about all of its great features. There’s one key thing it lacks: support for Advanced Custom Fields. My standard “modular design” theme relies almost entirely on ACF’s Flexible Content fields to work its page layout magic, and with all of the page content stored in custom fields instead of post_content, there’s nothing for The SEO Framework to latch onto to auto-generate meta descriptions.

Fortunately, the developer has built in some hooks to allow you to customize the meta description output.

Here’s a barebones starting point:

function my_seo_framework_description($description, $args) {
  if (empty($description)) {
    $description = ''; // Add your own logic here!
  }
  return $description;
}
add_filter('the_seo_framework_custom_field_description', 'my_seo_framework_description', 10, 2);
add_filter('the_seo_framework_generated_description', 'my_seo_framework_description', 10, 2);
add_filter('the_seo_framework_fetched_description_excerpt', 'my_seo_framework_description', 10, 2);

As the developer notes, it’s very important for SEO not to just output the same static description text on every page. You need to have a function of your own that will read your ACF field content and generate something meaningful here.

Fortunately in my case, I had already done that, for generating custom excerpts from ACF content, so I was able to just stick a call to that function into the // Add your own logic here! line. You’ll need to customize your function to suit your specific content structure, but here’s the post that I used as a starting point for my function.

Have fun!