How to Duplicate Posts and Pages Without Plugins

It is very useful when you work with a lot of similar posts. So, when you copy a post, you don’t have to enter all metadata, taxonomies etc. each time manually.

#wp-admin  /    /   212

Duplicating posts is a very useful functionality when you work with a lot of similar posts (for example products in online shop). Especially if the posts have the same custom fields values, but the different post title and content.

It means that you do not have to re-enter custom fields, post tags and categories each time.

This is the example:

Duplicate post link in post row actions.

When you click the «Duplicate» link, the post will be cloned, but it won't be published, it will be saved as a draft and you will be redirected to the post edit admin page.

This is enough easy to do, so, look at the following code, insert it into your theme functions.php file (or into the another file if you know what to do).

/*
 * Function creates post duplicate as a draft and redirects then to the edit post screen
 */
function rd_duplicate_post_as_draft(){
	global $wpdb;
	if (! ( isset( $_GET['post']) || isset( $_POST['post'])  || ( isset($_REQUEST['action']) && 'rd_duplicate_post_as_draft' == $_REQUEST['action'] ) ) ) {
		wp_die('No post to duplicate has been supplied!');
	}
 
	/*
	 * Nonce verification
	 */
	if ( !isset( $_GET['duplicate_nonce'] ) || !wp_verify_nonce( $_GET['duplicate_nonce'], basename( __FILE__ ) ) )
		return;
 
	/*
	 * get the original post id
	 */
	$post_id = (isset($_GET['post']) ? absint( $_GET['post'] ) : absint( $_POST['post'] ) );
	/*
	 * and all the original post data then
	 */
	$post = get_post( $post_id );
 
	/*
	 * if you don't want current user to be the new post author,
	 * then change next couple of lines to this: $new_post_author = $post->post_author;
	 */
	$current_user = wp_get_current_user();
	$new_post_author = $current_user->ID;
 
	/*
	 * if post data exists, create the post duplicate
	 */
	if (isset( $post ) && $post != null) {
 
		/*
		 * new post data array
		 */
		$args = array(
			'comment_status' => $post->comment_status,
			'ping_status'    => $post->ping_status,
			'post_author'    => $new_post_author,
			'post_content'   => $post->post_content,
			'post_excerpt'   => $post->post_excerpt,
			'post_name'      => $post->post_name,
			'post_parent'    => $post->post_parent,
			'post_password'  => $post->post_password,
			'post_status'    => 'draft',
			'post_title'     => $post->post_title,
			'post_type'      => $post->post_type,
			'to_ping'        => $post->to_ping,
			'menu_order'     => $post->menu_order
		);
 
		/*
		 * insert the post by wp_insert_post() function
		 */
		$new_post_id = wp_insert_post( $args );
 
		/*
		 * get all current post terms ad set them to the new post draft
		 */
		$taxonomies = get_object_taxonomies($post->post_type); // returns array of taxonomy names for post type, ex array("category", "post_tag");
		foreach ($taxonomies as $taxonomy) {
			$post_terms = wp_get_object_terms($post_id, $taxonomy, array('fields' => 'slugs'));
			wp_set_object_terms($new_post_id, $post_terms, $taxonomy, false);
		}
 
		/*
		 * duplicate all post meta just in two SQL queries
		 */
		$post_meta_infos = $wpdb->get_results("SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=$post_id");
		if (count($post_meta_infos)!=0) {
			$sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) ";
			foreach ($post_meta_infos as $meta_info) {
				$meta_key = $meta_info->meta_key;
				if( $meta_key == '_wp_old_slug' ) continue;
				$meta_value = addslashes($meta_info->meta_value);
				$sql_query_sel[]= "SELECT $new_post_id, '$meta_key', '$meta_value'";
			}
			$sql_query.= implode(" UNION ALL ", $sql_query_sel);
			$wpdb->query($sql_query);
		}
 
 
		/*
		 * finally, redirect to the edit post screen for the new draft
		 */
		wp_redirect( admin_url( 'post.php?action=edit&post=' . $new_post_id ) );
		exit;
	} else {
		wp_die('Post creation failed, could not find original post: ' . $post_id);
	}
}
add_action( 'admin_action_rd_duplicate_post_as_draft', 'rd_duplicate_post_as_draft' );
 
