Skip to content

Instantly share code, notes, and snippets.

@ThatStevensGuy
Last active Sep 29, 2021
Embed
What would you like to do?
Prepend a Taxonomy to the WordPress Permalink Structure
<?php
/**
* @package WordPress
* @subpackage Prepend a Taxonomy to the WordPress Permalink Structure
* @author That Stevens Guy
* @phpcs:disable PSR1.Files.SideEffects
*
* wp-config.php:
* // Add a main suburb for the homepage to the Suburbs taxonomy,
* // define the slug chosen here.
* define('MAIN_SUBURB_SLUG', 'suburb');
* define('DEBUG_REWRITES', false);
*/
/**
* Initialise custom suburb permalink.
* Note: Save permalinks settings page to flush rewrites.
*
* @return void
*/
add_action('init', 'tsg_prepare_permalink');
function tsg_prepare_permalink(): void
{
add_action('parse_request', 'tsg_debug_rewrites');
tsg_add_rewrites();
add_filter('rewrite_rules_array', 'tsg_customise_rewrite_rules_array');
add_filter('request', 'tsg_customise_request_query_vars');
add_action('template_redirect', 'tsg_verify_query_vars');
add_filter('query_vars', 'tsg_add_query_var');
add_filter('available_permalink_structure_tags', 'tsg_add_permalink_structure_tag');
add_filter('page_link', 'tsg_customise_page_link');
add_filter('post_link', 'tsg_customise_link');
add_filter('post_type_link', 'tsg_customise_link');
add_filter('post_type_archive_link', 'tsg_customise_link');
add_filter('term_link', 'tsg_customise_link');
add_filter('author_link', 'tsg_customise_link');
add_filter('day_link', 'tsg_customise_link');
add_filter('month_link', 'tsg_customise_link');
add_filter('year_link', 'tsg_customise_link');
add_filter('feed_link', 'tsg_customise_link');
add_filter('attachment_link', '__return_empty_string');
}
/**
* Add rewrite rules for the query variable %suburb_slug%.
*
* @return void
*/
function tsg_add_rewrites(): void
{
global $wp_rewrite;
$wp_rewrite->set_permalink_structure('/%suburb_slug%/%year%/%monthnum%/%day%/%postname%/');
$wp_rewrite->set_category_base('%suburb_slug%/category');
$wp_rewrite->set_tag_base('%suburb_slug%/tag');
$wp_rewrite->author_base = '%suburb_slug%/' . $wp_rewrite->author_base;
$wp_rewrite->comments_base = '%suburb_slug%/' . $wp_rewrite->comments_base;
$wp_rewrite->date_structure = '%suburb_slug%/%year%/%monthnum%/%day%';
$wp_rewrite->page_structure = '%suburb_slug%/%pagename%';
add_rewrite_tag('%suburb_slug%', '([^/]+)', 'suburb_slug=');
$feedregex = tsg_get_feedregex();
$feedregex2 = tsg_get_feedregex(2);
// Fix root rules.
add_rewrite_rule('^sitemap\.xml$', 'index.php?sitemap=index', 'top');
add_rewrite_rule('^wp-register\.php$', 'index.php?register=true', 'top');
add_rewrite_rule('^wp-app\.php(/.*)?$', 'index.php?error=403', 'top');
add_rewrite_rule('^wp-(atom|rdf|rss|rss2|feed|commentsrss2)\\.php$', 'index.php?feed=old', 'top');
add_rewrite_rule("^$feedregex", 'index.php?feed=$matches[1]', 'top');
add_rewrite_rule("^$feedregex2", 'index.php?feed=$matches[1]', 'top');
add_rewrite_rule("^embed/?$", 'index.php?embed=true', 'top');
add_rewrite_rule('^page/?([0-9]{1,})/?$', 'index.php?paged=$matches[1]', 'top');
add_rewrite_rule("^comments/$feedregex", 'index.php?feed=$matches[1]&withcomments=1', 'top');
add_rewrite_rule("^comments/$feedregex2", 'index.php?feed=$matches[1]&withcomments=1', 'top');
add_rewrite_rule('^comments/embed/?$', 'index.php?pagename=comments&embed=true', 'top');
add_rewrite_rule('([^/]+)/trackback/?$', 'index.php?suburb_slug=$matches[1]&tb=1', 'top');
add_rewrite_rule('([^/]+)(?:/([0-9]+))?/?$', 'index.php?suburb_slug=$matches[1]&page=$matches[2]', 'top');
// Fix %suburb_slug% rules.
add_rewrite_rule('([^/]+)/comments/?$', 'index.php?suburb_slug=$matches[1]&pagename=comments', 'bottom');
add_rewrite_rule('([^/]+)/comments/embed/?$', 'index.php?suburb_slug=$matches[1]&pagename=comments&embed=true', 'bottom');
add_rewrite_rule('([^/]+)/author/?$', 'index.php?suburb_slug=$matches[1]&pagename=author', 'bottom');
add_rewrite_rule('([^/]+)/tag/?$', 'index.php?suburb_slug=$matches[1]&pagename=tag', 'bottom');
add_rewrite_rule('([^/]+)/category/?$', 'index.php?suburb_slug=$matches[1]&pagename=category', 'bottom');
}
/**
* Customise rewrite rules.
*
* @param array $rules
* @return array
*/
function tsg_customise_rewrite_rules_array(array $rules): array
{
$post_types = get_post_types([
'has_archive' => true,
'_builtin' => false
]);
// Custom post types don't apply the filter to replace structure tags
// with regex for the array keys. Substitute our own,
// without changing the array key position.
$is_wrong = false;
foreach ($post_types as $post_type) {
foreach ($rules as $rule => $query) {
if (strpos($rule, "%suburb_slug%/$post_type") !== false) {
$is_wrong = true;
$new_rule = str_replace('%suburb_slug%', '([^/]+)', $rule);
$rules = tsg_replace_key($rules, $rule, $new_rule);
}
}
}
// After the above fix, the queries are messed up, correct them.
if ($is_wrong) {
foreach ($post_types as $post_type) {
foreach ($rules as $rule => $query) {
if (
strpos($rule, "([^/]+)/$post_type") !== false &&
strpos($rule, "([^/]+)/$post_type/category") === false &&
strpos($rule, "([^/]+)/$post_type/([^/]+)") === false &&
strpos($rule, "([^/]+)/$post_type/page/?") === false
) {
if (strpos($rule, $post_type . '/?$') !== false) {
$rules[$rule] =
'index.php?suburb_slug=$matches[1]&post_type=' . $post_type;
} elseif (strpos($rule, '/feed') !== false || strpos($rule, '/(feed') !== false) {
$rules[$rule] =
'index.php?suburb_slug=$matches[1]&post_type=' . $post_type . '&feed=$matches[2]';
} elseif (strpos($rule, '/page') !== false) {
$rules[$rule] =
'index.php?suburb_slug=$matches[1]&post_type=' . $post_type . '&paged=$matches[2]';
}
}
}
}
}
// Remove rewrite rules that don't work,
// or that we can't be bothered dealing with.
$remove_by_rule = [
'amp_',
'type',
'search',
'.*wp-(atom|rdf|rss|rss2|feed|commentsrss2)\\.php$',
'.*wp-app\\.php(/.*)?$',
'.*wp-register.php$'
];
$remove_by_query = [
'attachment',
'?&'
];
foreach ($rules as $rule => $query) {
foreach ($remove_by_rule as $match) {
if (strpos($rule, $match) !== false) {
unset($rules[$rule]);
}
}
foreach ($remove_by_query as $match) {
if (strpos($query, $match) !== false) {
unset($rules[$rule]);
}
}
}
return $rules;
}
/**
* Fix request query variables.
*
* @param array $query_vars
* @return array
*/
function tsg_customise_request_query_vars(array $query_vars): array
{
// Fix canonical redirect and allow root pages.
if (
isset($query_vars['suburb_slug']) &&
!array_key_exists($query_vars['suburb_slug'], tsg_get_all_suburbs_by_slug())
) {
$query_vars['pagename'] = $query_vars['suburb_slug'];
unset($query_vars['suburb_slug']);
}
return $query_vars;
}
/**
* The %suburb_slug%/%pagename% query variable has no effect on the
* 404 status of posts for some reason.
*
* @return void
*/
function tsg_verify_query_vars(): void
{
global $wp_query;
$slug = get_query_var('pagename');
if (
$slug &&
is_single() &&
!array_key_exists($slug, tsg_get_all_suburbs_by_slug())
) {
$wp_query->set_404();
status_header(404);
nocache_headers();
}
}
/**
* Add query variable %suburb_slug%.
*
* @param array $public_query_vars
* @return array
*/
function tsg_add_query_var(array $public_query_vars): array
{
$public_query_vars[] = 'suburb_slug';
return $public_query_vars;
}
/**
* Add the permalink structure tag %suburb_slug% to the admin page.
*
* @param array $tags
* @return array
*/
function tsg_add_permalink_structure_tag(array $tags): array
{
$tags['suburb_slug'] = __('%s (The slug of the Suburb.)');
return $tags;
}
/**
* Replace %suburb_slug% with the current suburb slug.
*
* @param string $link
* @return string
*/
function tsg_customise_link(string $link): string
{
return str_replace('%suburb_slug%', tsg_get_current_suburb_slug(), $link);
}
/**
* Replace main suburb slug with root, or alternate slug if provided.
*
* @param string $link
* @param string $slug
* @return string
*/
function tsg_customise_link_slug(string $link, string $slug = '/'): string
{
$slug = $slug !== '/' ? "/$slug/" : $slug;
return str_replace('/' . MAIN_SUBURB_SLUG . '/', $slug, $link);
}
/**
* Replace %suburb_slug% and main suburb slug in page links.
*
* @param string $link
* @return string
*/
function tsg_customise_page_link(string $link): string
{
return tsg_customise_link_slug(tsg_customise_link($link));
}
/**
* Grab the slug of the current suburb.
*
* @param bool $echo
* @return string|void
*/
function tsg_get_current_suburb_slug(bool $echo = false)
{
$slug = MAIN_SUBURB_SLUG;
$query_var = get_query_var('suburb_slug');
if (!empty($query_var) && array_key_exists($query_var, tsg_get_all_suburbs_by_slug())) {
$slug = $query_var;
}
if ($echo) {
echo $slug;
} else {
return $slug;
}
}
/**
* Are we on the main suburb?
*
* @param string $slug
* @return bool
*/
function tsg_is_main_suburb(string $slug = ''): bool
{
if ($slug) {
return $slug === MAIN_SUBURB_SLUG;
}
return tsg_get_current_suburb_slug() === MAIN_SUBURB_SLUG;
}
/**
* Return the current suburb.
*
* @return WP_Term object or false on failure
*/
function tsg_get_current_suburb()
{
return tsg_get_suburb_by_slug(tsg_get_current_suburb_slug());
}
/**
* Retrieve a suburb object by slug.
*
* @param string $slug
* @return WP_Term object or false on failure
*/
function tsg_get_suburb_by_slug(string $slug)
{
$terms = tsg_get_all_suburbs_by_slug();
if (isset($terms[ $slug ])) {
return $terms[ $slug ];
}
return false;
}
/**
* Returns a list of Suburbs in the form of
* [
* term_id => WP_Term {},
* ...
* ]
*
* @return array
*/
function tsg_get_all_suburbs(): array
{
$result = get_option('tsg_suburbs');
if (!$result) {
$terms = get_terms('suburb', [ 'hide_empty' => false ]);
$result = [];
foreach ($terms as $term) {
$result[ $term->term_id ] = $term;
}
update_option('tsg_suburbs', $result, false);
}
return $result;
}
/**
* Returns a list of Suburbs in the form of
* [
* term_slug => WP_Term {},
* ...
* ]
*
* @return array
*/
function tsg_get_all_suburbs_by_slug(): array
{
$result = get_option('tsg_suburbs_by_slug');
if (!$result) {
$terms = tsg_get_all_suburbs();
$result = [];
foreach ($terms as $term) {
$result[ $term->slug ] = $term;
}
update_option('tsg_suburbs_by_slug', $result, false);
}
return $result;
}
/**
* Clear cached suburb lists.
*
* @return void
*/
add_action('edited_suburb', 'tsg_update_suburb', 11);
add_action('create_suburb', 'tsg_update_suburb', 11);
function tsg_update_suburb(): void
{
delete_option('tsg_suburbs');
delete_option('tsg_suburbs_by_slug');
if (function_exists('w3tc_flush_all')) {
w3tc_flush_all();
}
}
/**
* Register Suburbs taxonomy.
*
* @return @void
*/
add_action('after_setup_theme', function (): void {
register_taxonomy(
'suburb',
[ 'post' ],
[
'label' => 'Suburbs',
'labels' => [
'name' => 'Suburbs',
'singular_name' => 'Suburb',
'add_new' => 'Add New',
'add_new_item' => 'Add New Suburb',
'edit_item' => 'Edit Suburb',
'new_item' => 'New Suburb',
'view_item' => 'View Suburb',
'search_items' => 'Search Suburbs',
'not_found' => 'Nothing Found',
'not_found_in_trash' => 'Nothing found in the Trash',
'parent_item_colon' => ''
],
'publicly_queryable' => false,
'has_archive' => false,
'rewrite' => false,
'hierarchical' => true,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_admin_column' => false,
'capabilities' => [ 'assign_terms' => 'edit_posts' ],
'show_in_rest' => true
]
);
});
/**
* Limit posts to just those for the current suburb.
*
* @param WP_Query $query Query object before WP_Query is called.
* @return WP_Query
*/
add_action('pre_get_posts', 'tsg_pre_get_posts');
function tsg_pre_get_posts(WP_Query $query): WP_Query
{
if ($query->is_admin) {
return $query;
}
// Bypass this entirely for menus.
if (
isset($query->query[ 'post_type' ]) &&
$query->query[ 'post_type' ] === 'nav_menu_item'
) {
return $query;
}
$suburb = tsg_get_current_suburb();
if (empty($suburb)) {
$query->set('pagename', '');
$query->set_404();
status_header(404);
nocache_headers();
return $query;
}
// Main suburb sees all posts.
if (tsg_is_main_suburb($suburb->slug)) {
return $query;
}
// Apply the tax query.
$query->set('tax_query', [
'relation' => 'AND',
[
'taxonomy' => 'suburbs',
'field' => 'slug',
'include_children' => false,
'terms' => [ tsg_get_current_suburb_slug() ],
'operator' => 'IN'
]
]);
return $query;
}
/**
* Debug rewrite rules.
* Based on: https://gist.github.com/adamrosloniec/e34fcc7a0743769c75db1b072d677946
*
* @param WP $query
* @return void
*/
function tsg_debug_rewrites(WP $query): void
{
global $wp_rewrite, $wp_post_types, $wp_taxonomies;
if (empty(DEBUG_REWRITES)) {
return;
}
if (is_admin() || !is_user_logged_in()) {
return;
}
echo '<p><strong>--- START REWRITE DEBUG ---</strong></p>';
echo '<h2>Rewrite Rules</h2><table style="font-size:1em;">' .
'<tr><th align="left">Rule</th><th align="left">Query</th></tr>';
foreach ($wp_rewrite->wp_rewrite_rules() as $rule => $match) {
$rewrite_bg = $rule === $query->matched_rule ? 'style="background:yellow;"' : '';
echo "<tr $rewrite_bg><td>" .
var_export($rule, true) . "</td><td>$match</td></tr>";
}
echo '</table>';
echo '<h2>Permalink Structure</h2><table style="font-size:1em;">' .
'<tr><th align="left" colspan="2">Post Type</th></tr>' .
'<tr><td>Page</td><td>' . $wp_rewrite->get_page_permastruct() . '</td></tr>' .
"<tr><td>Post</td><td>$wp_rewrite->permalink_structure</td></tr>";
foreach ($wp_post_types as $post_type) {
if (
!empty($post_type->name) &&
!empty($post_type->label) &&
// @phpstan-ignore-next-line
!empty($post_type->rewrite['slug'])
) {
$post_type_bg = !empty($query->query_vars['post_type']) &&
$post_type->name === $query->query_vars['post_type']
? 'style="background:yellow;"'
: '';
echo "<tr $post_type_bg><td>$post_type->label</td><td>" .
$post_type->rewrite['slug'] . '</td></tr>';
}
}
echo '<tr><th align="left" colspan="2">Taxonomy</th></tr>';
foreach ($wp_taxonomies as $taxonomy) {
if (
!empty($taxonomy->name) &&
// @phpstan-ignore-next-line
!empty($taxonomy->labels->singular_name) &&
// @phpstan-ignore-next-line
!empty($taxonomy->rewrite['slug'])
) {
$taxonomy_bg = !empty($query->query_vars[$taxonomy->name])
? 'style="background:yellow;"'
: '';
echo "<tr $taxonomy_bg><td>{$taxonomy->labels->singular_name}</td><td>" .
$wp_rewrite->get_extra_permastruct($taxonomy->name) . '</td></tr>';
}
}
echo '<tr><th align="left" colspan="2">Archive</th></tr>';
$author_bg = !empty($query->query_vars['author_name']) ? 'style="background:yellow;"' : '';
echo "<tr $author_bg><td>Author</td><td>" .
$wp_rewrite->get_author_permastruct() . '</td></tr>';
$date_bg = !empty($query->query_vars['author_name']) ? 'style="background:yellow;"' : '';
echo "<tr $date_bg><td>Date</td><td>" .
$wp_rewrite->get_date_permastruct() . '</td></tr>';
echo '<tr><th align="left" colspan="2">Feed</th></tr>';
echo "<tr><td>Feed</td><td>" .
$wp_rewrite->get_feed_permastruct() . '</td></tr>';
echo "<tr><td>Comments</td><td>" .
$wp_rewrite->get_comment_feed_permastruct() . '</td></tr>';
echo '</table>';
echo '<h2>Request</h2><p>' .
var_export($query->request, true) . '</p>';
$matched_bg = !empty($query->matched_rule) ? 'style="background:yellow;"' : '';
echo "<h2>Matched Rewrite Rule</h2><p $matched_bg>" .
var_export($query->matched_rule, true) . '</p>';
echo '<h2>Matched Query</h2><p>' .
var_export($query->matched_query, true) . '</p>';
echo '<h2>Query Variables</h2><p>' .
var_export($query->query_vars, true) . '</p>';
echo '<p><strong>--- END REWRITE DEBUG ---</strong></p>';
}
/**
* Build a regex to match the feed section of URLs, something like (feed|atom|rss|rss2)/?
* Based on: https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-rewrite.php#L873
*
* @param int $version
* @return string
*/
function tsg_get_feedregex(int $version = 1): string
{
global $wp_rewrite;
$feedregex2 = '';
foreach ((array)$wp_rewrite->feeds as $feed_name) {
$feedregex2 .= $feed_name . '|';
}
$feedregex2 = '(' . trim($feedregex2, '|') . ')/?$';
if ($version === 2) {
return $feedregex2;
}
/*
* $feedregex is identical but with /feed/ added on as well, so URLs like <permalink>/feed/atom
* and <permalink>/atom are both possible
*/
return $wp_rewrite->feed_base . '/' . $feedregex2;
}
/**
* Replace an array key without changing the array key position.
* Based on: https://stackoverflow.com/a/8884153
*
* @param array $array
* @return array
*/
function tsg_replace_key(array $array, $old_key, $new_key): array
{
$keys = array_keys($array);
$index = array_search($old_key, $keys, true);
if ($index === false) {
return $array;
}
$keys[$index] = $new_key;
return array_combine($keys, array_values($array));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment