Post types and taxonomies: Linking terms to a specific post

Almost every week I get asked a question about custom WordPress taxonomies and post types that has been bothering me for quite a while. I’ve thought over several solutions to the question and haven’t necessarily found the perfect one, but I have found one that may work in some cases.

The question usually goes back to my original post on creating a movie database with taxonomies. In the tutorial, I explain how to make several taxonomies that represent people (actor, director, producer, and writer). Taxonomies give us a way to clearly label and organize what roles a person takes on within the production of a movie. The relationship between the given taxonomy and the movie post type usually stops there.

The question: How does one make an “actor” both a taxonomy term and a post of a specific post type?

That’s essentially what most of the questions boil down to. We want the clear organizational structure of taxonomies, but we also want each term in a taxonomy to behave as if it were a post/page with the ability to edit the content, upload media, have custom fields, and allow comments. So, we also need a “person” post type to handle this.

Creating taxonomies and post types is the easy part. Making the connection we want is the complex part of the question.

Taxonomy to post type

What we’re doing

In this tutorial, we’re trying to make connections between two post types and one taxonomy.

  • Movie (post type).
  • Person (post type).
  • Actor (taxonomy).

In my movie database example, we’d be making the connection with several taxonomies. But, I want to keep this tutorial simple.

The goal is to create a “movie” and list its actors on the movie page. But, instead of going to an actor archive when clicking on one of the actor’s names, we want to visit a “person” page with custom content. This would allow you to write custom content, upload images, add videos, open up the commenting section, and do all the things you’d normally be able to do with posts.

On the movie database, when we see Tom Hanks listed as an actor, we’d want to visit a custom person page we set up for him.

Post type and taxonomy setup

The rest of this tutorial assumes a working knowledge of creating post types and taxonomies. The examples below will be limited and likely won't be helpful unless you have an understanding of this process. Please read my previous tutorials on post types and taxonomies for a more in-depth review.

Open your theme’s functions.php file and add this PHP code to create the movie and person post types.

add_action( 'init', 'my_register_post_types' );

function my_register_post_types() {

	register_post_type(
		'movie',
		array(
			'public' => true,
			'labels' => array( 'name' => 'Movies', 'singular_name' => 'Movie' )
		)
	);

	register_post_type(
		'person',
		array(
			'public' => true,
			'labels' => array( 'name' => 'People', 'singular_name' => 'Person' )
		)
	);
}

Now, let’s add some code for our actor taxonomy.

add_action( 'init', 'my_register_taxonomies' );

function my_register_taxonomies() {

	register_taxonomy(
		'actor',
		array( 'movie' ),
		array(
			'public' => true,
			'labels' => array( 'name' => 'Actors', 'singular_name' => 'Actor' )
		)
	);
}

That should give us a simple setup to work with and base to build our new functionality on.

The biggest thing to note here is that we have to be careful with our actor and person slugs. When we create a new actor (term), we need to have the exact same slug as the person (post). For example, when creating the Tom Hanks “actor,” it needs to have the slug of tom-hanks. And, when we create the Tom Hanks “person,” it needs to have the slug of tom-hanks. Otherwise, the proposed solutions below won’t work.

Option #1: Changing term links to post permalinks

One option is to change the links to actor archives to a specific post. We’ll be working with term and post slugs in this step. I normally don’t recommend using slugs because IDs are usually best to work with. They don’t change and slugs can. Unfortunately, connecting the terms and posts via ID isn’t something we can do right now.

The code below will do this step automatically for you. What it does is filter term_link (link to actor archive page). It grabs the term slug (actor name) and searches for a post slug that matches the term slug and has a post type of “person.” If it finds the post, it switches the link for you. Otherwise, it does nothing.

If you’re listing a ton of terms on one page, this solution isn’t ideal unless you mix it in with a caching solution.

add_filter( 'term_link', 'my_term_to_type', 10, 3 );

function my_term_to_type( $link, $term, $taxonomy ) {

	if ( 'actor' == $taxonomy ) {
		$post_id = my_get_post_id_by_slug( $term->slug, 'person' );

		if ( !empty( $post_id ) )
			return get_permalink( $post_id );
	}

	return $link;
}

function my_get_post_id_by_slug( $slug, $post_type ) {
	global $wpdb;

	$slug = rawurlencode( urldecode( $slug ) );
	$slug = sanitize_title( basename( $slug ) );

	$post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_name = %s AND post_type = %s", $slug, $post_type ) );

	if ( is_array( $post_id ) )
		return $post_id[0];
	elseif ( !empty( $post_id ) );
		return $post_id;

	return false;
}

Option #2: Redirect term archives to posts

In the above example, term archives are still publicly available. We’re not doing a redirect, so it’s possible for someone to still stumble upon the archive or for you to even make it available if you wanted to. But, if you’re looking for a way to redirect anyone that visits a term archive to the appropriate post, use the code below. This method is also a lot more efficient.

Use this code in your functions.php file.

add_action( 'template_redirect', 'my_redirect_term_to_post' );

function my_redirect_term_to_post() {
	global $wp_query;

	if ( is_tax() ) {
		$term = $wp_query->get_queried_object();

		if ( 'actor' == $term->taxonomy ) {
			$post_id = my_get_post_id_by_slug( $term->slug, 'person' );

			if ( !empty( $post_id ) )
				wp_redirect( get_permalink( $post_id ), 301 );
		}
	}
}

function my_get_post_id_by_slug( $slug, $post_type ) {
	global $wpdb;

	$slug = rawurlencode( urldecode( $slug ) );
	$slug = sanitize_title( basename( $slug ) );

	$post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_name = %s AND post_type = %s", $slug, $post_type ) );

	if ( is_array( $post_id ) )
		return $post_id[0];
	elseif ( !empty( $post_id ) );
		return $post_id;

	return false;
}

Asking for a term meta table

There has been some discussion in the WordPress community about adding a term meta table to the WordPress core. I can list a ton of reasons this would be a great idea, but let me just focus on how it would make the above solutions even better.

Right now (in the examples), we’re calling up the database to search for a post based on a slug. This isn’t something I really like to do on the front end, especially with the first example. For example, listing 50 actors on a page using the first option would cause us to search the database 50 times to see if a post slug matches the taxonomy term.

A term meta table would easily solve this problem. We could save the post ID as metadata for individual terms. We could even do the post lookup in the backend instead of doing it on-the-fly on the front end of the site. Or, even provide a nice interface for easily selecting the post/term we want to connect. Having term meta would allow us to do a quick check if there’s a post we want to redirect to.

Other solutions and thoughts

The above solutions are just something I was tinkering around with because of the enormous number of questions I get about this. I haven’t fully explored every possible option, so I can’t say that either is the best route to take. Think of it more as “Justin thinking out loud” than a definite solution.

I would love to hear your take on other solutions or even solutions that we could try to get rolled into core to better handle metadata and relationships between post types and taxonomies.