The WordPress, Rewrite API is like a mythical creature. Not many know it exists, and few still know how to tame it. In this article, we’ll cover the basics of the Rewrite API and how to make it work for you.

The Rewrite API includes three commonly used functions (and a fourth function that doesn’t get much love).

add_rewrite_rule

add_rewrite_tag

add_rewrite_endpoint

add_feed – Not incredibly useful, generally, but it could be!

How WordPress Permalinks Work

When you request a page on a WordPress site, assuming it’s using pretty permalinks, the rules in htaccess file (or webconfig, if you’re into windows) first test to see if the request file and or directory exists on the server. In other words, if someone request yoursite.com/some-folder/ and you’ve created some-folder in the root of your site (via FTP or whatever else), Apache will serve that directory. Same deal for static files. NginX works a similar way, but does so with a try_files directive: try_files $uri $uri/ index.php .

If the folder or file doesn’t exist, the server directs the request to index.php, which is, of course, where the magic happens. From here WordPress loads. During this process, WordPress tries to match the request’s url (the stuff after yourdomain.com ) with a series of rewrite rules, which are just regular expressions. If it finds a match, WP will translate the URI into a database query, render the correct template file and serve up the page.

When you add a rewrite rule, you’re augmenting the built in rewrite rules with your own. There’s all kinds of stuff you can do with this. Like creating custom shortlinks or giving your WordPress site an API.

Adding a Rewrite Rule

add_rewrite_rule is the thing to add your own custom rules. It tags three arguments: the regex, the index.php + query string URL you to rewrite the regex matching URI to, and the priority. Generally $priority is always set to “top” to push your rewrite rules before the built in WP rules.

Here’s an example. We’re going to rewrite yoursite.com/p/some_number to a post that has the ID, some_number. We’ll hook into init to add the rule.

