WordPress Rewrite API Examples

Rewrite rules are how WordPress creates clean / pretty URIs from URL query parameters. When your new page or blog post automatically gets a human-friendly URL, this is provided by a rewrite rule, which itself is using WordPress’s Rewrite API.

In this post I hope to cover the basics of the Rewrite API, create a few new rewrite rules as examples, and make use of the data the Rewrite API provides the global WP_Query object when it matches a rewrite tag. Let’s get started!

How does URL rewriting work?

You’ve probably heard of “redirects” before. This is when one URL sends you to another URL. In your browser, the URL in the location bar will actually change to reflect the new URL you have been redirected to.

Rewrites are similar to redirects, but they are done behind the scenes so that the URL in your browser’s location bar does not change. This should become more clear by examining how you can configure your web server to handle rewrites.

The web server does the rewrite

For rewrites to work in any system, the web server must be configured correctly. For Apache web servers, this is most-easily achieved in the form of an .htaccess file. This file tells Apache to take the requested URL and serve it as a URI to the index.php file.

Let’s see what this looks like:

<IfModule mod_rewrite.c>
    # enable rewriting
    RewriteEngine on

    # don't rewrite files that exist in the file system
    RewriteCond %{REQUEST_FILENAME} !-f

    # don't rewrite directories that exist in the file system
    RewriteCond %{REQUEST_FILENAME} !-d

    # rewrite the request to index.php
    RewriteRule ^ index.php [QSA,L]
</IfModule>
.htaccess

First, the configuration is checking to make sure the Apache rewrite module is enabled. Then, it makes sure that the requested URL is not a real file or directory within the codebase. And finally, it rewrites the requested URI to the index.php file.

Example: If you were to visit example.com/some-pretty-url, behind the scenes Apache would deliver that URI to the index.php file, resulting in example.com/index.php/some-pretty-url (behind the scenes).

For all intents and purposes, this is now acting as a Front Controller. Meaning that you now have a single point of entry for requests into your application. That single point of entry is your index.php file.

index.php handles the request

Now that your requests are all going through index.php, you can write a script within that file to look for the request, and handle it appropriately. Here is a very simple example of how that might be done using a pattern matching strategy similar to that you’ll find in WordPress:

<?php
/**
 * Process a requested URI into a simple response
 */
// requested URI
$request = trim( $_SERVER['REQUEST_URI'], '/' );
// default response
$response = '--- not found';
// a empty array in which to parse query strings
$query_vars = [];
// see if the requested uri matches the pattern for a single post
$query = preg_replace_callback(
  // the regex pattern to be matched
  '#post/(.*)#',
  // the callback that fires if the regex is matched
  function( $matches ){
    // a query-like string with key-value pairs.
    return "post=$matches[1]";
  },
  // the requested URI
  $request );
// parse the query string into the array
parse_str( $query, $query_vars );
// respond to the request for a single post
if ( isset( $query_vars['post'] ) ){
  $response = "Well hellooooooo!  You've reached the post with the slug: {$query_vars['post']}";
}
print $response;
index.php

Note that the global $_SERVER variable contains a key called ‘REQUEST_URI’. This is how you get the URI that was requested by the visitor: $_SERVER['REQUEST_URI']. Simple huh?

Next, this script attempts to match the requested URI against a pattern of post/(some value), and if it is found, returns a query-like string with the found key-value pair.

For example: If we were to visit example.com/post/1234, the pattern would find a match and return a string of post=1234.

Then the script parses that query-like string into an array named $query_vars.

And finally, if the ‘post’ key is found in $query_vars, the script alters the response accordingly.

Rewrites in WordPress

These examples should not be used as-is. They are purely for illustrative purposes.

The WordPress rewrite mechanisms work in much the same way. They provide an API through the use of simple functions, and those functions tend to expect either a regex (regular expression) pattern, rewrite string, or both. Let’s take a look at 3 of those API functions.

Function add_rewrite_rule

Adding rewrite rules requires both a regex pattern, and resulting rewrite string. Just like our above example, WordPress will use the regex pattern to attempt to match the requested URI; and when a match is found it will convert the found values into the defined rewrite string.

Let’s look at a simple example:

<?php
add_action('init', 'rewrite_rule_example');
/**
 * Add rewrite rule for a pattern matching "post-by-slug/<post_name>"
 */
function rewrite_rule_example() {
	add_rewrite_rule('^post-by-slug/(.*)/?', 'index.php?name=$matches[1]', 'top');
}
rewrite-rule-example.php

Using the above example along with a real post slug from your site, you could visit the URL example.com/post-by-slug/hello-world and see the content from the default “Hello World” post that comes with a fresh WordPress install.

