Web Development by Solarise

The Solarise.dev Blog

WordPress Rewrite Rules – Pain Free Permalinks with Upstatement/routes

Take the hassle out of WordPress rewrite rules with an external library, Upstatement/routes for pain free routing and URL structures.

Blog archive Robin Metcalfe 20th September 2023

WordPress permalinks – There must be a better way!

My name is Robin, I’m a freelance WordPress developer and I often have projects that require complex non-standard URLs. Relying on WordPress’ internal routing and permalink structure can lead to problems.

Looking for the solution? Jump straight to Upstatement/routes!

WordPress has grown a lot since its creation in 2003. Starting out as a simple blogging platform, it rapidly grew into the incredible software platform we know today.

Originally, WordPress’ permalink structure was rooted in the idea of categories, archives, and simple blog posts.

As the platform grew and diversified, accommodating everything from e-commerce sites to portfolios, the need for a more flexible and sophisticated routing system became evident. But it was still difficult to work around WordPress rewrite rules and their stubborn complexity.

In the very early days, WordPress URLs were straightforward. They often looked something like this:

http://example.com/?p=123

This structure mapped the URL in your browser directly to a database query.

For instance, ?p=123 here would be fetching the post with the ID of 123. Other parameters could be added, for example ?p=123&post_type=post&s=keyword

But this approach is very limited, and leaves room open for

  • Potential SQL injection attacks if the URL is mapped directly to the database
  • System information could be gleaned by the specific format of the URL
  • Poor SEO and poor human readability
  • Ambiguity – difficult to tell at a glance what code the parameters should map to
  • Inefficient caching

As the platform matured, “pretty permalinks” were introduced, allowing URLs to be more human-readable and SEO-friendly, like so

http://example.com/2023/09/my-awesome-blog-post/

However, these pretty permalinks were still really limited. They were based on a set of predefined structures, primarily revolving around dates, post names, and categories. Customising them or adding in new URL structures became difficult. WordPress rewrite rules were still a limiting factor.

The Challenge of Modern Routing In WordPress

Imagine you’re setting up an online shop site with WordPress (forget that WooCommerce exists for a minute – let’s go back to basics!)

You want to run a promo where each product category has its own sale day:

http://example.com/sale/shoes/

http://example.com/sale/jackets/

Using traditional WordPress rewrite rules, achieving this structure would be a bit of a nightmare. You’d likely have to create a “sale” category and then add sub-categories like “shoes” and “jackets”. But what if “shoes” already exists as a primary category? The structure becomes messy and conflicts can arise.

Furthermore, what if you wanted to add a date to the sale, like:

http://example.com/sale/2023-09-16/shoes/

Using traditional WordPress permalinks, you’d face several challenges:

  1. Dynamic Date Integration: If you wanted to run a promo for shoes on September 20th, 2023, your ideal URL might look like this: http://example.com/sale/2023-09-20/shoes. Standard permalinks don’t easily allow for such dynamic date integrations within the URL.
  2. Category Overlaps: As mentioned earlier, if “shoes” already exists as a primary category, integrating it into a sale structure could lead to conflicts or require redundant category creation.
  3. Time-Sensitive Routes: For promos that expire, you’d want the URL to become inactive or redirect to a general sale page after a certain date. Achieving this behavior with standard permalinks would be achievable but… I don’t really want to think about what’s involved there!

(If you’d like to see an example of the code required to map out something like this in WordPress, check out the end of the article)

To understand the power and importance of good permalink structures, let’s look at some examples from other sites:

  1. Medium: https://medium.com/@username/title-of-the-article-uniqueID
    • The structure includes the author’s username, making it personal, and a unique ID ensuring no conflicts.
  2. YouTube: https://youtube.com/watch?v=uniqueID
    • Simple and direct. The unique video ID is all that’s needed to fetch the content.
  3. Airbnb: https://airbnb.com/rooms/uniqueID
    • The URL clearly indicates that you’re looking at a room listing. The unique ID fetches the specific room’s details.
  4. Reddit: https://reddit.com/r/subredditname/posttitle-uniqueID
    • The structure highlights the subreddit, giving context, and the unique ID ensures the correct post is fetched.
  5. GitHub: https://github.com/username/repositoryname
    • The URL structure is clear and hierarchical, first showing the user, then the specific repository.

Advanced Routing With Upstatement/routes

As developers tried to get more control over their URLs with WordPress rewrite rules, particularly if they were digging into complex nested URL routes, the need for more advanced routing solutions arose.

During a project which required some particularly complicated routing config, I discovered a really cool project – https://github.com/Upstatement/routes

Upstatement/routes is designed for use with Timber, a flexible WordPress theme builder, but can be used as a standalone helper.

Upstatement allows for developers to bypass the WordPress rewrite rules and

  • Add custom routes with a clear, concise syntax (e.g. “/blog/:category/:slug”)
  • Easily define alternative templates to be loaded
  • Insert additional logic on a per-route basis
  • Organise routing code in a more logical way

This gives a lot more control over a project’s URL routes – especially if WordPress rewrite rules are giving you a headache, then installing Upstatement/routes could seriously help!

All you need to do is install it via Composer and ensure you’ve added require 'vendor/autoload.php'; to the top of your functions.php

composer require upstatement/routes

You’re then good to start implementing the routing functionality. Here’s a very basic example:

Routes::map('my-custom-page', function($params) {
    $myvar = "Hello";
    Routes::load('custom-template.php', ['myvar' => $myvar]);
});

This should then show you the contents of custom-template.php (assuming it exists within your theme folder) when you visit /my-custom-page in the browser.