/*
 * Add the duplicate link to action list for post_row_actions
 */
function rd_duplicate_post_link( $actions, $post ) {
	if (current_user_can('edit_posts')) {
		$actions['duplicate'] = '<a href="' . wp_nonce_url('admin.php?action=rd_duplicate_post_as_draft&post=' . $post->ID, basename(__FILE__), 'duplicate_nonce' ) . '" title="Duplicate this item" rel="permalink">Duplicate</a>';
	}
	return $actions;
}
 
add_filter( 'post_row_actions', 'rd_duplicate_post_link', 10, 2 );

But what if this code works only for posts, not for pages or any registered post types? Do not worry, all we need to do is to change the last filter to this:

add_filter('page_row_actions', 'rd_duplicate_post_link', 10, 2);

For custom post types view this example in comments.
You can use several filters at the same time, of course.

Misha Rudrastyh
About the author Misha Rudrastyh

Passionate about WordPress and snowboarding, creating websites for over 10 years! Let's work together — just contact me.

If you are a developer too, subscribe to my facebook page.

Comments 212

← Older
  • Wordpress Customization

    Great tutorial! This was so easy to follow and exactly what I was looking for. This is a beautiful tutorial. Good job and well explained.

    Thanks.

  • Great code, thanks!

    I would really like to have it work for my custom post types as well, but following the link in the last paragraph of the article didn’t get me to the comment in question. Could you please correct the link or maybe paste the code snippet again?

    • Never mind, just figured out how to customize the code you wrote to filter pages in a way that it works for my custom posts.

      Thank you anyway, great tutorial! :)

    • I’m glad you’ve figured it out :)

  • I’m curious about Custom Posts as well. No joy on the link. I’m firing up my green lampshade (I’m an old guy!) and putting on my programmer glasses in the meantime.

    • Greg Hollowell
       
      function rd_duplicate_post_link( $actions, $post ) {
      	if (current_user_can('edit_posts') && $post->post_type=='YOUR_CUSTOM_POST_TYPE') {
      		$actions['duplicate'] = '<a href="' . wp_nonce_url('admin.php?action=rd_duplicate_post_as_draft&post=' . $post->ID, basename(__FILE__), 'duplicate_nonce' ) . '" title="Duplicate this item" rel="permalink">Duplicate</a>';
      	}
      	return $actions;
      }
       
      add_filter( 'post_row_actions', 'rd_duplicate_post_link', 10, 2 );
      • Greg , thanks for sharing that bit – it got in me in a right direction. A have possible suggestion, I think it should be ‘||’ not “&&”.

        if (current_user_can('edit_posts') || $post->post_type=='YOUR_CUSTOM_POST_TYPE')
  • Misha, thank you for this. However, when I try to insert into my functions.php, it gives me a syntax error, unexpected ‘if’ (T_IF) on line 197 (which is line 13 of the code you posted). How to fix please and thank you?

  • Hi Misha,

    why do you use
    wp_get_object_terms($post_id, $taxonomy, array('fields' => 'slugs'))
    and not
    get_the_terms($post_id, $taxonomy)
    ?

    Thanks, Fred.

  • I just wanted to say a big thank you, I love your code, beutiful format and well commented.

    This was really helpfull, i would suggest though that you add in how to enable this across custom post types. As the #comment link does not work!

    It’s easy enough, but i think people will struggle if they don’t have the coding knowledge.

    • Hey Aaron,

      I tested it at least 3 times – works perfectly for custom post types. Have no idea what happens for those, who can not make it work for CPT.

      • Oh I see :)

        What makes this confusing is the final comment that’s suggesting that they need to make a change by following a comment in your closing statement.

        Again thanks for sharing this beautiful code, it works perfectly

  • Thank you so much, Misha. This is incredibly helpful!

  • THANK YOU! YOU’RE A LIFESAVER!

Comments are closed.
If you need my personal help, please contact me. Contact me