It achieves this by rewriting the data found in the url after /post-by-slug/ to the core “name” rewrite tag, so that post-by-slug/hello-world is rewritten to index.php?name=hello-world. Later in the page load, when WordPress finds that the “name” query variable has a value, core mechanisms take over and serve the post associated with that name (aka, post_name or slug).

Function add_rewrite_tag

Adding a rewrite tag allows you to provide custom variables to the global $wp_query object. When you add a rewrite tag to WordPress, you are informing the system that this variable is to be expected, and available through the common query API functions such as get_query_var('some-tag'), or directly from the object $wp_query->query_vars['some-tag'].

The main reason to use a rewrite tag is to allow a URL variable to be stored in the global $wp_query object. This lets you access it from any part of the site using get_query_var().

One example use case might be that of tracking visits to your site. For example, if you wanted to track affiliate links to products on your site:

<?php
add_action( 'init', 'rewrite_tag_example_init' );
add_action( 'template_redirect', 'rewrite_tag_example_template_redirect' );
/**
 * Add rewrite rule and tag to WP
 */
function rewrite_tag_example_init(){
	// rewrite tag adds the matches found in the pattern to the global $wp_query
	add_rewrite_tag( '%affiliate%', '(.*)' );
}
/**
 * Modify the query based on our rewrite tag
 */
function rewrite_tag_example_template_redirect(){
	// get the value of our rewrite tag
	$affiliate = get_query_var( 'affiliate' );
	// check if our rewrite tag has value
	if ( !empty( $affiliate ) ){
		// track where this visitor came from
		setcookie( 'affiliate', $affiliate, 0, '/' );
	}
}
rewrite-tag-example.php

In this example a cookie will be set for any user who visits a url with an ?affiliate= query.

The reason to use a rewrite_tag alone is that it does not impose a structure on the URL. It only tells WP_Query to expect a variable of the given name.

Next, let’s combine the use of add_rewrite_rule and add_rewrite_tag to a new reliable application route!

URL Longerer

You may have heard of URL shorteners before. They’re great because they make URLs easier to share online through sites and services that have strict character limits.

But have you ever considered a URL lengthener? I know I haven’t. But none-the-less, I find great joy in creating impractical examples, so here we are.

<?php
add_action( 'init', 'url_longerer_init' );
add_action( 'template_redirect', 'url_longerer_template_redirect' );
/**
 * Add rewrite rule and tag to WP
 */
function url_longerer_init(){
	// rewrite rule tells wordpress to expect the given url pattern
	add_rewrite_rule( '^longerer/(.*)/?', 'index.php?longerer=$matches[1]', 'top' );
	// rewrite tag adds the matches found in the pattern to the global $wp_query
	add_rewrite_tag( '%longerer%', '(.*)' );
}
/**
 * Modify the query based on our rewrite tag
 */
function url_longerer_template_redirect(){
	
	// get the value of our rewrite tag
	$longerer = get_query_var( 'longerer' );
	
	// look for the existence of our rewrite tag
	if ( get_query_var( 'longerer' ) ){
		// get the post ID from the longerer string
		$post_ID = url_longerer_decode_in_some_way( $longerer );
		// attempt to find the permalink associated with this post ID
		$permalink =  get_permalink( $post_ID );
		// if valid, send to permalink
		if ( $post_ID && $permalink ){
			wp_redirect( $permalink );
		}
		// otherwise, send to homepage
		else {
			wp_redirect( home_url() );
		}
		exit;
	}
}
url-longerer.php

In this example we create a new rewrite rule and rewrite tag.

The rewrite rule tells WordPress, “Expect a URI that starts with longerer/, and rewrite it to the URL variable named longerer.”

The rewrite tag tells WordPress, “Keep a lookout for a URL variable named longerer. If you find it, please put it along with its value into the global $wp_query.”

Then using the template_redirect hook, we look for our new tag as a query_var (get_query_var( 'longerer' )), decode it to find the post ID. If the post ID is legitimate, we redirect to that post; otherwise redirect to the home page.

Function add_rewrite_endpoint

An “endpoint” is a partial URI that can be appended to another URI to produce an alternate result or provide additional data. For example, if you had a post located at example.com/post/hello-world and wanted to allow other parties to retrieve that post data as json, you could create an endpoint located at example.com/post/hello-world/json and return json when that endpoint is visited.

Adding endpoints to your WordPress install might be the easiest way to use the Rewrite API, as it doesn’t require any regex or fancy formatting.

Endpoints can also receive values appended to them in the form of another path argument in the URL. Let’s create a few endpoints.

Debug endpoint

