Advertisement
Scroll to top

This is part one of a two part series looking at WordPress' Rewrite API. In this tutorial we look at how rewrites work and the basic methods available to create custom rewrite rules.


What Is Rewriting?

WordPress, like all content management systems, decides what content to display based on the variables (usually called query variables) passed to it. For instance: https://example.com/index.php?category=3 tells WordPress we are after posts in a category with an ID of 3 and http://example.com/index.php?feed=rss tells WordPress we want the site's feed in RSS format.

Unfortunately this can leave us with rather ugly URLs:

1
http://example.com/index.php?post_type=portfolio&taxonomy=wordpress&portfolio=my-fancy-plugin

This is where WordPress' rewriting steps in. It allows us to replace the above with:

1
http://example.com/portoflio/wordpress/my-fancy-plugin

Which is now not only much more readable (and memorable) but also more SEO friendly. This is, in a nutshell, what rewrites do.


How Does It Work?

Now http://example.com/portoflio/wordpress/my-fancy-plugin doesn't exist as a directory or a file. So how does WordPress serve up the correct content? When WordPress receives a 'pretty permalink' like the above it needs to convert that into something it understands, namely a query object. More simply it has to take the pretty URL, and map the appropriate parts to the correct query variable. So for our example:

1
http://example.com/portoflio/wordpress/my-fancy-plugin
  • post_type is set to 'portfolio'
  • portfolio-taxonomy is set to 'wordpress'
  • portfolio is set to 'my-fancy-plugin' (the post name)

Then WordPress knows we are after posts of type 'portfolio', in the 'wordpress' 'portfolio-taxonomy' taxonomy term with name 'my-fancy-plugin'. (As you may have guessed the first two are actually redundant). WordPress then performs that query, chooses the appropriate template with which to display the results and then serves that to the viewer. But clearly WordPress doesn't just guess how to interpret the URLs, it needs to be told...

It Starts With .htaccess

Assuming you can and have enabled pretty permalinks on your Settings -> Permalinks page (see the Codex for minimum requirements – for WordPress on Nginx servers there is this plug-in) – then things start with the .htaccess file. It plays a simple and yet significant role. WordPress includes something similar to the the following in this file:

1
	# BEGIN WordPress

2
	<IfModule mod_rewrite.c>
3
		RewriteEngine On
4
		RewriteBase /
5
		RewriteRule ^index\.php$ - [L]
6
		RewriteCond %{REQUEST_FILENAME} !-f
7
		RewriteCond %{REQUEST_FILENAME} !-d
8
		RewriteRule . /index.php [L]
9
	</IfModule>
10
	# END WordPress

This simply checks if the file or directory actually exists – and if it does, you are simply taken there. For instance:

1
http://example.com/blog/wp-content/uploads/2012/04/my-picture.png

Would simply take you the PNG attachment 'my-picture.png'. But, as in the case of:

1
http://example.com/blog/portoflio/wordpress/my-fancy-plugin

Where the directory does not exist – you are taken to your WordPress' index.php file. It's this file which boots WordPress.

Interpreting the URL

At this point, WordPress doesn't yet know what you're after. After some initial loading of WordPress and its settings, it fires the parse_request method of the WP class (located in the class-wp.php file). It's this method which takes the /portoflio/wordpress/my-fancy-plugin and converts it into a WordPress-understandable query object (almost, it actually sets the query_vars array and afterwards $wp->query_posts turns this into a query).

In short this function compares the received URL (/portoflio/wordpress/my-fancy-plugin) with an array of 'regular expressions'. This is the rewrite array – and it will look something like this:

1
	category/(.+?)/page/?([0-9]{1,})/?$ => index.php?category_name=$matches[1]&paged=$matches[2]
2
	category/(.+?)/?$ => index.php?category_name=$matches[1]
3
	tag/([^/]+)/page/?([0-9]{1,})/?$=> index.php?tag=$matches[1]&paged=$matches[2]
4
	tag/([^/]+)/?$ => index.php?tag=$matches[1]
