To show WordPress taxonomy terms as links with a limit, say the first four categories or your five biggest product types, you need get_terms(), a link for each term and some care for the cases where the taxonomy has no terms.
This shortcode does that for any taxonomy. The older version of this post printed the PHP warning “Attempt to read property name on array” when get_terms() returned an error instead of terms, for example with the placeholder taxonomy left in. Now it prints nothing in that case, and every name and link is escaped.
[dpc_term_links]shows four categories.[dpc_term_links taxonomy="genre" number="5" orderby="count"]shows the five genres with the most posts.[dpc_term_links taxonomy="post_tag" number="0" separator=" · "]shows every tag.
The snippet
<?php
/**
* [dpc_term_links taxonomy="category" number="4"]
* Terms of a taxonomy as links, limited to a number.
*/
add_shortcode( 'dpc_term_links', 'dpc_term_links_shortcode' );
function dpc_term_links_shortcode( $atts ): string {
$atts = shortcode_atts(
array(
'taxonomy' => 'category',
'number' => 4, // 0 = all.
'hide_empty' => 'yes',
'orderby' => 'name', // name, count, id or term_order.
'separator' => ', ',
),
$atts,
'dpc_term_links'
);
$taxonomy = sanitize_key( $atts['taxonomy'] );
if ( ! taxonomy_exists( $taxonomy ) ) {
return '';
}
$orderby = in_array( $atts['orderby'], array( 'name', 'count', 'id', 'term_order' ), true ) ? $atts['orderby'] : 'name';
$terms = get_terms(
array(
'taxonomy' => $taxonomy,
'number' => absint( $atts['number'] ),
'hide_empty' => ! in_array( strtolower( (string) $atts['hide_empty'] ), array( 'no', 'false', '0', 'off' ), true ),
'orderby' => $orderby,
'order' => 'count' === $orderby ? 'DESC' : 'ASC',
)
);
// WP_Error (e.g. a broken query) used to end up in foreach: "Attempt to read property name on array".
if ( is_wp_error( $terms ) || ! $terms ) {
return '';
}
$current = ( is_category() || is_tag() || is_tax() ) ? get_queried_object_id() : 0;
$links = array();
foreach ( $terms as $term ) {
$url = get_term_link( $term );
if ( is_wp_error( $url ) ) {
continue;
}
$is_current = ( (int) $term->term_id === $current );
$links[] = sprintf(
'<a class="dpc-term-links__link%1$s" href="%2$s"%3$s>%4$s</a>',
$is_current ? ' is-current' : '',
esc_url( $url ),
$is_current ? ' aria-current="page"' : '',
esc_html( $term->name )
);
}
return $links ? '<span class="dpc-term-links">' . implode( esc_html( $atts['separator'] ), $links ) . '</span>' : '';
}
Add it with Scripts Organizer
In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor and publish. Leave Action/Hook empty.
Then use [dpc_term_links] anywhere shortcodes run: post content, a Shortcode block in a block theme template, a widget or your page builder’s shortcode element. A normal shortcode fits better here than a Shortcode location block, because the taxonomy and the limit are attributes: one code block covers every place and taxonomy, instead of one block per combination.
The same code also works unchanged in a small plugin or an mu-plugin.
How it works
- The taxonomy goes through
sanitize_key()and must exist.numbergoes throughabsint()(0 means all),hide_emptyacceptsno,false,0oroffto show empty terms, andorderbygoes through an allowlist. Ordering bycountlists the biggest terms first. get_terms()returns aWP_Erroron failure. The old code looped over that object, which is where the warning came from; the shortcode now checks for it and for an empty result.get_term_link()can also return aWP_Error, for example for a taxonomy that is not public, so such terms are skipped.- On a term archive the matching link gets an
is-currentclass andaria-current="page". Names go throughesc_html(), URLs throughesc_url()and the separator throughesc_html().
Customise it
- A full list with the active term highlighted, for a filter menu: see List WordPress categories with an active class.
- Only the terms of the current post: use
get_the_terms( get_the_ID(), $taxonomy )instead ofget_terms()andarray_slice()for the limit. - Separators in CSS instead: pass
separator=""and style.dpc-term-links__link+.dpc-term-links__link::before{content:" / "}.
Gotchas
- Empty terms are hidden by default. The old snippet showed them; pass
hide_empty="no"for that. - With
numberandhide_emptytogether, WordPress limits after hiding empty terms, so you get up to that many terms that have posts.
In the Scripts Organizer library Import it with one click from Scripts Organizer.