This snippet provides an endpoint named “debug” that expects a value of “post” or “query”, and shows debugging information about the global $post or $wp_query objects accordingly.

<?php
add_action( 'init', 'debug_endpoint_init' );
add_action( 'loop_start', 'debug_endpoint_loop_start' );
/**
 * Add our new debug endpoint
 */
function debug_endpoint_init(){
	add_rewrite_endpoint( 'debug', EP_ALL );
}
/**
 * Respond to our new endpoint
 */
function debug_endpoint_loop_start(){
	// main query only
	if ( !is_main_query() ) {
		return;
	}
	$debug = get_query_var( 'debug' );
	// look for a debug query variable that has a value
	if ( !empty( $debug ) ) {
		// show post information if on a permalink
		if ( $debug == 'post' ){
			$post = get_post();
			d( $post );
		}
		// show wp_query info if debug has value of "query"
		if ( $debug == 'query' ) {
			global $wp_query;
			d( $wp_query );
		}
	}
}
rewrite-endpoint-debug.php

Note that I have used the EP_ALL bitmask because I want this endpoint to be available on all pages of my WordPress site.

JSON endpoint for posts and pages

Considering the previously mentioned example of a JSON endpoint, let’s see what this would look like in WordPress:

<?php
add_action( 'init', 'json_endpoint_init' );
add_action( 'template_include', 'json_endpoint_template_include' );
/**
 * Add our new json endpoint
 */
function json_endpoint_init(){
	add_rewrite_endpoint( 'json', EP_PERMALINK | EP_PAGES );
}
/**
 * Respond to our new endpoint
 *
 * @param $template
 *
 * @return mixed
 */
function json_endpoint_template_include( $template ){
	global $wp_query;
	// since the "json" query variable does not require a value, we need to
	// check for its existence
	if ( is_singular() && isset( $wp_query->query_vars['json'] ) ) {
		$post = get_post();
		wp_send_json( (array) $post );
	}
	return $template;
}
rewrite-endpoint-json.php

This is as simple as it gets. We use add_rewrite_endpoint to create a new expectation for an endpoint named “json”, and we tell it to only work on permalinks (individual posts) and pages.

Using the template_include hook, we look to see if our endpoint is being accessed, and return the $post object as json to the visitor.

To see the available EP_* masks, see the add_rewrite_endpoint codex page.

Rewrite Tag for Permalinks

This final example is to show how you can use a rewrite tag to provide a new permalink structure for your site. Similar to the URL Longerer example, this uses both a rewrite rule and a rewrite tag to set expectations for both URIs and query variables.

Additionally this task will need to do a few other things, let’s take a look:

<?php
add_action( 'init', 'rewrite_tag_permalink_init' );
add_action( 'pre_get_posts', 'rewrite_tag_permalink_pre_get_posts' );
// normal posts
add_filter( 'post_link', 'rewrite_tag_permalink_post_link', 10, 2 );
// custom post types
add_filter( 'post_type_link', 'rewrite_tag_permalink_post_link', 10, 2 );
/**
 * Add our rewrite rule and rewrite tag
 */
function rewrite_tag_permalink_init(){
	// rewrite rule looks for custom_folder
	add_rewrite_rule( '^([^/]+)/(.*)/?', 'index.php?custom_folder=$matches[1]&name=$matches[2]', 'bottom' );
	// rewrite tag puts custom_folder value into the query vars
	add_rewrite_tag( '%custom_folder%', '(.*)' );
}
/**
 * Alter permalinks to reflect the new custom_folder rewrite tag
 *
 * @param $permalink
 * @param $post
 *
 * @return mixed
 */
function rewrite_tag_permalink_post_link( $permalink, $post ) {
	$rewrite_tag = '%custom_folder%';
	// return early if tag not found
	if ( strpos( $permalink, $rewrite_tag ) === FALSE ) {
		return $permalink;
	}
	// look for custom permalink meta data
	$custom_folder = get_post_meta( $post->ID, 'custom_folder', true );
	// if a custom permalink exists as meta data, use it
	if ( !empty( $custom_folder ) ){
		$permalink = str_replace( $rewrite_tag, $custom_folder, $permalink );
	}
	// fallback by completely removing our tag from the permalink
	else {
		$permalink = str_replace( $rewrite_tag . '/', '', $permalink );
	}
	return $permalink;
}
/**
 * Use pre_get_posts to make the custom_folder meta data value  optional
 *
 * @param $query
 */
