Creating Virtual Pages in WordPress

From time to time, I have the need to create the contents for a virtual page — that is, a page that is not stored in the database. This sort of thing is easy using a framework like CodeIgniter, but WordPress’s thing is displaying content from your database and not virtual content that is created only when you need it.

I’ve used a few different methods for doing this over the years and I’ve seen lots of examples on how to do this on the ‘net. But most of these require a lot of code in order to set up the page and create the page contents. I don’t mind a bit of code in the background, but I want the code that uses it to be as small as possible and I want as much code reuse as possible.

The other problem I’ve seen is that the code that creates the page content is often called even if the page that WordPress is trying to render is not your virtual page. So that’s wasted time and processing.

Since I like Object Oriented Programming, I thought I’d create a class to do all the heavy lifting for me. Here’s what I came up with:

<?php/**
 * @snippet       Virtual Page
 * @url           https://davejesch.com/2012/12/creating-virtual-pages-in-wordpress/
 * @author        Dave Jesch
 * @date-written  Dec 15 2012
 * @date-revised  Aug 19 2018
 * @donate $5     https://davejesch.com/send-me-coffee/
 */

if ( ! class_exists( 'D3JVirtualPage', FALSE ) ) {
	class D3JVirtualPage
	{
		private $args = NULL;

		public function __construct( $args )
		{
			if ( ! isset( $args['slug'] ) )
				throw new Exception( 'No slug given for virtual page' );

			$this->args = $args;	// save for use in virtual_page() method
			add_filter( 'the_posts', array( $this, 'virtual_page' ) );
		}

		// filter callback to create virtual page content
		public function virtual_page( $posts )
		{
			global $wp;
			$slug = isset( $this->args['slug'] ) ? $this->args['slug'] : '';
			if ( 0 === count( $posts ) &&
				( 0 === strcasecmp( $wp->request, $slug ) || $slug === $wp->query_vars['page_id'] ) ) {
				// create a fake post instance
				$post = new stdClass();

				// fill properties of $post with everything a page in the database would have
				$post->ID = -1;							// use an illegal value for page ID
				$post->post_author = isset( $this->args['author'] ) ? $this->args['author'] : 1;	// post author id
				$post->post_date = isset( $this->args['date'] ) ? $this->args['date'] : current_time( 'mysql' ); // date of post
				$post->post_date_gmt = isset( $this->args['dategmt'] ) ? $this->args['dategmt'] : current_time( 'mysql', 1 );
				$post->post_content = isset( $this->args['content'] ) ? $this->args['content'] : '';
				$post->post_title = isset( $this->args['title'] ) ? $this->args['title'] : '';
				$post->post_excerpt = '';
				$post->post_status = 'publish';
				$post->comment_status = 'closed';		// mark as closed for comments, since page doesn't exist
				$post->ping_status = 'closed';			// mark as closed for pings, since page doesn't exist
				$post->post_password = '';				// no password
				$post->post_name = $slug;
				$post->to_ping = '';
				$post->pinged = '';
				$post->post_modified = $post->post_date;
				$post->post_modified_gmt = $post->post_date_gmt;
				$post->post_content_filtered = '';
				$post->post_parent = 0;
				$post->guid = get_home_url( '/' . $slug );
				$post->menu_order = 0;
				$post->post_type = isset( $this->args['type'] ) ? $this->args['type'] : 'page';
				$post->post_mime_type = '';
				$post->comment_count = 0;

				// allows for any last minute updates to the $post content
				$post = apply_filters( 'd3j_virtual_page_content', $post );

				// set filter results
				$posts = array( $post );

				// reset wp_query properties to simulate a found page
				global $wp_query;
				$wp_query->is_page = TRUE;
				$wp_query->is_singular = TRUE;
				$wp_query->is_home = FALSE;
				$wp_query->is_archive = FALSE;
				$wp_query->is_category = FALSE;
				unset( $wp_query->query['error'] );
				$wp_query->query_vars['error'] = '';
				$wp_query->is_404 = FALSE;
			}
			return $posts;
		}
	}
}

/**
 * Callback for 'init' action that creates the virtual page
 */
function d3j_create_virtual()
{
	$url = trim( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH), '/' );
	if ( 'dave-virtual-page' === $url ) {
		$args = array( 'slug' => 'dave-virtual-page',
			'title' => 'Dave\'s Virtual Page',
			'content' => 'This can be generated content, or static content&amp;lt;br /&amp;gt;
			Whatever you put here will appear on your virtual page.' );
		$pg = new D3JVirtualPage( $args );
	}
}
add_action( 'init', 'd3j_create_virtual' );

The class does most of the grunt work for you, and as is typical of tools like this you don’t really need to know how it works, just how to use it. So I’ll start with that.

The d3j_create_virtual() function is used to detect and create the virtual page contents when needed. It’s hooked in on the ‘init’ action so it gets called early in the process of handling your page request. Any later and you’ll miss the opportunity to present the page and WordPress will display a ‘Page not found’ result.

It’s first job here is to do the detection. There’s no sense setting up the virtual page if the current request is for a different page. So the first line of code in there uses the parse_url() PHP function in order to grab the “slug” for the current page out of the $_SERVER[‘REQUEST_URI’] variable. Next, we just need to test it and see if it’s for the virtual page that we’re trying to create, in this case a page called creatively, “dave-virtual-page”. If the value in the current url matches this, then it goes ahead and creates the virtual page content.

Creating the virtual page content is done in only two steps. 1) Create the arguments to pass to the virtual page class and then 2) create an instance of that class. The arguments are what determines the contents that will be displayed. At a minimum, you need to specify the slug. The title and content are also important, as these will be displayed as the page title and page contents for your virtual page. To keep your code clean here, you could call another function that fills in the content.

The rest of the work is done by the D3JVirtualPage class itself, so let’s go to that. First, it checks to see that it got a slug so it knows which page to virtualize. If it didn’t get that, it throws an Exception, which will make it very obvious when you view the page that you missed something. It then stores the values from the $args parameter as local class properties. Here you’ll see the other values that you can use to customize the virtual page.

It then sets up a filter for the ‘the_posts’ results. This filter gives you a chance to modify the results of the query that WordPress ran to find your page. So let’s look at the filter. We already know this will only be called when the virtual page is to be displayed, but it still does some checking to make sure that the query results found nothing, and that it’s for the page that we’re building. If it is, then it creates the virtual $post object. This needs to look just like the data that you’d get out of the database, so it sets all the members of that object to completely mimic that. When doing this, it uses some of the class properties — the values that were passed in to the constructor for the page title, content, date, author, etc.

This is the only area of the code that I’m a little worried about: if WordPress changes it’s database model and adds or removes a column to the wp_posts table, then the values being set here won’t exactly match what other parts of WordPress might be looking for. So if the code starts misbehaving, this is probably where you should look. But this data hasn’t been changed in a while and it’s not likely to change a whole lot in the future, so we’re pretty safe.

The last bit of work is to reset the WP_Query object that was used to run the query — it didn’t find a page, after all. So we need to reset the values here to make it look like the page was actually found in the database. Most importantly here is the is_404 property. This has to be set to FALSE to indicate that it’s not an invalid page. Otherwise the HTTP status code of this page request will be a 404 and search engines will think that it’s a bad page, even though you’re displaying content instead of a 404 Page Template. Other properties for singular, archive, category, etc. are adjusted to complete the picture.

So that’s it. All the work is done in the class. All you need to do is detect when you want to create your virtual page based on the URL and then create an instance of the D3JVirtualPage class to complete all the work.

Leave a Comment

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