Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save joshuadavidnelson/282614233c25071669f2502d77a7a8f9 to your computer and use it in GitHub Desktop.
Save joshuadavidnelson/282614233c25071669f2502d77a7a8f9 to your computer and use it in GitHub Desktop.
Using a Parent > Child category structure with duplicate child url slugs; for urls like `category/parent-1/child` and `category/parent-2/child`.
<?php
/**
* Plugin Name: Duplicated Child Term Url Slugs
* Description: Duplicate child term urls slugs in hierarchical taxonomies.
* Version: 0.1.0
* Author: joshuadnelson
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
*
* This is setup like a plugin, but can be included in a theme or as a class in a plugin.
*
* Using a Parent > Child category structure with duplicate child url slugs;
* for urls like `category/parent-1/child` and `category/parent-2/child`.
*
* Child term slugs must be unique per WordPress core requirements.
* In order to have duplicated child url slugs, the term slug must follow
* a pattern we can transform to tell WordPress how to route the url.
*
* Then apply a custom rewrite rule to match the pattern and filter the
* term link to remove the parent term slug from the child term url.
*
* Here the child slugs are structured like "{parent-slug}_{child-url-slug}"
* and the term link is shown as `category/{parent-slug}/{child-url-slug}`.
*
* Examples:
* - url `example.url/category/books/hobbit/`
* for the `books_hobbit` child term and `books` parent term
* - url `example.url/category/movies/hobbit/`
* for the `movies_hobbit` child term and `movies` parent term
*
* Notes:
* - Permalinks need to be "pretty" and need to be flushed after apply these changes.
* - Parent terms must be unique, child terms can be duplicated.
* - Terms cannot have a "_" in a slug other than for this purpose.
* - Child terms must contain the parent term slug, be careful if you change those on
* the parent term you will need to update the child term slugs as well.
* - Only works for hierarchical taxonomies, specifically the built-in 'category' taxonomy.
* You can modify the taxonomy name in the code to work with other taxonomies
* by changing the 'category' taxonomy name and the 'category_name' query var.
*/
/**
* Main class for the Duplicated Child Url Slugs plugin.
*
* @since 0.1.0
*/
class DuplicatedChildUrlSlugs {
/**
* The taxonomy name.
*
* @since 0.1.0
* @var string
*/
protected $taxonomy = 'category';
/**
* The taxonomy query var.
*
* @since 0.1.0
* @var string
*/
protected $query_var = 'category_name';
/**
* The separator used to separate parent and child term slugs.
*
* @since 0.1.0
* @var string
*/
protected $sep = '_';
/**
* Register the plugin
*
* @since 0.1.0
* @return void
*/
public function register() {
// Filter the term link to handle hierarchical taxonomy permalinks.
add_filter( 'term_link', array( $this, 'term_link' ), 10, 3 );
// Parse the query to handle hierarchical taxonomy permalinks.
add_action( 'parse_query', array( $this, 'parse_query' ) );
// Add custom rewrite rules for parent/child category urls.
add_action( 'init', array( $this, 'rewrite_rules' ), 100 );
}
/**
* Add custom rewrite rules for parent/child category urls.
*
* Adds a rewrite rule that will match terms
* that have their slug prefixed with the parent term
* slug, separated by an underscore, but their url
* is just the child term slug.
*
* @since 0.1.0
* @return void
*/
function rewrite_rules() {
$tax_obj = get_taxonomy( $this->taxonomy );
if ( $tax_obj ) {
$tax_slug = $tax_obj->rewrite['slug'];
add_rewrite_rule(
$tax_obj->rewrite['slug'] . '/([^/]+)/([^/]+)/?',
'index.php?' . $tax_obj->query_var . '=$matches[1]_$matches[2]',
'top'
);
}
}
/**
* Filter the term link to handle hierarchical taxonomy permalinks.
*
* This will filter the term link to remove the parent term slug
* from the child term url, so that the url is just the child term slug,
* where the child term name is "parent_child" and the parent term name is "parent".
*
* @since 0.1.0
* @param string $link The term link
* @param WP_Term $term The term object
* @param string $taxonomy The taxonomy name
* @return string
*/
function term_link( $link, $term, $taxonomy ) {
if ( $taxonomy !== $this->taxonomy ) {
return $link;
}
// Get the parent term
$parent_term = get_term( $term->parent, $taxonomy );
if ( $parent_term && ! is_wp_error( $parent_term ) ) {
// Remove the "parent_" prefix from the child term slug
// to generate a url that looks like /parent/child/
$link = str_replace( '/' . $term->slug, '/' . str_replace ( $parent_term->slug . '_', '', $term->slug ), $link );
}
return $link;
}
/**
* Parse the query to handle hierarchical taxonomy permalinks.
*
* This filter the query to check for terms that are child terms
* yet do not have the parent term in the slug, thus the parent/child
* structure should literally look for a "child" slug, not "parent_child"
*
* @since 0.1.0
* @param WP_Query $query The WP_Query instance (passed by reference)
* @return void
*/
function parse_query( $query ) {
// Check if we have the taxonomy query var set.
if ( isset( $query->query_vars[ $this->query_var ] ) ) {
// If we have a term slug with a separator, check if it's a child term
$slug = $query->query_vars[ $this->query_var ];
if ( strpos( $slug, $this->sep ) !== false ) {
// Separate the parent and child term slugs.
list( $parent, $child ) = explode( $this->sep, $slug, 2 );
// If the parent-child term does not exist, but the child term
// slug as-is is valid, then set the query var to the child term.
if ( ! term_exists( $slug, $this->taxonomy ) && term_exists( $child, $this->taxonomy ) ) {
$query->query_vars[ $this->query_var ] = $child;
}
}
}
}
}
$dup_url_slugs = new DuplicatedChildUrlSlugs();
$dup_url_slugs->register();
@joshuadavidnelson
Copy link
Author

