Share a Draft With a Secret Link
Adds a "Share draft" link under unpublished posts that creates a secret preview address, so a client or reviewer can read the draft without a WordPress account. Links expire after a set number of days.
Opens WPIDE on
Code
// Days a shared link stays valid.
$wpide_share_days = 7;
add_filter( 'post_row_actions', function ( $actions, $post ) {
if ( ! in_array( $post->post_status, array( 'draft', 'pending', 'future' ), true ) || ! current_user_can( 'edit_post', $post->ID ) ) {
return $actions;
}
$token = get_post_meta( $post->ID, '_wpide_share_token', true );
$expires = (int) get_post_meta( $post->ID, '_wpide_share_expires', true );
if ( $token && $expires > time() ) {
$actions['wpide_share'] = sprintf(
'<a href="%s" target="_blank" rel="noopener">Shared link</a> | <a href="%s">Stop sharing</a>',
esc_url( add_query_arg( array( 'p' => $post->ID, 'wpide_share' => $token ), home_url( '/' ) ) ),
esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpide_share_draft&stop=1&post=' . $post->ID ), 'wpide_share_' . $post->ID ) )
);
} else {
$actions['wpide_share'] = sprintf( '<a href="%s">Share draft</a>', esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=wpide_share_draft&post=' . $post->ID ), 'wpide_share_' . $post->ID ) ) );
}
return $actions;
}, 10, 2 );
add_action( 'admin_post_wpide_share_draft', function () use ( $wpide_share_days ) {
$post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
check_admin_referer( 'wpide_share_' . $post_id );
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( 'Not allowed.', 403 );
}
if ( ! empty( $_GET['stop'] ) ) {
delete_post_meta( $post_id, '_wpide_share_token' );
delete_post_meta( $post_id, '_wpide_share_expires' );
} else {
update_post_meta( $post_id, '_wpide_share_token', wp_generate_password( 24, false ) );
update_post_meta( $post_id, '_wpide_share_expires', time() + $wpide_share_days * DAY_IN_SECONDS );
}
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'edit.php' ) );
exit;
} );
// Let visitors with a valid token see the draft.
add_filter( 'posts_results', function ( $posts, $query ) {
if ( is_admin() || empty( $_GET['wpide_share'] ) || 1 !== count( $posts ) || ( ! $query->is_main_query() && ! $query->get( 'p' ) ) ) {
return $posts;
}
$post = $posts[0];
$token = (string) get_post_meta( $post->ID, '_wpide_share_token', true );
$expires = (int) get_post_meta( $post->ID, '_wpide_share_expires', true );
if ( '' !== $token && $expires > time() && hash_equals( $token, sanitize_text_field( wp_unslash( $_GET['wpide_share'] ) ) ) && in_array( $post->post_status, array( 'draft', 'pending', 'future' ), true ) ) {
$post->post_status = 'publish'; // In memory only, for this request.
add_filter( 'wp_robots', 'wp_robots_no_robots' );
nocache_headers();
}
return $posts;
}, 10, 2 );
// Drafts are not public, so the main query needs to be allowed to find them first.
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() && $query->is_main_query() && ! empty( $_GET['wpide_share'] ) && $query->get( 'p' ) ) {
$query->set( 'post_status', array( 'publish', 'draft', 'pending', 'future' ) );
}
} );
add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
if ( 'publish' === $new_status ) {
delete_post_meta( $post->ID, '_wpide_share_token' );
delete_post_meta( $post->ID, '_wpide_share_expires' );
}
}, 10, 3 );
Notes
Change $wpide_share_days; publishing the post or clicking "Stop sharing" disables the link. Shared drafts are marked noindex.
How to add this snippet
- Install WPIDE on your WordPress site (it is free) and open Code Snippets.
- Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
- It is added inactive: review the code and settings, then switch it on.