WordPress 4.*: Create Virtual Pages in Code Without Page Database Entries
The standard approach in WordPress to creating unique pages in which the content displayed is entirely defined in code is as follows: create a PHP template file in the active theme, create an empty page with a custom URL, and then associate the page with the template. So for example, if I wanted a page that displayed completely custom formatting for a random selection of posts in a particular category, I'd create a template in my theme much like the following and make use of it as described:
<?php /* Template Name: Random Interesting Things */ get_header(); $query = new WP_Query(array( 'category_name' => 'interesting-things', 'orderby' => 'rand', 'posts_per_page' => 10 )); if ($query->have_posts()) : while ($query->have_posts()) : $query->the_post(); ?> <a name="<?php the_ID(); ?>"></a> <article class="post" id="post-<?php the_ID(); ?>"> <div class="post-body"> <?php the_excerpt();?> <p class="read-more"> <a href="<?php print get_permalink();?>" >Read More »</a> </p> </div> </article> <?php endwhile; endif; wp_reset_postdata(); get_footer();
The need for a page to exist in the database to link the template with a path on the site is very aggravating, however. It is possible to programmatically generate the necessary page row using wp_insert_post(), and to arrange things in plugin or theme code such that the page is created only if needed, and deleted when the plugin or theme is inactivated. This seems painful and clumsy, however. All I want is the ability to specify a path and have a page served at that path by WordPress, with the definition existing entirely in theme or plugin code, and without the need to play around with adding a page or post that administrators can see and perhaps accidentally delete or edit.
Digging around for a better solution took some time, but it turns out that it is possible to use the internal rewrite rules and a few related hooks in WordPress to more closely achieve the goal of a virtual page defined in code. Of course, being WordPress, the approach that works isn't particularly intuitive. It is not a strategy that you would reach on your own unless you happen to be very familiar with WordPress internals. In short, this is how it is done:
- Use a query_vars filter to add one or more new query string parameters recognized by WordPress. E.g. add a
virtualpage
parameter. - Use the init action to add rewrite rules directing desired virtual page paths to the new query string. E.g.
/interesting-things
to/index.php?virtualpage=interesting-things
. - Use a template_include filter to serve each rewritten virtual page path with the appropriate virtual page template, or serve a 404 page for an invalid request.
- Lastly, add mod_rewrite rules to
/.htaccess
to add trailing slashes where appropriate. E.g. set up a redirect to send^interesting-things$
to/interesting-things/
.
The following is an example implementation, and could be placed in either theme or plugin code. The template, as it stands, would have to be in the active theme.
// --------------------------------------------------------------------------- // Add virtual pages. // --------------------------------------------------------------------------- /** * First create a query variable addition for the pages. This means that * WordPress will recognize index.php?virtualpage=name */ function example_virtualpage_query_vars($vars) { $vars[] = 'virtualpage'; return $vars; } add_filter('query_vars', 'example_virtualpage_query_vars'); /** * Add redirects to point desired virtual page paths to the new * index.php?virtualpage=name destination. * * After this code is updated, the permalink settings in the administration * interface must be saved before they will take effect. This can be done * programmatically as well, using flush_rewrite_rules() triggered on theme * or plugin install, update, or removal. */ function example_virtualpage_add_rewrite_rules() { add_rewrite_tag('%virtualpage%', '([^&]+)'); add_rewrite_rule( 'interesting-things/?$', 'index.php?virtualpage=interesting-things', 'top' ); // An alternative approach. // add_rewrite_rule( // 'vp/([^/]*)/?$', // 'index.php?virtualpage=$matches[1]', // 'top' // ); // There is also nothing stopping you from declaring more new variables // via query_vars and mixing them in if a page needs additional parameters. // add_rewrite_tag('%vp_state%', '([^&]+)'); // add_rewrite_rule( // 'interesting-things/([^/]*)/?$', // 'index.php?virtualpage=interesting-things&vp_state=$matches[1]', // 'top' // ); } add_action('init', 'example_virtualpage_add_rewrite_rules'); /** * Assign templates to the virtual pages. */ function example_virtualpage_template_include($template) { global $wp_query; $new_template = ''; if (array_key_exists('virtualpage', $wp_query->query_vars)) { switch ($wp_query->query_vars['virtualpage']) { case 'interesting-things': // We expect to find virtualpage-interesting-things.php in the // currently active theme. $new_template = locate_template(array( 'virtualpage-interesting-things.php' )); break; } if ($new_template != '') { return $new_template; } else { // This is not a valid virtualpage value, so set the header and template // for a 404 page. $wp_query->set_404(); status_header(404); return get_404_template(); } } return $template; } add_filter('template_include', 'example_virtualpage_template_include');
With this code as written, the permalink settings in the WordPress administrative interface must be saved in order to pick up new changes to the rewrite rules. That can be automated away by invoking flush_rewrite_rules() at the appropriate time during theme or plugin activation and deactivation. Examples of how to do this can be found by following that link to the function documentation. Flushing the rewrite rules is an expensive operation, so it absolutely should not be invoked on every page load - only when the rules change.
Finally, to keep things tidy and URLs canonical, add the following to the .htaccess
file for each virtual page to ensure that trailing slashes are always added:
RewriteRule ^interesting-things$ /interesting-things/ [L,R] # The alternative approach. # RewriteRule ^vp/([^/?]+)$ /vp/$1/ [L,R] # With vp_state. # RewriteRule ^interesting-things/([^/?]+)$ /interesting-things/$1/ [L,R]