Routing in Modern PHP Frameworks

Upstatement aims to replicate the way many modern PHP frameworks handle routing.

In the early days of the web, the browser’s URL path was directly mapped to the server’s file system, reflecting the exact files and folders on disk. This meant that the URL’s structure was often tied to the physical structure of the application’s files.

In time, URL rewriting became the norm, though there was still some connection between the URL and the physical disk or database structure. The default WordPress rewrite rules continued down that road, maintaining some degree of coupling between URL and posts/categories/archives.

However, modern frameworks have moved away from this approach. Instead, they use a style of routing that decouples the URL path from the filesystem. Frameworks such as Zend or Laravel deliver very clean and precise routing systems.

Advanced Routing Example – Flash Sales

Imagine you’re the owner of a thriving e-commerce platform. As part of your marketing strategy, you decide to run a series of promotional events for different product categories. Each promo is unique, with some being time-sensitive, while others are evergreen.

Maybe you’ve already taken a look at WordPress rewrite rules syntax and found it lacking (and confusing!)

A Modern Solution with Advanced Routing

With tools like Upstatement’s router, we could build out our URL strategy like so

  1. Flexible URL Patterns: Define patterns like sale/:date/:category. This allows for dynamic generation of URLs based on the date and category, making it easy to set up and manage multiple promos.
  2. Avoiding Conflicts: Since the routing is decoupled from the traditional category structure, there’s no risk of conflicts. The route sale/2023-09-20/shoes/ and the primary category shoes/ can coexist without issues.
  3. Time-Sensitive Behavior: With advanced routing, you can introduce logic into the routing process. For instance, if a user accesses an expired promo URL, the router can automatically redirect them to the main sale page or display a custom “promo expired” message.
// easily define the URL path you want with a clean syntax
Routes::map('flash-sale/:category', function($params){

    // some additional internal logic
    $currentTime = time();
    $saleStartTime = strtotime("today 2:00 PM");
    $saleEndTime = strtotime("today 5:00 PM");
    
    if ($currentTime < $saleStartTime || $currentTime > $saleEndTime) {
        // If outside the flash sale window, redirect to a teaser page
        wp_redirect(home_url('/flash-sale-teaser/'));
        exit;
    }

    // specify the Loop query to use within the fallback template    
    $query = 'category_name=' . $params['category'] . '&orderby=rand&posts_per_page=5';
    Routes::load('flash-sale.php', null, $query);
});

In this code,

  1. The route logic checks if the current time is within the 2 PM to 5 PM window.
  2. If not, users are redirected to a teaser page that hints at the next flash sale.
  3. If within the window, the route displays the top 5 random products from the specified category, potentially offering them at a discount – you can add any logic you like in here to control product pricing via e.g. other actions/filters

Deepening Engagement with Dynamic Routes

Such dynamic and time-sensitive routes might be one way to significantly enhance user engagement. Customers might frequently check the site around potential flash sale times, increasing traffic and potential sales.

This is something that would be tough to throw together quickly using WordPress` rewrite rules out-of-the-box.

So you can see how this flexible, fluid approach lets you really open up the possibilities when it comes to WordPress routing and template design!

The route to success

Hopefully this should have given you a decent insight into how you can use custom routes to make dealing with URLs, permalinks and WordPress rewrite rules a bit less of a nightmare!

Appendix A – WordPress Rewrite Rules Implementation

Here’s what the code to implement the e-commerce example URLs might look like with the default WordPress rewrite rules syntax. You can see how it’s

  • Less readable
  • More prone to errors with complex regex
  • Just… too much!
function custom_sale_rewrite_rule() {
    // Add a rewrite rule for the sale structure
    add_rewrite_rule(
        '^sale/([0-9]{4}-[0-9]{2}-[0-9]{2})/([^/]+)/?$',
        'index.php?sale_date=$matches[1]&sale_category=$matches[2]',
        'top'
    );
    
    // Register custom query vars for the sale date and category
    add_filter('query_vars', function($vars) {
        $vars[] = 'sale_date';
        $vars[] = 'sale_category';
        return $vars;
    });
}
add_action('init', 'custom_sale_rewrite_rule');

function handle_sale_request() {
    if (get_query_var('sale_date') && get_query_var('sale_category')) {
        // Fetch the products based on the sale date and category
        // This would require additional logic to determine which products are on sale
        $args = array(
            'post_type' => 'product',
            'tax_query' => array(
                array(
                    'taxonomy' => 'product_cat',
                    'field'    => 'slug',
                    'terms'    => get_query_var('sale_category'),
                ),
            ),
        );
        $query = new WP_Query($args);
        
        // Load a custom template to display the sale products
        include 'path-to-your-sale-template.php';
        exit;
    }
}
add_action('template_redirect', 'handle_sale_request');
Author Image

About the author

Robin is the dedicated developer behind Solarise.dev. With years of experience in web development, he's committed to delivering high-quality solutions and ensuring websites run smoothly. Always eager to learn and adapt to the ever-changing digital landscape, Robin believes in the power of collaboration and sharing knowledge. Outside of coding, he enjoys diving into tech news and exploring new tools in the industry.

If you'd like to get in touch to discuss a project, or if you'd just like to ask a question, fill in the form below.

Get in touch

Send me a message and I'll get back to you as soon as possible. Ask me about anything - web development, project proposals or just say hi!

Please enable JavaScript in your browser to complete this form.

Solarise.dev Services

Maintenance Retainers

Min 3 hours per month, no contract

Project Planning

Get in touch to discuss your project details

Consultancy

Face-to-face chats. Solve your problems!