How do you create a “virtual” page in WordPress

I’m trying to create a custom API endpoint in WordPress, and I need to redirect requests to a virtual page in the root of WordPress to an actual page that ships with my plug-in. So basically, all requests to the one page are actually routed to the other.

Example: =>

The point of this is to make the url for the API endpoint as short as possible (similar to but to ship the actual API endpoint file with the plug-in rather than requiring the user to move files around in their installation and/or hack core.

My first stab was to add a custom rewrite rule. However, this had two problems.

  1. The endpoint always had a trailing slash. It became
  2. My rewrite rule was only ever partially applied. It wouldn’t redirect to wp-content/plugins..., it would redirect to index.php&wp-content/plugins.... This lead to WordPress displaying either a page not found error or just defaulting to the homepage.

Ideas? Suggestions?


There are two types of rewrite rules in WordPress: internal rules (stored in the database and parsed by WP::parse_request()), and external rules (stored in .htaccess and parsed by Apache). You can choose either way, depending on how much of WordPress you need in your called file.

External Rules:

The external rule is the easiest to set up and to follow. It will execute my-api.php in your plugin directory, without loading anything from WordPress.

add_action( 'init', 'wpse9870_init_external' );
function wpse9870_init_external()
    global $wp_rewrite;
    $plugin_url = plugins_url( 'my-api.php', __FILE__ );
    $plugin_url = substr( $plugin_url, strlen( home_url() ) + 1 );
    // The pattern is prefixed with '^'
    // The substitution is prefixed with the "home root", at least a ""
    // This is equivalent to appending it to `non_wp_rules`
    $wp_rewrite->add_external_rule( 'my-api.php$', $plugin_url );

Internal Rules:

The internal rule requires some more work: first we add a rewrite rule that adds a query vars, then we make this query var public, and then we need to check for the existence of this query var to pass the control to our plugin file. By the time we do this, the usual WordPress initialization will have happened (we break away right before the regular post query).

add_action( 'init', 'wpse9870_init_internal' );
function wpse9870_init_internal()
    add_rewrite_rule( 'my-api.php$', 'index.php?wpse9870_api=1', 'top' );

add_filter( 'query_vars', 'wpse9870_query_vars' );
function wpse9870_query_vars( $query_vars )
    $query_vars[] = 'wpse9870_api';
    return $query_vars;

add_action( 'parse_request', 'wpse9870_parse_request' );
function wpse9870_parse_request( &$wp )
    if ( array_key_exists( 'wpse9870_api', $wp->query_vars ) ) {
        include 'my-api.php';

Leave a Comment