Introduction

Wordpress is more and more used as a CMS instead of only a Blogging-Software.

However, since Wordpress initially was intended as a Blogging-Software it lacks some major CMS-Features and everything has to be done within the codebase, rather than just having a setting for the task within the backend. I gotta admit, that I don't use a plugin for every task that I need to do within a Wordpress-Installation - I'm a developer, right?

In one of my client's project I wanted to achieve a certain permalink structure for a custom posttype, which has a dynamic permalink-root.

The posttype

The posttype I created was called jobs and was intended for job-offers, where page-visitors could apply for. The code example is stripped down, to the important things.

// functions.php
register_post_type('jobs', [
    'label' => 'Jobs',
    'public' => true,
    'has_archive' => false,
    'menu_icon' => 'dashicons-businessman',
    'rewrite' => [
        'slug' => 'jobs',
        'with_front' => false,
        'feeds' => false,
        'pages' => false
    ]
]);
flush_rewrite_rules(true);

This is how the posttype is registered. As you can see the slug - i.e. the permalink-root - is jobs as specified in line 8. Assume we create a job in the Wordpress backend titled Art Director. This would result in having a permalink of jobs/art-director/.

Note: flush_rewrite_rules() is called here. This should only be done in development mode, especially if you have a big Wordpress site. If you don't add this line, you will have to flush the permalink-structure manually in the backend (go to Settings > Permalinks and click the Save Changes-Button), after making a change in the code, which we will do several times within this post.

Adjust the permalink: make it dynamic

In the client's Wordpress there were also some static pages. A page Company containing information about the client and a page Jobs, which is a subpage of Company and where all jobs should be listed. The jobs are listed through a custom template for this page.

Naturally the Jobs-page had a permalink of company/jobs/. This permalink should be the base for our jobs-posttype, so that our Art Director-job would have a permalink like company/jobs/art-director/.

A first attempt would be to fix the slug, white registering the posttype:

// functions.php
register_post_type('jobs', [
    'label' => 'Jobs',
    'public' => true,
    'has_archive' => false,
    'menu_icon' => 'dashicons-businessman',
    'rewrite' => [
        'slug' => 'company/jobs',
        'with_front' => false,
        'feeds' => false,
        'pages' => false
    ]
]);
flush_rewrite_rules(true);

Great. This would work! However, what if you decide after some time to change the permalink for the Company-page to About Us and therefore the permalink for the Jobs-page would be about-us/jobs/. The permalink for our Art Director would still be company/jobs/art-director/. That's ugly... So we need to make the slug within the registration of the posttype more dynamic:

// functions.php
$slug = rtrim(preg_replace('#^(.*?/){3}#', '', get_permalink(6)), '/'); // 6 is the ID of the Jobs-page
register_post_type('jobs', [
    'label' => 'Jobs',
    'public' => true,
    'has_archive' => false,
    'menu_icon' => 'dashicons-businessman',
    'rewrite' => [
        'slug' => $slug,
        'with_front' => false,
        'feeds' => false,
        'pages' => false
    ]
]);
flush_rewrite_rules(true);

Note the $slug-variable. We first get the permalink of our Jobs-page, which can change over time. The get_permalink($id)-function however returns the complete permalink for this page including the domain and a trailing-slash. We cut the domain (and protocol) with the preg_replace() and remove the trailing slash with rtrim('...', '/').

What if we need even more dynamics?