function rewrite_tag_permalink_pre_get_posts( $query ){
	if ( $query->is_main_query() ){
		// if only the first argument is presented, use it as the post name
		if ( get_query_var( 'custom_folder' ) &&
		     !get_query_var( 'name' ) )
		{
			// what we thought was the custom_folder is actually the post_name
			$query->set( 'name', get_query_var( 'custom_folder' ) );
			$query->is_home = false;
			$query->is_singular = true;
			$query->is_single = true;
		}
	}
}
rewrite-tag-permalink.php

Using both the post_link and post_type_link hooks, it alters permalinks as they are presented on the site by replacing our rewrite tag with the value found in the query variables. It also uses pre_get_posts to provide a fallback mechanism for any post that does not have a meta data with the key custom_folder.

I am not really a big fan of this example because it seems a bit hacky for my tastes. Ideally, if you need to provide a new permalink structure using a custom rewrite tag, you provide it for a piece of data you can reliably expect each post to have.

Refreshing rewrite rules

It’s important to note that WordPress caches rewrite rules. This means that when you change your plugin’s rewrite code, you probably need to refresh that cache to pick up your changes.

One way to do that is to visit your site’s Dashboard > Settings > Permalinks page, and “Save Changes”.

Programmatically, you can use the flush_rewrite_rules function. If you’re writing a plugin that implements the Rewrite API, chances are you want to flush rewrite rules on plugin activation.

One last thing

I’d like to reiterate again that these examples are not complete and should not be used as-is. That being said, I hope you learned something useful; and if you see any mistakes in this post, please let me know in the comments below.

Happy rewriting!

References

5 Thoughts

Discussion

Michael Hull
June 9, 2016

Thanks for the post, and for the talk you put together on this. I just had a cool use case come up and I came straight here to revisit what you presented.

One goal I have on a particular site is to create a kind of “combo page,” where we have a Therapy (post type) that is paired with an Ailment (another post type). And on this combo page, it’s supposed to show some bbPress topics that are associated with both the Therapy and the Ailment.

Your sharing about the Rewrite API opened up my eyes to the fact that I don’t have to rely on post types/taxonomies to create my URL’s for me all the time. So…thank you!

I’m now able to have URL’s that look really nice, like /acupuncture/for/headaches. Pretty cool!!

Jonathan Daggerhart
June 14, 2016

Thats a very cool use-case! Thanks for sharing it. You’ll have to send me a link when it’s ready ?

YeZin
July 18, 2016

Thanks

Patrick
January 31, 2019

In your Endpoint section, you mention E_ALL where I think you meant EP_ALL.

Jonathan Daggerhart
February 6, 2019

Good catch, I’ve updated the post. Thanks!

horace
May 22, 2019

thanks for this post!

i would like to avoid using WPML and do my own simple multilanguage solution.

so i need to figure out the language from a url like that: /en/path/to/my/page or /de/path/to/my/page

so if i understand this correctly i simply could use your url longerer example and then use get_page_by_path() instead of your url_longerer_decode_in_some_way(), right?

and the regex would have to be changed from:
add_rewrite_rule( ‘^longerer/(.*)/?’, ‘index.php?longerer=$matches[1]’, ‘top’ );
to:
add_rewrite_rule( ‘^en/(.*)?’, ‘index.php?en=$matches[1]’, ‘top’ );
or something like that?

Lukas
May 25, 2022

Hi Horace! Appreciate it was 2019 but where did you end up with this? I am doing something similar. Did you modify the query in pre_get_posts to grab the actual page you wanted?

Sudhir Pandey
June 13, 2019

Need Some help to generate SEO friendly url of my custom post

I have implement the following code. Please check it

add_rewrite_rule('^([^/]+)/(.*)/(.*)/?', 'index.php?news_detail=$matches[1]news_category=$matches[2]news_slug=$matches[3]','top');
add_rewrite_tag( '%news_detail%', '(.*)' ); 

http://localhost/wptest/news/economic-development/testing-news-content

I have created link like this

$urlLink = add_query_arg(array( 
	'news_detail' => 'news',
	'news_category' => 'economic-development',
	'news_slug' => 'testing-news-content',
), home_url() ); 
Amanda
September 6, 2019

I am adding this code but the URL is not rewriting. what should I do I have seen the exact code here and implemented it following these steps https://www.wpblog.com/wordpress-url-rewrite-for-custom-page-template/

function prefix_WP_rewrite_rule() {
add_rewrite_rule( 'newsmedia/([^/]+)/photos', 'index.php?WP=$matches[1]&photos=yes', 'top' );
add_rewrite_rule( 'films/([^/]+)/videos', 'index.php?WP=$matches[1]&videos=yes', 'top' );
}

add_action( 'init', 'prefix_WP_rewrite_rule' );

			

Leave a Reply

Your email address will not be published. Required fields are marked *