<?php add_action( 'init', 'pmg_rewrite_add_rewrites' ); function pmg_rewrite_add_rewrites() { add_rewrite_rule( '^p/(d+)/?$', // p followed by a slash, a series of one or more digits and maybe another slash 'index.php?p=$matches[1]', 'top' ); }

The first argument says, “match p followed by a slash, then a series of one or more numbers (and maybe another slash).” The second argument tells wordpress to rewrite the rule to index.php?p=the_group_of_numbers. $matches[1] refers to the first group of parenthesis inside the rewrite regex (the first argument).

Before this will work for you, however, you’ll need to flush your rewrite rules. You can do this pro grammatically, with an activation hook in a plugin, or manually by visiting the Settings > Permalinks page in your WordPress admin and hitting save.

Here’s how to do it with an activation hook:

<?php register_activation_hook( __FILE__, 'pmg_rewrite_activation' ); function pmg_rewrite_activation() { pmg_rewrite_add_rewrites(); flush_rewrite_rules(); }

The reason the above rewrite rule works is because WordPress recognizes what the p query string key means — it knows to make it to a post id. What if you want to create something like a URL endpoint for a form submission? None of the built in query variables would do for this, so we need to role our own. Here’s the rewrite rule:

<?php add_rewrite_rule( '^submit/?$', 'index.php?form=true', 'top' );

If you flush your rewrite rules and visit yoursite.com/submit/ , it’s going to show the blog/index page. WordPress doesn’t know what to do with the form query variable. And if you try to grab the variable with get_query_var, it’s not going to work. Why? WP did not recognize the the form variable, so it stripped it out.

There’s one of two ways you can add a query variable. We’ll cover one here, and one in the next section of this article. To add the variable you add a filter to query_vars and push form onto the query variable array.

<?php add_filter( 'query_vars', 'pmg_rewrite_add_var' ); function pmg_rewrite_add_var( $vars ) { $vars[] = 'form'; return $vars; }

Catching Custom Query Variables

You can get a query variable with the get_query_var function. Like this.

<?php if( get_query_var( 'form' ) ) { // do stuff }

get_query_var can be used any time after the query is set up — any time after init . It’s safe to use inside a template, in other words. This is a fairly common practice for me: hook into template redirect, catch a custom query variable, do stuff inside the WordPress environment, then exit(); to stop the theme from loading.

<?php add_action( 'template_redirect', 'pmg_rewrite_catch_form' ); function pmg_rewrite_catch_form() { if( get_query_var( 'form' ) ) { // do stuff exit(); } }

Adding Rewrite Tags

add_rewrite_tag lets you add custom tags similar to %postname% or any of the permalink structure tags. It can be used in place of filtering query_vars .

In other words, instead of this:

<?php add_rewrite_tag( '%form%', '[^/]' ); add_rewrite_rule( '^submit/?$', 'index.php?form=true', 'top' );

Is a complete unit. No need to filter query_vars to make sure WordPress recognizes the form variable. That said, is this the right approach? Rewrite tags are more helpful if you’re going to use them as part of, say, a custom permlink structure for a custom post type. In our example, they probalby aren’t necessary. add_rewrite_tag is also misleading. It sort of implies that, with registration, you somehow tell WordPress to know how to replace the tag with the correct value. WP does not do that. You’ll have to do that manually, but it is possible.

Adding Rewrite Endpoints

Rewrite endpoints are things that get tacked out the end of URLs on your site. Trackbacks are a good example: any post on a WordPress side has the endpoint /trackback/ that alters the behavior of the page to, you guessed it, add pingbacks/trackbacks.

We’ll add an endpoint to posts to create an “api”. Whenever you visit yoursite.com/some-permalink/json/ WP will spit out a nice json version of the post. add_rewrite_endpoint tags two arguments: (1) the endpoint itself and (2) where you want the endpoint to live ( $place ). $place is going to be one of the many EP_* constants (view them here). If you want to place an endpoind in more than one place, you can combine EP constants with the bitwise OR (the pipe: |).

Like our other rewrites, we’ll hook into init to add our endpoint.

<?php add_action( 'init', 'pmg_rewrite_add_rewrites' ); function pmg_rewrite_add_rewrites() { add_rewrite_endpoint( 'json', EP_PERMALINK ); }

add_rewrite_endpoint takes care of adding the rewrite rules and adding the query variable for us! Here’s the code we’ll use to catch our /json/ endpoint.

<?php add_action( 'template_redirect', 'pmg_rewrite_catch_form' ); function pmg_rewrite_catch_form() { if( is_singular() && get_query_var( 'json' ) ) { exit(); } }

If you visit yoursite.com/some-permalink/json/ (after flushing rewrite rules, of course) it’s not going to work, but yoursite.com/some-permalink/json/asdf will! That’s because add_rewrite_endpoint is going to set the json query variable equal to the stuff that comes after the /json/ endpoint. If it’s an empty string (as it is with yoursite.com/some-permalink/json/ ) php will evaluate it as false.

if( is_singular() && get_query_var( ‘json’ ) !== false ) won’t work either as it will stop all permalinks from loading. Instead, we need to hook into request and give our json query variable a value if it’s set.

<?php add_filter( 'request', 'pmg_rewrite_filter_request' ); function pmg_rewrite_filter_request( $vars ) { if( isset( $vars['json'] ) ) $vars['json'] = true; return $vars; }

Now if you visit yoursite.com/some-permalink/json/ , it will work. Next up we just need to enhance our handler function (hooked into template_redirect a bit).

<?php add_action( 'template_redirect', 'pmg_rewrite_catch_json' ); function pmg_rewrite_catch_json() { if( is_singular() && get_query_var( 'json' ) ) { $post = get_queried_object(); $out = array( 'title' => $post->post_title, 'content' => $post->post_content ); header('Content-Type: text/plain'); echo json_encode( $out ); exit(); } }

Now if you visit yoursite.com/some-permalink/json/ a nice JSON will be there for consumption.

Adding Custom Feeds

The little used (or talked about) add_feed function is used to — well, to add feeds to wordpress. Like yoursite.com/feed/rss . add_feed takes to arguments: the feed name and the function you wish to fire when the feed is loaded. Like our other rewrite, writes, we’ll hook into init . Since we’re into JSON in this article, let’s make a feed that displays our list of posts as a JSON string.

<?php add_action( 'init', 'pmg_rewrite_add_rewrites' ); function pmg_rewrite_add_rewrites() { add_feed( 'json', 'pmg_rewrite_json_feed' ); }

The function pmg_rewrite_json_feed is going to receive one argument: whether or not this is a comments feed. Since we don’t really need this, we’ll leave it out. Our hooked function is what you’d expect: get some posts, turn them into a JSON.

<?php function pmg_rewrite_json_feed() { $posts = get_posts(); $out = array(); foreach( $posts as $p ) { $out[] = array( 'title' => $p->post_title, 'content' => $p->post_content ); } header('Content-Type: text/plain'); echo json_encode( $out ); }

Demystifying the WordPress Rewrite API

Hopefully this article cleared some things up for you regard the rewrite API. It’s very powerful, and allows you to use WordPress in ways that are well outside its usual scope. All of this code is available here.