Doing the above would sufficient in most cases. However there a circumstances where this just isn't enough. Imagine your posttype, is registered through a third-party-plugin, which you don't (and shouldn't) modify to keep it updatetable. Another case would be a multi language site, where the permalinks for the root content page is different within each language. In either of these cases you will need to dig a bit deeper into Wordpress' routing system which can be accessed through the global variable $wp_rewrite.

First things first. We should only modify the rewriting rules within an applied action-hook. We could put our code directly in our functions.php, however not all core rewrite-rules of Wordpress have been injected at this point. We should add our rules as additions to the core rules. So we start like this:

// functions.php
function modify_rewrite_rules()
{
    global $wp_rewrite; /* @var $wp_rewrite \WP_Rewrite */
    // ....
}
add_action('init', 'modify_rewrite_rules', 20, 2);

We add an action modify_rewrite_rules() to Wordress' init-hook. Within the callback-function we get the global variable $wp_rewrite which is an instance of WP_Rewrite. This class has some interesting methods you could work with. For simplicity most of it's properties are publicly accessible. Let's dump the object to see what it offers:

// functions.php
function modify_rewrite_rules()
{
    global $wp_rewrite; /* @var $wp_rewrite \WP_Rewrite */
    echo '<pre>';print_r($wp_rewrite);echo '</pre>';exit;
    // ....
}
add_action('init', 'modify_rewrite_rules', 20, 2);

If you look through the dump you will a permalink-structure for our jobs-posttype. However we will be dealing with the property $extra_rules. Depending on which plugins you have installed already this property could be NULL or already be an array with some extra rules. In a fresh Wordpress-installation it's NULL. That's why I'm casting it into an array within the next code example. If it's NULL it will be converted to an empty array. If it already is an array, nothing will happen:

// functions.php
function modify_rewrite_rules()
{
    global $wp_rewrite; /* @var $wp_rewrite \WP_Rewrite */
    $wp_rewrite->extra_rules = (array)$wp_rewrite->extra_rules;
    $slug = preg_replace('#^(.*?/){3}#', '', get_permalink(6));
    // ....
    flush_rewrite_rules(true);
}
add_action('init', 'modify_rewrite_rules', 20, 2);

Here I also added the call to flush_rewrite_rules(), which I explained in the Note earlier. And there is another thing. I fetched the root-permalink which should be the base for our jobs and stored it into $slug (Remember? 6 was the ID of the Jobs-page).

Here comes the final part. We need to put a new rule into the $wp_rewrite-object:

// functions.php
function modify_rewrite_rules()
{
    global $wp_rewrite; /* @var $wp_rewrite \WP_Rewrite */
    $wp_rewrite->extra_rules = (array)$wp_rewrite->extra_rules;
    $slug = preg_replace('#^(.*?/){3}#', '', get_permalink(6));
    $rule = '^' . $slug . '([^/]+)'; // $rule looks like this: ^company/jobs/([^/]+)
    $wp_rewrite->extra_rules[$rule] = '?post_type=jobs&jobs=$matches[1]';
    flush_rewrite_rules(true);
}
add_action('init', 'modify_rewrite_rules', 20, 2);

Okay now for the sake of completion. Imagine we do have a multilanguage-site, with languages German and English configured. We want our jobs to have a permalink like company/jobs/$job-name/ in English and something like unternehmen/stellenangebote/$job-name/ in German. Just add two rules or iterate your languages and add a rule for each:

// functions.php
function modify_rewrite_rules()
{
    global $wp_rewrite; /* @var $wp_rewrite \WP_Rewrite */
    $wp_rewrite->extra_rules = (array)$wp_rewrite->extra_rules;

    // English
    $slug = preg_replace('#^(.*?/){3}#', '', get_permalink(6));
    $rule = '^en/' . $slug . '([^/]+)';
    $wp_rewrite->extra_rules[$rule] = '?post_type=jobs&jobs=$matches[1]';

    // German
    $slug = preg_replace('#^(.*?/){3}#', '', get_permalink(7));
    $rule = '^de/' . $slug . '([^/]+)';
    $wp_rewrite->extra_rules[$rule] = '?post_type=jobs&jobs=$matches[1]';
    flush_rewrite_rules(true);
}
add_action('init', 'modify_rewrite_rules', 20, 2);

Conclusion

Of course these are only examples. Hopefully they give you a starting point if you ever run into the situation where you need to modify the routing-system of Wordpress. Since this is one of my first blogposts, please provide some feedback. Cheers!

Helpful Resources