joshuadavidnelson commented Mar 29, 2024

For a custom taxonomy, set the $taxonomy var as your taxonomy slug and $query_var with your taxonomy's query variable (see register_taxonomy)

@sergenadiyaman
Copy link

Hello, first of all, thank you for your plugin. I'm trying to do it for woocommerce product categories but I couldn't do it. Can you help me? Example of the URL structure I want to use

curtain/rooms/living-room/
wall-murals/rooms/living-room/

Have a nice day

@joshuadavidnelson
Copy link
Author

joshuadavidnelson commented Apr 18, 2024

Hey @sergenadiyaman. You're example needs to account for two modifications: a custom taxonomy and another level of duplication in the url slug - nesting 3-terms deep instead of 2-terms deep.

Custom taxonomy: product_cat

This example is for the built-in category taxonomy, you'll need to make some changes to use this on custom taxonomies.

I believe WooCommerce "product categories" are registered as product_cat, so you'll first want set $taxonomy = 'product_cat' and also $query_var = 'product_cat' (I'm not 100% sure on the WooCommerce naming for this, but the taxonomy slug is usually the default query_var).

Additional level to the url pattern

This is more complicated. You'll also need to update this to support 3-levels of nested permalink structure, this gist adds support for two levels (/parent/child/) but your example has three (/grandparent/parent/child/), which means the rewrite rule needs to look at three different parts of the link and you'll need a consistent term slug pattern to match to the correct grandparent, parent, and child terms.

For instance you may need to name each of those terms as:

  • curtain + curtain_rooms + curtain_rooms_living-room to support a url like /curtain/rooms/living-room/
  • wall-murals + wall-murals_rooms + wall-murals_rooms_living-room to support a url like wall-murals/rooms/living-room/

...and then parse that permalink to match the terms accordingly, some like this (warning I have not tested this):

function rewrite_rules() {

	$tax_obj = get_taxonomy( $this->taxonomy );

	if ( $tax_obj ) {
		$tax_slug = $tax_obj->rewrite['slug'];
		add_rewrite_rule(
			$tax_obj->rewrite['slug'] . '/([^/]+)/([^/]+)/([^/]+)/?',
			'index.php?' . $tax_obj->query_var . '=$matches[1]_$matches[2]_$matches[3]',
			'top'
		);
	}
}

You'll also need to update the logic in the term_link filter to look for a grandparent term & update the url. Lastly, the parse_query would need to be updated as well, to accommodate terms not matching these patterns. Best of luck!

@sergenadiyaman
Copy link

I'm sorry, I'm not a software developer. I'm just trying to make my website more efficient and I discovered your plugin. It works for post types, but I couldn't do it for Woocommerce product categories. I have no idea which taxamony to change or how to update term_link. I would appreciate it if you could write more clearly. Or it would be great if you could make a new plugin like the one you made. Thank you for your help

@joshuadavidnelson
Copy link
Author

To get this gist working with WooCommerce you should only need to change two lines:

  • Line 55 from protected $taxonomy = 'category'; to protected $taxonomy = 'product_cat';
  • Line 63 from protected $query_var = 'category_name'; to protected $query_var = 'product_cat';.

However, that will still only work for a 2-level permalink structure like rooms\living-rooms. Going to a 3-level permalink structure like curtain/rooms/living-room/ is outside of the scope of this plugin as it's currently written.

@sergenadiyaman
Copy link

sergenadiyaman commented Apr 19, 2024

thanks for your help

@johnflufin
Copy link

johnflufin commented May 29, 2024

Thank you for this. This works for categories but doesn't seem to do anything for underlying post permalinks.

  • example.url/top1/sub/
  • example.url/top2/sub/
  • example.url/top1/top1_sub/post

Am I missing something there? Can it be modified to carry over to underlying posts?

@joshuadavidnelson
Copy link
Author

Hey @johnflufin! This is really targeted at custom category permalink bases, what you're talking about is the category + post permalink base.

I think in that case you'd need to add another rewrite rule or two to cover conditions where you're using /cat/subcat/post structure, to tell WP what you expect the queried object for that url to be (the post id). You would update the rewrite_rules method in this example to include a rewrite condition and add some logic in the parse_query to account for it.

That could get really complex if you support categories on a hierarchal post type like pages, where you can have nested pages page/subpage in cat/subcat/page/subpage etc.

@chidinweke
Copy link

I attempted to use this plugin, but it didn't work. Is it a standalone plugin, or does it require another plugin to function properly?

@joshuadavidnelson
Copy link
Author

@chidinweke This is intended to work as a standalone plugin, but is very specific in it's use case. I've also saved it as a gist because it's not a fully supported project, just a proof of concept example.

It only works for core post categories where parent names are unique and child terms are the same, but within WordPress you have to structure the child slugs like "{parent-slug}_{child-url-slug}" and the term link is shown as category/{parent-slug}/{child-url-slug}.

After you install it, you'll likely need to refresh your permalinks as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment