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

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