I have implemented Document Short URL to allow people to access PDF documents on the servers using their unique IDs, without knowing their full paths.

Documents are PDF files, so they should be opened inline browser. This works so that is not the problem.

For example


Where INV0001-00-001 is the Unique ID

My Attempt

add_action('init', 'pins_add_rewrite_rule', 11);

add_action('template_redirect', 'pins_short_url_redirect');

function pins_add_rewrite_rule() {

    add_rewrite_rule('^document/(.*)$', 'index.php/?action=short_url&id=$1', 'top'); // works
    add_rewrite_rule('^document/(.*)$', 'index.php?action=short_url&id=$1', 'top'); // doesn't work

    global $wp;


function pins_short_url_redirect() {

    global $wp_query;
    if (empty($wp_query->query_vars['action'])) {
    $action = $wp_query->query_vars['action'];

    if ($action == 'short_url') {
        require_once 'classes/class-short-url.php';
            PINS_Short_Document_URL::init(); // class responsible for pulling the ID and serve the pdf document in browser
            // We are done

My Questions

  1. Am I doing it correct? Any better suggestions?
  2. Why one add_rewrite_rule works and not the other? If I remove '/' after index.php it doesn't work. Why?
  3. Why do I get 404 in firebug? The ID is valid and I can see the pdf document in browser, however, still get this 404 in firebug

enter image description here

  • Don't flush your rewrite rules every page load!
    – jgraup
    Commented Dec 31, 2015 at 16:19

1 Answer 1


There are two types of rewrite rules you can generate via the API and add_rewrite_rule: internal rules and external rules. External rules get inserted into your .htaccess file, and must be in a format that can be parsed by Apache's mod_rewrite. Internal rules get added to the WordPress internal rewrite rule array and must be in a format that can be parsed by the API via PHP.

WordPress determines what type of rule you're adding by looking at the second argument of add_rewrite_rule. Internal rules must point to index.php, with query vars appended via query string. Any rule that doesn't point to index.php is assumed to be an external rule.

The issue you have with your rules is that you've got a mixture of internal and external formats. I assume the first rule works because the trailing slash confuses the parser and it gets added as an external rule. The second rule doesn't work because it's interpreted as an internal rule, but uses $1 instead of $matches[1] to access the captured matches.

With these things in mind, this rule should work as an internal rule:

add_action('init', 'pins_add_rewrite_rule', 11);
function pins_add_rewrite_rule() {

As for intercepting these requests- you can hook a much earlier action that runs before the database is queried so these requests will be fulfilled much more quickly with less overhead.

add_action( 'parse_query', 'pins_short_url_redirect' );
function pins_short_url_redirect( $wp_query ) {
    if( isset( $wp_query->query_vars['action'] ) && 'short_url' == $wp_query->query_vars['action'] ){
        // output file here

A few other bits:

Use the query_vars filter to add query vars instead of accessing the $wp object directly within init.

action is already a WordPress query var, I don't know if re-registering it will have any negative impact, it's best to be safe and use something you know will be unique, like ghazanfar_action. You should probably do the same for id.

Don't flush rewrite rules on every request. Rewrites should only be flushed when you know they've changed. You can flush on plugin activation, or quickly flush for testing purposes by visiting the Permalinks Settings page in admin. See flush_rewrite_rules for more info.

  • Thansk for the detailed answer @Milo, I didn't know much abotu the internal/external rules. Commented Jan 4, 2016 at 11:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.