Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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

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