The following is a guest post by Christian Nolen. Christian details a situation where data is being intelligently and reasonably organized, yet is a little tricky to query for just right. He details a solution that doesn’t go too far into the weeds to get it right.
If you’ve ever done any WordPress development, you’ve likely needed to create custom post types and register taxonomies. Working with taxonomies is straightforward when the relationship is one-to-one, i.e. “Get me all taxonomy terms, use get_terms()
, or get all terms associated with the current post, use get_the_terms()
.”
But, what do you do when you need the returned results to be relative to another taxonomy? Neither of the typical functions help. Before I delve into the solution, let’s take a closer look at the problem. The use case is a portfolio website that offers a variety of services in multiple markets. Our setup is as follows:
- Custom Post Type: Portfolio
- Taxonomy: Services
- Terms: Development, Illustration, Photography, Print Design, Web Design
- Taxonomy: Markets
- Terms: Civic, Corporate, Education, Health, Not for Profit
This site requires the use of a taxonomy template for both Services and Markets. When a visitor lands on one of these taxonomy pages, a tab layout showing portfolio items for associated markets (if viewing a service) or associated services (if viewing a market) is displayed.

In the screenshot above, I’m displaying a Photography Service page. I’ve usedget_terms('market')
to retrieve the valid markets (assigned to a $markets
variable). I iterate through each market to create my tab and then use WP_Query
to retrieve all corresponding portfolio items. The tax_query
parameter is used to narrow results to the current market and service (defined using get_query_var('term')
in this case).
The first thing we notice is an embarrassing empty “Civic” tab that brings focus to the fact that services have not been provided in this market. To boot, it’s the first tab a visitor sees when landing on this page.
The Solution
I spent a significant amount of time researching a resolution. The only solution I found involved querying the database using wpdb. Yes, this was a solution, but it didn’t feel right. I knew there must be a built in way to handle this use case.
After delving deeper into the WordPress Codex, I discovered wp_get_object_terms()
. The description of the function is as follows:
Retrieves the terms associated with the given object(s) in the supplied taxonomies.
The example provided is very similar to one you’d find for get_the_terms()
. The key is the object parameter can be an array.
The object we want to pass to wp_get_object_terms()
is an array of post IDs. The most efficient way I could think of building this array was using get_posts()
with the fields parameter set to “ids.” You could use WP_Query
, but I opted for get_posts() because:
get_posts()
returns an array vs. an object- I’ll never confuse my initial
get_posts()
call fromWP_Query
used within the tabbed content.
$service_slug = get_query_var('term');
$service_post_IDs = get_posts(array(
'post_type' => 'portfolio',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'service',
'field' => 'slug',
'terms' => $service_slug
)
),
'fields' => 'ids'
));
The above is how we get the right subset of post IDs. As mentioned earlier, I retrieve the current service slug by using get_query_var('term')
and assign it to the $service_slug
variable. This is used in tax_query
to ensure we only get post IDs that are assigned to the current taxonomy term. By setting the fields
parameter to ids
the result is an indexed array of post IDs.
Now that I have the array of post IDs, I can replace get_terms()
with the following:
$markets = wp_get_object_terms($service_post_IDs, 'market');
When I refresh the page, the “Civic” tab is gone and we have a tab populated with the correct content.

I could stop here. But now the arguments used within WP_Query
are bothering me.
$args = array(
'post_type' => 'portfolio',
'posts_per_page' => -1,
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'market',
'field' => 'slug',
'terms' => $m->slug,
),
array(
'taxonomy' => 'service',
'field' => 'slug',
'terms' => $service_slug,
),
)
);
Yes, the above works just fine. But I feel like I’m duplicating my efforts in regards to my tax_query
. What we can do is refactor the tax_query to include only the current market and leverage the post__in
parameter with the array of service post IDs.
$args = array(
'post_type' => 'portfolio',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'market',
'field' => 'slug',
'terms' => $m->slug,
)
),
'post__in' => $service_post_IDs
);
The front-end result is the same, but now the query arguments are slimmer and easier to read. Most importantly, you now have a template that is showing valid information to your visitors.
So happy I left that WP shit behind.
Jekyll for the win!
Couldn’t you have also used the
hide_empty
argument inget_terms
?No because “hide_empty” only hides terms that have 0 posts assigned to it.
He needed to hide the term if the current post is not assigned to it – but there may be other posts who are assigned to it, and hide_empty would not have worked in that case.
Anyway, it’s better to be precise than try and find workarounds that work in only the current use-case.
Great job!!
I will try your solution hopefully it will work for me as well.
Thank you
Totally awesome! I’m currently using a monster query via wpdb to achieve something similar.
Does this solution work across hierarchical and non-hierarchical taxonomies like categories and tags for example?
Absolutely. It works with any taxonomy.
This is great, word a dream ! I spend many hours looking for a solution ! thanks
This has helped me more than you think. Thank you very much!