Make WordPress Core

Opened 3 years ago

Closed 22 months ago

#18752 closed enhancement (fixed)

Allow category preference in permalinks

Reported by: aaroncampbell Owned by: nacin
Milestone: 3.5 Priority: normal
Severity: normal Version: 3.2
Component: General Keywords: has-patch commit
Focuses: Cc:


I know that the general consensus is that a post should be assigned to a single category and have many tags. However, we ALLOW a user to set as many categories as they want and I'm constantly facing customers that use multiple categories and are frustrated that they can't set a "preferred" category for their permalinks. The category in the permalink is simply determined by the lowest ID with this code:

usort($cats, '_usort_terms_by_ID');

If we add a filter just after that, or better yet change it to a filter and add the usort USING the filter, then a plugin could use business logic to decide what category belongs in the permalink instead of being forced to use one that may make no sense.

Attachments (5)

18752.diff (2.4 KB) - added by aaroncampbell 3 years ago.
18752.2.diff (2.4 KB) - added by aaroncampbell 3 years ago.
18752.3.diff (626 bytes) - added by nacin 22 months ago.
18752.4.diff (725 bytes) - added by aaroncampbell 22 months ago.
18752.5.diff (749 bytes) - added by aaroncampbell 22 months ago.

Download all attachments as: .zip

Change History (28)

aaroncampbell3 years ago

comment:1 aaroncampbell3 years ago

  • Keywords has-patch 2nd-opinion added

comment:2 kawauso3 years ago

  • Cc kawauso added

comment:3 scribu3 years ago

  • Keywords 2nd-opinion removed


The // order by ID comment is redundant, though.

comment:4 aaroncampbell3 years ago

The comment was already there, I just copy/pasted the line. It's probably not a big deal to leave it there.

I'm testing this patch on a site and it seems to be working exactly as expected. I'm using this code to NOT use 'publications' as the category in the permalink if there are other options:

function da_remove_publications_cat( $cat ) {
	return ( 'publications' != $cat->slug );
function da_permalink_categories( $cats ) {
	if ( count( $cats ) > 1 )
		$cats = array_filter( $cats, 'da_remove_publications_cat' );

	return $cats;
add_filter( 'post_link_categories', 'da_permalink_categories', 9 );

comment:5 scribu3 years ago

The comment was already there, I just copy/pasted the line. It's probably not a big deal to leave it there.

It's also probably not a big deal to remove it either. :P

comment:6 aaroncampbell3 years ago

Sorry, yes I agree that it could be removed...I just didn't think it was worth the time to make another patch just to remove it. However, since it seems to be becoming an issue I just re-made it.

aaroncampbell3 years ago

comment:7 follow-up: scribu3 years ago

Hm... won't this trip up the canonical redirect stuff?

comment:8 in reply to: ↑ 7 nacin3 years ago

Replying to scribu:

Hm... won't this trip up the canonical redirect stuff?

Nope, because get_permalink() will return the proper URL and canonical will think it's on the correct page.

comment:9 aaroncampbell3 years ago

It has the potential to change exiting permalinks but only if someone specifically writes code (like in my comment above) to do that. If they *do* then as nacin said, the new permalink will match the canonical URL. However, URLs changed in this way will NOT get redirected with redirect_canonical() because when it deals with %category% it checks if the cat in the current URL is valid for the post and if it is it doesn't try to redirect. Since the previous category would be valid (even if not preferred) it would not redirect. However, this is consistent with what happens currently (if a post is in 3 categories it has three URLs that will work and not redirect, but only one that is canonical), and you could force a redirect with your own function hooking into template_redirect for these situations.

Having had the chance to test it a lot more today...it's working really well for my use cases.

comment:10 johnbillion3 years ago

Could this approach be used to aid the issues in #10786?

comment:11 aaroncampbell3 years ago

The site that I needed this form is getting really close to going live. I'd really like to see this get put in (or a lead say that it will) before I put this code into the wild. Anyone have any concerns?

comment:12 scribu3 years ago

  • Type changed from defect (bug) to enhancement

Looks solid. If it were by me, I'd get it into 3.3.

comment:13 ryan3 years ago

  • Milestone changed from Awaiting Review to Future Release

comment:14 soulseekah2 years ago

  • Cc gennady@… added

nacin22 months ago

comment:15 nacin22 months ago

After coming back to this, I think this can be implemented in a much simpler and more obvious way.

What we're doing is exposing an array of categories to be sorted however someone would like. But no matter what happens, we only care about the first value. (And actually, we care about the key of 0, so if someone wanted to do some kind of sorting or pushing they would need to make sure keys did get reset, which wouldn't always be the case depending on the method.)

So what about 18752.3.diff? A filter specifically on the category object. It expects a category object in return. (We could also do IDs.) Whichever object we get back, gets used.

aaroncampbell22 months ago

comment:16 follow-up: aaroncampbell22 months ago

Replying to nacin:

That works for me, but it looks like you missed another reference to $cats[0] on the very next line. If I return a category object to your filter, it'll be prepended with the parents of $cats[0] if it has any. 18752.4.diff should fix that.

comment:17 in reply to: ↑ 16 nacin22 months ago

  • Keywords commit added
  • Milestone changed from Future Release to 3.5

Replying to aaroncampbell:

Replying to nacin:

That works for me, but it looks like you missed another reference to $cats[0] on the very next line.

You got the gist, anyway. :-)

