How to make draft posts or posts in review accessible via full url / slug?

Caveat: the code examples within this answer are very basic and may or may not need further conditional logic to adapt to your precise needs, this logic is meant as an example to get you on your way.

There’s two considerations you need to be aware of:

Consideration 1:

If you add a new post and save it with a post_status of draft first, the post will not have the slug in the post_name field in the database *_posts table until you publish at least once.

Consideration 2:

If you add a new post, save it with a post_status of draft, or simply by pass the draft stage and go straight to publish then put the post back into a draft post_status, then the post will have the slug inserted into the post_name column within the *_posts table.

Why this is important:

  • In relation to consideration 1…

WordPress relies on at least one identifying marker, typically the id or the post_name which is unique in order to retrieve a post.

You will notice that when you create a draft post and attempt to preview it, WordPress takes you to a preview URL similar to:

http://example.com/?p=123&preview=true

It does this because the only way to identify and preview the post in question is to do it by the ID to which it is associated with in the databse.

  • In relation to consideration 2…

If you publish the post at least once, then put it back into draft, you will be able to access the post whilst in draft status using the slug (pretty permalink) because the post_name column contains the slug value.

Some example code:

The following code will allow you to return posts in a draft or pending status by using the slug so long as the post has been published at least once before (see consideration 2).

function preview_draft_posts($query) {

  if ( is_admin() || get_query_var('suppress_filters') )
    return $query;

   
  $query->set('post_status', array('publish', 'pending', 'draft'));


  return $query;

}

add_filter('pre_get_posts', 'preview_draft_posts');

The above will work for logged in and logged out users.

What happens if you are logged out and want to preview a draft?

Ok so if the draf has not been published at least once before, the only way to preview the post is by using the following format:

http://example.com/?p=123

WordPress depends on this format to find a post where it has no slug.

If you are logged out though you will not be able to see this post despite the pre_get_posts logic we have used in the example above.

Instead you need to hook onto the filter the_posts and add back the post to the results you want to view because WP_Query will exclude draft posts from the result set for logged out users because logged out users don’t have the proper user-capabilities.

If on the other hand, you attempt (whilst logged out) to access a post similar to that in consideration 1 by it’s default preview URL as mentioned above in the example http://example.com/?p=123&preview=true (with or without the preview=true) then you will be met with a 404 Page Not Found notice.

The following example code remedies that:

function the_posts_preview_draft_posts($posts, $wp_query) {

    //abort if $posts is not empty, this query ain't for us...
    if ( count($posts) ) {
        return $posts;
    }

    $p = get_query_var('p');

    //get our post instead and return it as the result...
    if ( !empty($p) ) {
        return array(get_post($p));
    }
}

add_filter('the_posts', 'the_posts_preview_draft_posts', 10, 2);

Great… but hang on, that still sucks, kind of, because you have to use a URL format such as http://example.com/?p=123 to access the post.

Back to the main problem outlined in consideration 1, the only solutions I can think of are the following:

Solution #1

  • save draft post and find the post by it’s title by converting the slug
    into a title.

Example…

function the_posts_preview_draft_posts($posts, $wp_query) {
    
    //abort if $posts is not empty, this query ain't for us...
    if ( count($posts) ) {
        return $posts;
    }

    $id    = get_query_var('p');
    $name = get_query_var('name');

    //this is a bit of a bad idea...
    if ( empty($id) && !empty($name) ) {

        $name = preg_replace('/\W/', ' ', $name);

        global $wpdb;
        $id = $wpdb->get_var( 
            "
            SELECT ID 
            FROM $wpdb->posts 
            WHERE UPPER(post_title) LIKE UPPER('".$name."')
            LIMIT 1" 
        );

    }


    //get our post instead and return it as the result...
    if ( !empty($id) ) {
        return array(get_post($id));
    }

}

add_filter('the_posts', 'the_posts_preview_draft_posts', 10, 2);

The above… ↑ …is a bad idea but just an example of the lengths you need to go to. Why this is a bad idea is simple, you need to ensure that you have no duplicate post titles in the database and you need to ensure that you do not modify the slug so that it is different to the title when you save it as a draft.

Fraught with issues as you can see.

Solution #2

Probably a better way to handle this would be to set the post_status of the post to publish so that the post_name column gets the slug value and then immediately put the post back into a draft status.

Doing that manually would suck so here’s some code:

function allow_draft_public_preview( ) {

        global $post;

        if ( !empty($post->post_name) )
            return;

        $html =<<<DOC
        <div class="misc-pub-section">
            <label>
                <input type="checkbox" id="_allow_public_preview" name="_allow_public_preview" value="1" />
                Allow public preview
            </label>
        </div>
DOC;

echo $html;

}

add_action('post_submitbox_misc_actions', 'allow_draft_public_preview');

function update_post_status( $post_id, $post, $update ) {

    if ( !empty($_POST['_allow_public_preview']) ) {
        
        //un-hook to prevent infinite loop
        remove_action( 'save_post', 'update_post_status', 13, 2 );

        //set the post to publish so it gets the slug is saved to post_name
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );

        //immediately put it back to draft status
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) );

        //re-hool
        add_action( 'save_post', 'update_post_status', 13, 2 );
    }

}

add_action('save_post', 'update_post_status', 13, 2);

The first callback attached to the post_submitbox_misc_actions action adds a checkbox to the Post edit screen “Allow public preview”…

enter image description here

…when you mark this checkbox it will be recognized by the second callback fired on the save_post action.

In the save_post action we check for the existence of our checkbox in the $_POST superglobal and if it’s present, we set the post status to publish then immediately set it back to draft.

Now you can access draft posts, logged in or logged out, by their slugs/pretty permalinks.

All together now:

function preview_draft_posts($query) {

  if ( is_admin() || get_query_var('suppress_filters') )
    return $query;


  $query->set('post_status', array('publish', 'pending', 'draft'));


  return $query;

}

add_filter('pre_get_posts', 'preview_draft_posts');

function the_posts_preview_draft_posts($posts, $wp_query) {

    //abort if $posts is not empty, this query ain't for us...
    if ( count($posts) ) {
        return $posts;
    }

    $p = get_query_var('p');

    //get our post instead and return it as the result...
    if ( !empty($p) ) {
        return array(get_post($p));
    }
}

add_filter('the_posts', 'the_posts_preview_draft_posts', 10, 2);

function allow_draft_public_preview( ) {

        global $post;

        if ( !empty($post->post_name) )
            return;

        $html =<<<DOC
        <div class="misc-pub-section">
            <label>
                <input type="checkbox" id="_allow_public_preview" name="_allow_public_preview" value="1" />
                Allow public preview
            </label>
        </div>
DOC;

echo $html;

}

add_action('post_submitbox_misc_actions', 'allow_draft_public_preview');

function update_post_status( $post_id, $post, $update ) {

    if ( !empty($_POST['_allow_public_preview']) ) {
        
        //un-hook to prevent infinite loop
        remove_action( 'save_post', 'update_post_status', 13, 2 );

        //set the post to publish so it gets the slug is saved to post_name
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );

        //immediately put it back to draft status
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) );

        //re-hool
        add_action( 'save_post', 'update_post_status', 13, 2 );
    }

}

add_action('save_post', 'update_post_status', 13, 2);

Leave a Comment