5
	([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/?$ => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]
6
	(.+?)(/[0-9]+)?/?$ => index.php?pagename=$matches[1]&page=$matches[2]

The keys of this array are regular expressions, and the received URL is compared against each in turn until there is a match with the pattern of the URL received. The corresponding value, is how the URL is then interpreted. The $matches array contains the captured values (indexed from 1) from the matching.

For example, visiting www.example.com/blog/tag/my-tag, WordPress will look for the first pattern that matches 'tag/my-tag'. With the above array, it matches the third pattern: tag/([^/]+)/?$. This tells WordPress to interpret the URL as www.example.com/blog/index.php?tag=my-tag and correspondingly the 'my-tag' archive is served.

Of course, WordPress lets you customise this array, and the rest of this tutorial is dedicated to showing you how.


Customising the Rewrite Rules

Settings -> Permalinks

Your first port of call should be the 'Permalink' settings page. This page allows you to alter the rules for the default 'post' post type, and 'category' and 'tags' taxonomies. The 'default' option has pretty permalinks disabled, but you can select from a list of preset structures or create a custom structure. Please note that custom structures should not contain your site URL. WordPress allows you to alter your permalink structure by adding in provided tags such as %postname% (the post's name), %year% (the year the post was published) and %author% (the author of the post). A permalink structure such as:

1
/%year%/%author%/%postname%/

Would produce a post link such as:

1
www.example.com/2012/stephen/my-post

The documentation for these options can be found in the WordPress Codex. (Later on I'll be showing you how to create your own custom tags).

However, the provided options are quite limited. In this tutorial I shall focus on the functions provided by WordPress that offer greater control over permalink structures and how they are interpreted. I won't be covering the rewrite options available for custom post types or taxonomies, as this will be covered in part 2.

Why Flush the Rewrite Rules?

After any alteration to the rewrite rules (for example, by either using one of the following methods, or registering a custom post type or taxonomy) you may find that the new rules do not take effect. This is because you you need to flush the rewrite rules. This can be done either one of two ways:

  • Simply visit the Settings -> Permalink page
  • Call flush_rewrite_rules() (covered in part 2)

What does this do? Recall that the parse_request method compares the request against a rewrite array. This array lives in the database. Flushing the rewrite rules updates the database to reflect your changes – and until you do so, they won't be recognised. But parse_request also writes to the .htaccess file. This makes it an expensive operation. So, although I won't cover the use of flush_rewrite_rules() until part 2, I will give you this warning: Do not call flush_rewrite_rules on every page load. Plug-ins should only call this when the plug-in is activated and deactivated.

Add Rewrite Rule

The add_rewrite_rule allows you to add additional rules to the rewrite array. This function accepts three arguments:

  • rule – a regular expression with which to compare the request URL against
  • rewrite – the query string used to interpret the rule. The $matches array contains the captured matches and starts from index '1'.
  • position – 'top' or 'bottom'. Where to place the rule: at the top of the rewrite array or the bottom. WordPress scans from the top of the array to the bottom and stops as soon as it finds a match. In order for your rules to take precedence over existing rules you'll want to set this to 'top'. Default is 'bottom'.

Note: if you use add_rewrite_rule several times, each with position 'top' – the first call takes precedence over subsequent calls.

Let's suppose our posts have an event date associated with them and we want to have this structure: www.example.com/blog/the-olympics-begin/2012-07-27 interpreted as www.example.com/blog/index.php?postname=the-olympics-begin&eventdate=2012-07-27 then we can add this rule as follows:

1
	function wptuts_add_rewrite_rules() {
2
		add_rewrite_rule(
3
			'^([^/]*)/([0-9]{4}-[0-9]{2}-[0-9]{2})/?$' // String followed by a slash, followed by a date in the form '2012-04-21', followed by another slash

4
			'index.php?pagename=$matches[1]&eventdate=$matches[2]',
5
			'top'
6
		);
7
	}
8
	add_action( 'init', 'wptuts_add_rewrite_rules' );

The following would interpret www.example.com/olympics/2012/rowing as www.example.com/index.php?p=17&olymyear=2012&game=rowing

1
	add_rewrite_rule(
2
		'^olympics/([0-9]{4})/([^/]*)',
3
		'index.php?p=17&olymyear=$matches[1]&game=$matches[2]',
4
		'top'
5
	);

If you're unsure of your regular expressions, you may find this introduction and this tool useful.

Add Rewrite Tag

You may think that the value of eventdate (2012-07-27 in the above example), olymyear and game may be accessible from WordPress' internals via the get_query_var (in the same way that get_query_var('paged') gets the page number you are on). However, WordPress does not automatically recognise the variable eventdate even though it's interpreted as a GET variable. There are a couple of ways to make WordPress recognise custom variables. One is to use the query_vars filter as demonstrated in the 'Add Custom Endpoint' section below. Alternatively, we can go a step further and use add_rewrite_tag to register a custom tag like the default %postname% and %year%

This function accepts three arguments:

  • tag name – (with leading and trailing %) e.g: %eventdate%
  • regex – Regular expression to validate the value, e.g. '([0-9]{4}-[0-9]{2}-[0-9]{2})'
  • query – (optional) How the tag is interpreted, e.g. 'eventdate='. If provided, must end with a '='.
1
	function wptuts_register_rewrite_tag() {
2
		add_rewrite_tag( '%eventdate%', '([0-9]{4}-[0-9]{2}-[0-9]{2})');
3
	}
4
	add_action( 'init', 'wptuts_register_rewrite_tag');

Not only will get_query_var('eventdate') return the value of the date in the URL, but you can use the tag %eventdate% in the Settings -> Permalink (along with the default %year%, %postname% etc.) and WordPress will correctly interpret it. Unfortunately when generating a post's permalink, WordPress doesn't know how to replace %eventdate% with the appropriate value: so our post permalinks end up like:

1
www.example.com/the-olympics-begin/%eventdate%

We need to replace %eventdate% with an appropriate value, and we can do so using using the post_link filter. (In this example, I shall assume that the value is stored in a custom field 'eventdate').

1
	function wp_tuts_filter_post_link( $permalink, $post ) {
2
3
		// Check if the %eventdate% tag is present in the url:

4
		if ( false === strpos( $permalink, '%eventdate%' ) )
5
			return $permalink;
6
7
		// Get the event date stored in post meta

8
		$event_date = get_post_meta($post->ID, 'eventdate', true);
9
10
		// Unfortunately, if no date is found, we need to provide a 'default value'.

11
		$event_date = ( ! empty($event_date) ? $event_date : '2012-01-01' );
12
13
		$event_date =urlencode($event_date);
14
15
		// Replace '%eventdate%'

16
		$permalink = str_replace( '%eventdate%', $event_date , $permalink );
17
18
		return $permalink;
19
	}
20
	add_filter( 'post_link', 'wp_tuts_filter_post_link' , 10, 2 );

In part 2 of this series I will cover custom tags for custom post types.

Add Custom Endpoint

Endpoints are tags which are appended to the URL (/trackback/[value] is the most common). It has several other potential uses: displaying different templates depending on the value set, custom notifications, and displaying posts in different 'formats' (printable, XML, JSON etc).

You can create endpoints with add_rewrite_endpoint. This function accepts two arguments:

  • name – The name of the endpoint e.g. 'json', 'form', etc.
  • where – Endpoint mask specifying where the endpoint should be added. This should be one of the EP_* constants listed below (or a combination using bitwise operators). When you register custom post types you can create a mask for that post type:

The default endpoint masks are:

  • EP_PERMALINK – for post permalinks
  • EP_ATTACHMENT – for attachments
  • EP_DATE – for date archives
  • EP_YEAR – for year archives
  • EP_MONTH – for month archives
  • EP_DAY – for 'day' archives
  • EP_ROOT – for the root of the site
  • EP_COMMENTS – for comments
  • EP_SEARCH – for searches
  • EP_CATEGORIES – for categories
  • EP_TAGS – for tags
  • EP_AUTHORS – for archive of author's posts
  • EP_PAGES – for pages
  • EP_ALL – for everything

End points are extremely flexible, you can use them with bitwise operators so, for example, you can add an endpoint to post and page permalinks with EP_PERMALINK | EP_PAGES.

1
	function wptuts_add_endpoints() {
2
		add_rewrite_endpoint('myendpoint', EP_PERMALINK); // Adds endpoint to permalinks

3
		add_rewrite_endpoint( 'anotherendpoint', EP_AUTHORS | EP_SEARCH); // Adds endpoint to urls for authors or search results

4
	}
5
	add_action( 'init', 'wptuts_add_endpoints');

As a brief example, endpoints can be useful for form submissions. Suppose we have a contact form page with name contact-form and with permalink: www.example.com/contact and want the the URLs:

  • www.example.com/contact/submission/success – to reflect a successful form submission
  • www.example.com/contact/submission/error – to reflect an unsuccessful form submission

This can be done with endpoints. The following is a very simple example of how to use endpoints, and so the form is incredibly basic in its checks (and in fact doesn't do anything with the data). Normally a form like this would work best in a plug-in, using shortcodes – but for the purposes of this example, create a page with the following template and the rest of the code you can put in your functions.php

1
<?php
2
	/**

3
	* Template Name: Contact Form

4
	*/
5
	include('header.php');?>
6
7
	<h1> My Simple Contact Form </h1>
8
	<?php if ( 'success' == get_query_var( 'form' ) ): ?>
9
		<div class="message">
10
			Your message has been sent!
11
		</div>
12
13
	<?php elseif( 'error' == get_query_var( 'form' )): ?>
14
		<div class="message">
15
			Oops! There seems to have been an error...
16
		</div>
17
	<?php endif ?>
18
19
	<! -- Display form here -->
20
	<form method='post' action=''>
21
22
		<label> Your name: </label> <input type="text" name="wptuts_contact[name]" value=""/>
23
		<label> Your message: </label> <textarea name="wptuts_contact[message]" rows="20" cols="30"></textarea>
24
25
		<! -- Nonce field -->
26
		<?php wp_nonce_field('wptuts_send_message','wptuts_contact_nonce'); ?>
27
28
		<input type="hidden" name="action" value="wptuts_send_message"/>
29
30
		<! -- mmm... honey  -->
31
		<style scoped>.wptuts-e-mail-conformation{display:none}</style>
32
		<span class="wptuts-e-mail-conformation"><label for="email-confirmation">Email confirmation:</label><input type="text" name="wptuts_contact[confirmation]" value="" /></span>
33
34
		<input type='submit' name='wptuts_contact[send]' value='Send' />
35
	</form>
36
37
	<?php include('footer.php');?>

(In case you are wondering, the honey is referring to this very basic method of catching spam outlined here. It's certainly not fool proof, but proper form processing and spam protection is off topic here). Now we create our endpoint:

1
	function wptuts_add_endpoint() {
2
		add_rewrite_endpoint( 'form', EP_PAGES );
3
	}
4
	add_action( 'init', 'wptuts_add_endpoint');

Next we add our 'form' variable to the array of recognised variables:

1
	function wptuts_add_queryvars( $query_vars ) {
2
		$query_vars[] = 'form';
3
		return $query_vars;
4
	}
5
	add_filter( 'query_vars', 'wptuts_add_queryvars' );

Finally we provide a form handler, which will process the data, submit the form, and then redirect back to the contact page with the relevant endpoint value appended.

1
	function wptuts_form_handler() {
2
3
		// Are we wanting to process the form

4
		if( ! isset( $_POST['action'] ) || 'wptuts_send_message' != $_POST['action'] )
5
			return;
6
7
		// ID of the contact form page

8
		$form_id = 2163;
9
		$redirect= get_permalink($form_id);
10
11
		// Check nonces

12
		$data = $_POST['wptuts_contact'];
13
14
		if( !isset($_POST['wptuts_contact_nonce'] ) || !wp_verify_nonce($_POST['wptuts_contact_nonce'],'wptuts_send_message') ) {
15
			// Failed nonce check

16
			$redirect .= 'form/error';
17
			wp_redirect($redirect);
18
			exit();
19
		}
20
21
		if( !empty( $data['confirmation'] ) ) {
22
			// Bees in the honey...

23
			$redirect .= 'form/error';
24
			wp_redirect($redirect);
25
			exit();
26
		}
27
28
		// Santize and validate data etc.

29
30
		// Then actually do something with the sanitized data

31
32
		// Successful!

33
		$redirect .= 'form/success';
34
		wp_redirect($redirect);
35
		exit();
36
	}
37
	add_action( 'init', 'wptuts_form_handler');

Of course even this example could be greatly improved by providing more detailed error messages that convey the reason for failure.

Adding a Custom Feed

With pretty permalinks enabled WordPress automatically produces pretty URLs for your site's feed: www.example.com/feed/rss. The add_feed function allows you to create a custom feed, which if pretty permalinks are enabled, will also have a 'pretty' URL. This function accepts two arguments:

  • feed name – The name of the feed as it will be appear in feed/[feed-name]
  • feed callback – The function responsible for displaying the feed contents.

The following is intended as an example of add_feed, and provides a very basic custom feed.

1
	function wptuts_events_feed_callback() {
2
		$custom_feed = new WP_Query(array('meta_key' => 'eventdate'));
3
4
		header('Content-Type: ' . feed_content_type('rss-http') . '; charset=' . get_option('blog_charset'), true);
5
		echo '<?xml version="1.0" encoding="'.get_option('blog_charset').'"?'.'>'; ?>
6
7
		<rss version="2.0">
8
		<channel>
9
			<title>My custom feed</title>
10
			<link><?php bloginfo_rss('url') ?></link>
11
			<description><?php bloginfo_rss("description") ?></description>
12
			<lastBuildDate><?php echo mysql2date('D, d M Y H:i:s +0000', get_lastpostmodified('GMT'), false); ?></lastBuildDate>
13
			<language><?php echo get_option('rss_language'); ?></language>
14
			<?php if($custom_feed->have_posts()): ?>
15
				<?php while( $custom_feed->have_posts()): $custom_feed->the_post(); ?>
16
					<item>
17
						<title><?php the_title_rss() ?></title>
18
						<link><?php the_permalink_rss() ?></link>
19
						<pubDate><?php echo mysql2date('D, d M Y H:i:s +0000', get_post_time('Y-m-d H:i:s', true), false); ?></pubDate>
20
						<guid isPermaLink="false"><?php the_guid(); ?></guid>
21
						<description><![CDATA[<?php the_excerpt_rss() ?>]]></description>
22
					</item>
23
				<?php endwhile; ?>
24
			<?php endif; ?>
25
		</channel>
26
		</rss>
27
	<?php
28
	}
29
30
	function wptuts_add_feed() {
31
		add_feed('events', 'wptuts_events_feed_callback');
32
	}
33
	add_action( 'init', 'wptuts_add_feed );

After flushing the permalinks, the feed will be available at www.example.com/feed/events.


Checking the Rewrite Rules

Once you've added some of your own rewrite rules (and flushed them), you will probably want to check if they are working properly – and if they're not, find out what's going wrong. For this, I strongly recommended the Monkeyman Rewrite Analyzer plug-in, a free plug-in available in the WordPress repository. Once activated, this plug-in adds a page to your 'Tools' section, which lists all your rewrite rules.

You can also test the rules by giving it an example URL, and the plug-in will filter the rules to show only matching patterns and indicating how WordPress would interpret the URL.

Have fun, and keep an eye out for Part 2, coming soon!

Please note: There is a currently a bug in our syntax highlighter that displays "empty()" as "emptyempty()". Don't forget to adjust your code accordingly.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.