Let's test it out and commit. Any thoughts on a category object versus an ID? If we run $cat through get_term(), it should force an object if given an ID, the same way we'd do it with get_post().

aaroncampbell22 months ago

comment:18 aaroncampbell22 months ago

18752.5.diff passes the return from the filter through get_term. I'd been testing .4 and it worked great for me. I re-ran all the tests with .5 passing back IDs instead and it worked well there too. I was testing with this:

function essence_post_link_category( $cat, $cats, $post ) {
	//return $cats[1];
	//return $cats[2];
	//return $cats[3];
	//return $cats[4];
	//return $cats[1]->term_id;
	//return $cats[2]->term_id;
	//return $cats[3]->term_id;
	return $cats[4]->term_id;
add_filter( 'post_link_category', 'essence_post_link_category', null, 3 );

I was testing with a post with 5 categories and the fifth had parents that were set properly as well. I think it's ready to go in, but check it out and let me know what you think.

comment:19 nacin22 months ago

Worth highlighting the canonical code for this:

		} elseif ( is_single() && strpos($wp_rewrite->permalink_structure, '%category%') !== false && $cat = get_query_var( 'category_name' ) ) {
			$category = get_category_by_path( $cat );
			$post_terms = wp_get_object_terms($wp_query->get_queried_object_id(), 'category', array('fields' => 'tt_ids'));
			if ( (!$category || is_wp_error($category)) || ( !is_wp_error($post_terms) && !empty($post_terms) && !in_array($category->term_taxonomy_id, $post_terms) ) )
				$redirect_url = get_permalink($wp_query->get_queried_object_id());

A good test would be what happens if you choose to use a category that isn't actually assigned to the post.

comment:20 aaroncampbell22 months ago

Sorry, I see that it's not in my paste up there, but I did test that. It did what *I* would want it to do, which is that the permalink used the category even though the post wasn't in it and it is NOT canonically redirected. Since you're manually overriding this, I think that would be the expected response.

comment:21 aaroncampbell22 months ago

Thea reason it isn't canonically redirected is that when it finds that the category is NOT one of the post terms, it uses get_permalink() to generate the URL to redirect to. Since our filter applies to that URL as well, toward the end of redirect_canonical() it checks $redirect_url == $requested_url and returns false instead of redirecting.

comment:22 nacin22 months ago

Since our filter applies to that URL as well, toward the end of redirect_canonical() it checks $redirect_url == $requested_url and returns false instead of redirecting.

That's what I was looking for. Excellent!

comment:23 nacin22 months ago

  • Owner set to nacin
  • Resolution set to fixed
  • Status changed from new to closed

In [21169]:

Add a post_link_category filter to the permalink generation process.

This allows a plugin to easily change which category gets represented
in the URL. Previously, it went off the category with the smallest ID.

props aaroncampbell
fixes #18752

Note: See TracTickets for help on using tickets.