Copy Pages Between Sites in Multisite Network
Let me show you an easy way to duplicate WordPress pages between any of your sites within a multisite network.
Or to be more specific, I am about to show you two ways how you can do that:
- The first one – we’re going to it programmatically,
- The second one – we’re going to use my WordPress plugin to achieve the same result,
Either way, by the end of this tutorial, we will have brand-new bulk actions on the “All Pages” admin page. However, each page can also be copied individually to a specific sub-site in case you decide to go for the plugin approach.

By the way, if you’re not using a WordPress Multisite network, check out this article.
Copy or Move Pages Between Sites in a Multisite Network Programmatically
Let’s start with the programmatic approach. For your convenience, I decided to split this chapter into three parts.
1. Creating a bulk action
In this part, we’re just going to create bulk actions for every site in a multisite network. To achieve that, I am going to use the get_sites() WordPress function. By the way, don’t forget to check out my other in-depth tutorial about bulk actions in WordPress.
// add bulk actions
add_filter( 'bulk_actions-edit-page', 'rudr_my_bulk_multisite_actions' );
function rudr_my_bulk_multisite_actions( $bulk_array ) {
$sites = get_sites(
array(
// 'site__in' => array( 1, 2, 3 )
'site__not_in' => get_current_blog_id(), // exclude the current blog
'number' => 50,
)
);
if( $sites ) {
foreach( $sites as $site ) {
$bulk_array[ "move_to_{$site->blog_id}" ] = "Move to {$site->blogname}";
}
}
return $bulk_array;
}What is actually important to keep in mind here:
- If you’re going to use it for other WordPress post types, not for Pages, then you need either to change or duplicate the first line and provide a post type name as a part of the hook name, for example:
add_filter( 'bulk_actions-edit-{POST TYPE}' ... - Definitely, we need to exclude the current site from the bulk actions; it can be easily done with the help of the
site__not_inparameter. We don’t want to duplicate pages on the same subsite here, right? - You can also notice that we passed the maximum of 50 sites to get by the
get_sites()function. The thing is that WordPress Multisite networks support an unlimited number of sites, so if you have 1000+ sites in your Network, you’d better think about another implementation of this step. I can give you two hints, though – maybe you do not even need all the sites. In that case, thesite_inparameter will help you, or, if you need all of them, you could try to replace the default bulk action element with Select2 with AJAX search.
2. Copying every selected page to another site in a multisite network
In this part, we’re going to make our brand-new bulk action to do the thing. I mean, actually, to copy pages between sites in our multisite network.
For your convenience, I decided to create a separate PHP function rudr_copy_page_to_site():
/**
* WordPress Multisite: Copy or Move Pages between Sites
*
* @author Misha Rudrastyh
* @link https://rudrastyh.com/wordpress-multisite/copy-pages-between-sites.html
*/
function rudr_copy_page_to_site( $post_id, $blog_id ) {
// first of all we need to get some page information from the current site
// get the original WP_Post object as an array
$post = get_post( $post_id, ARRAY_A );
if( ! $post ) {
return;
}
// get all the page meta
$post_meta = get_post_custom( $post_id );
// empty ID field, to tell WordPress to create a new post, not update an existing one
$post[ 'ID' ] = '';
// once we collected all the page information, we are ready to switch to another blog and create a page copy there
switch_to_blog( $blog_id );
// insert the page
$inserted_post_id = wp_insert_post( $post ); // insert the post
// add page meta
foreach( $post_meta as $key => $values) {
// if you do not want weird redirects
if( '_wp_old_slug' === $key ) {
continue;
}
foreach( $values as $value ) {
add_post_meta( $inserted_post_id, $key, $value );
}
}
restore_current_blog();
// if you want just to copy pages, comment this line
wp_delete_post( $post_id );
}In this function:
- It will work out great for regular WordPress posts or even custom post types. The only thing you need to remember is that I didn’t do anything with post taxonomies here, because by default, pages don’t have them.
- Also, it is worth checking whether a page already exists on another site; you can use the
get_page_by_path()function for that purpose, for example. - I definitely excluded
_wp_old_slugmeta key when copying page custom fields, but consider that there could be more system custom fields worth excluding, for example_edit_lock. - Though everything in this function seems to be working well, it is very simplified and can not work in complex cases, for example, when you have images in your ACF fields or using Elementor for pages. In that case, I recommend the plugin approach.
Also, we need to connect this PHP function to a bulk action trigger.
add_filter( 'handle_bulk_actions-edit-page', 'rudr_bulk_action_multisite_handler', 10, 3 );
function rudr_bulk_action_multisite_handler( $redirect, $doaction, $object_ids ) {
// we need query args to display correct admin notices
$redirect = remove_query_arg( array( 'misha_posts_moved', 'misha_blogid' ), $redirect );
// our actions begin with "move_to_", so let's check if it is a target action
if( strpos( $doaction, 'move_to_' ) === 0 ) {
$blog_id = str_replace( 'move_to_', '', $doaction ); // get blog ID from action name
foreach ( $object_ids as $post_id ) {
// that's our custom function
rudr_copy_page_to_site( $post_id, $blog_id );
}
$redirect = add_query_arg( array(
'misha_posts_moved' => count( $object_ids ),
'misha_blogid' => $blog_id
), $redirect );
}
return $redirect;
}3. Admin notices
Last but not least, once pages are copied between sites, an appropriate admin notice should be displayed. We can do it with the admin_notices action hook. There is also another one, which is network_admin_notice, but we don’t need it because it is actually intended for network admin pages like “Sites”, “Users”, etc.
// show an appropriate notice
add_action( 'admin_notices', 'rudr_bulk_multisite_notices' );
function rudr_bulk_multisite_notices() {
if( ! empty( $_REQUEST[ 'misha_posts_moved' ] ) ) {
// because I want to add blog names to notices
$blog = get_blog_details( $_REQUEST[ 'misha_blogid' ] );
// depending on ho much posts were changed, make the message different
echo '<div class="updated notice is-dismissible"><p>';
printf(
_n(
'%d post has been moved to "%s".',
'%d posts have been moved to "%s".',
$_REQUEST[ 'misha_posts_moved' ]
),
$_REQUEST[ 'misha_posts_moved' ],
$blog->blogname
);
echo '</p></div>';
}
}Copying Pages Between Sites with a Plugin (Recommended)
In this part, we’ll talk about my Simple Multisite Crossposting plugin.
Let’s say you tried to use the code mentioned above, but what if:
- You don’t want to copy posts or pages between sites using only bulk action, and you want to do it when actually editing a post or page,
- You want to copy featured images or images in custom fields (ACF, for example),
- You want categories, tags, or custom taxonomy terms to be added to a copied version of a post,
- You’re using Elementor or another page builder.
Well, that’s when my plugin comes in 😁 It allows you to implement everything mentioned above and much more, actually.
You can still use bulk actions:

Or you can select specific sites directly when editing a page in a classic or block editor:

If you have any questions, guys, either about the plugin or about the code, please let me know in the comments section below.
Misha Rudrastyh
Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.
Need some developer help? Contact me
Hi Misha, excellent work, thank you so much for sharing!
Hi Jakub,
Always welcome! 🙂
Hi Misha again,
could you help me include a post thumbnail, please? I added a snippet
$thumbnail = get_post_thumbnail_id($post_id);and then
set_post_thumbnail($inserted_post_id, $thumbnail);but the thumbnail wouldn’t copy.
thanks in advance!
The thing is here is that when you’re using
set_post_thumbnail()you need to provide an attachment ID which exists on that specific website.So, you need to either copy an attachment there manually, or, for example, use a network media library plugin.
Hi, well done, thanks for sharing.
Just a question, i only need to show on the “main blog” a preview of the posts written on the “satellite blogs”, something like title, excerpt, thumb and href to the original post; i don’t need to get all the post.
It’s possible?
Thanks
Enrico
Hello,
This method only moves the post to the other site.
How to copy/duplicate a post?
Thank you.
Hello Selvi,
Just remove/comment line 39 in the code
Hello,
When copying a post, all custom fields are also copied. Is it possible to ignore e.g. fields such as “music_post”, “video_post” (this is meta key)?
Hello Arthur,
Just add more conditions:
Hi Misha,
Thanks for the wonderful code! Is it possible to configure it to work for pages as well as posts?
Anthony
Hi Anthony,
Yes, it is possible. I decided to update my code for Pages.
How to include its featured image?
You might check this tutorial for the ready code for copying attachments. Once you do that, you can use a new attachment ID as
_thumbnail_idkey of the post.Or as an option you can use my plugin that covers this by default.
Hi Misha,
Great work.
I have a question:
Is it possible to ignore comments once the post has been duplicated?
Please let me know
Thanks
Hi Tommaso,
The comments should be ignored by default. By I think you’re talking about comment count, in this case just add the line
$post[ 'comment_count' ] = 0;This was a great script – thanks for sharing. However – it totally broke all my pages that was created in Elementor. But for everything else it was awesome.
For Elementor we need to additionally regenerate its CSS files created for this page. Plus, there are custom fields like
_elementor_datawhich may require some replacements during the copying process.Hey, this script is exactly the sort of thing I was looking for and seems to work well. I was wondering how it could be edited to add to all blogs except the current one, rather than adding to one blog each time.
Thanks!
Hey, amazing!
Easy enough –
get_sites()andforeach()(I have added it to the code).