Sync Posts and Pages from Staging to Live Site

We all know about the export and import thing – when you move from staging to live the entire site at once. But from time to time I keep getting requests from clients about syncing only specific posts or pages (or custom post types – doesn’t matter).

In this guide, I am going to talk about two ways how we can achieve that – programmatically from scratch and with a WordPress plugin.

Let’s begin!

Syncing All Posts and Pages from Staging to Live Site Programmatically

Let’s start with a programmatic way which basically comes down to creating a simple WordPress plugin and using an action hook – save_post. Not a very complicated way at all unless you need to copy images in your posts from the staging to the live site as well.

Anyway, if you’re not very comfortable with coding or it seems quite complicated for you, you can always jump down to the plugin approach below.

Creating a simple WordPress plugin

Soon in this tutorial, I am going to provide you with a bunch of the code snippets and we need to put them somewhere, right?

If you know what you’re doing, you can skip this specific step and add the code wherever you want. But if you don’t, or aren’t sure, I recommend organizing all the code into a single custom WordPress plugin.

Let’s do it:

<?php
/*
 * Plugin name: Sync Posts and Pages from Staging to Live
 * Description: The plugin allows to push posts from the standing site to the live site
 * Version: 1.0
 * Author: Misha Rudrastyh
 * Author URI: https://rudrastyh.com
 */

// the following code snippets will go here

For example, let’s just create an empty PHP file, rudr-sync-staging-to-live.php, put it into the wp-content/plugins folder and here we go.

save_post action hook

Our goal right now is when a post or page gets updated on the staging site to push the changes to the live site. The most obvious way to do it – with the standard WordPress action hook – save_post and a simple REST API request.

Let’s do it:

add_action( 'save_post', function( $post_id, $post ) {
	
	// skip auto-saves
	if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
		return;
	}
	
	// check whether this post or page is allowed to be
	if( ! in_array( $post->post_type, array( 'post', 'page' ) ) ) {
		return;
	}
	
	// you can also check user capabilities here, for example
	
	// authentication data
	$url = ''; // URL of the live site
	$login = ''; // username of a user on the live site
	$pwd = ''; // an application password (not normal password)
	
	// let's organize post (or page) data
	$post_data = array(
		'title' => $post->post_title,
		'status' => $post->post_status,
		'date' => $post->post_date,
		'slug' => $post->post_name,
		'menu_order' => $post->menu_order,
		// don't forget to replace URLs in the post content
		'content' => str_replace( 
			site_url(), // current (stanging) site URLs
			$url, // production site URLs
			$post->post_content
		),
		// some more parameters can go here, like 'meta' or 'parent'
	);
	
	// depending on a post type we use an appropriate REST API endpoint
	$rest_base = 'page' === $post->post_type ? 'pages' : 'posts';
	$endpoint = "{$url}/wp-json/wp/v2/{$rest_base}";
	$method = 'POST';
	
	wp_remote_request(
		$endpoint,
		array(
			'method' => $method,
			'headers' => array(
				'Authorization' => 'Basic ' . base64_encode( "$login:$pwd" )
			),
			'body' => $post_data
		)
	);
	
}, 25, 2 );

A couple of moments to keep in mind related to the code above:

  • Of course, the code is super-simplified, I completely skipped the part where we need to update custom fields, taxonomies, and post featured images. About updating custom fields via the REST API you can read here, about featured images – here.
  • It is not exactly necessary to use the save_post action hook, instead you can use both save_post_post and save_post_page hooks and remove the condition (line 18).
  • Don’t forget that you need to create an application password on the target, live, site and provide the credentials (lines 25-27).

Right now, it is time to activate our brand new WordPress plugin on the staging site and to check whether the syncing from the staging to the live site is working as expected.

How to check if a post has already been pushed to the live site?

However, there is one more thing – maybe new posts are now created correctly, but what about pushing post updates to already created posts?

Somehow our function needs to understand whether a specific post is already published on the production site or not. If yes – it just needs to update it, otherwise – create a new post.

Right now, let’s just update a specific part of the code snippet above:

$synced_id = is_synced_to_live_site( $post, $url );
if( $synced_id ) {
	$endpoint = "{$url}/wp-json/wp/v2/{$rest_base}/{$synced_id}";
	$method = 'PUT';
} else {
	$endpoint = "{$url}/wp-json/wp/v2/{$rest_base}";
	$method = 'POST';
}

As you can see, I used a new function – is_synced_to_live_site(), it should check whether a post or page already exists on the live site, but at this moment this function is undefined.

Let’s create it.

function is_synced_to_live_site( $post, $url ) {
	// depending on a post type we use an appropriate REST API endpoint
	$rest_base = 'page' === $post->post_type ? 'pages' : 'posts';
	
	// now let's send a request to the live site and check whether a post exist there
	$response = wp_remote_get(
		add_query_arg(
			array( 
				'slug' => $post->post_name, // we can do it by its slug
			),
			"{$url}/wp-json/wp/v2/{$rest_base}"
		)
	);
	
	if( 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return 0;	
	}

	$posts = json_decode( wp_remote_retrieve_body( $response ), true );
	if( ! $posts ) {
		return 0;
	}
	
	$post = reset( $posts );
	return $post[ 'id' ];
	
}

I am not using any authentication in this function because we’re getting published posts. If you need to get drafts or scheduled posts, you will need to provide a username and an application password to the request.

Copying post attachments

Probably, I will not provide you with the complete code here, but at least I can give you a couple of recommendations.

First of all, we will need to decide, what specific attachments you’d like to copy from the staging to the live site:

  • images, added to the post content,
  • images in specific custom fields,
  • images, attached to a post.

No matter which way you choose (or maybe you need all of them), your goal is to identify the image IDs and then copy them to the live site, I described in detail how to do that here.

For example, if you need to get image IDs from the post content, you will need to use either a regular expression or the DOMDocument class, something like this:

$document = new DOMDocument();
$document->loadHTML( $post->post_content );
$xml = simplexml_import_dom( $document );
$images = $xml->xpath( '//img' );

If you need to get attached images, it is much easier, just use the get_posts() function:

$attachment_ids = get_posts(
	array(
		'post_parent' => $post_id, // our initial post ID
		'post_type'   => 'attachment',
		'numberposts' => -1,
		'post_status' => 'any',
		'fields'      => 'ids',
	)
);

Syncing Posts from Staging to Live With a Plugin

You can also easily sync your WordPress posts and pages from the staging to the live site with my plugin, Simple WP Crossposting. This method should be great if you don’t want to deal with the code and just want an “out of the box” solution.

In this chapter of this tutorial, I would like to show you step-by-step how to configure my plugin for this specific purpose (syncing from staging to live). The whole process is quite simple, however, it is better to keep in mind some specific moments.

Step 1. Install the plugin on the staging site and add the live site in the settings

Do you need to have the plugin installed on both staging and live sites? – No, you do not. Of course, unless you don’t want to perform the two-way sync (both from stanging to live and from live to staging).

So, let’s start by installing and activating the plugin on your staging site, after that you need to visit the settings page Settings > Crosspost and then switch to the “Sites” tab.

Connect staging WordPress site to live in order to sync posts and pages between them
An application password is required to establish the connection between staging and live sites, you can read more about where to get it in this guide.

Step 2. Activate “Auto Mode” (optional)

Since we currently have the plugin settings page opened, let’s do a little bit more configuration right now. First of all, let’s switch back to the “General” tab and talk a little bit about the “Auto Mode” option.

By default, my plugin is intended to allow you to sync posts and pages to multiple WordPress websites, not only from a staging site to a live site. So, when editing a WordPress post or a page, you may notice a specific section in the editor where you can choose some specific sites (from the added ones in the “Sites” tab”) where you’d like to publish the post to:

Publish posta or pages on multiple sites
For each post (or page) we can decide on which site to publish it.

But since we only have one target site – a production version of the website, then maybe you don’t need this section in the post editor at all? So all the posts and pages are going to be published to the production site automatically.

automatically sync posts and pages from a staging to a live WordPress site.
You can find this setting on the “General” tab, at the very bottom.

Step 3. Turn on the “Copy attachments” option

Ok, now posts and pages from our staging site are going to be automatically published and then synced with the live site. But there are still some moments left to consider.

The plugin only copies the featured images, product images (if you’re using WooCommerce), and images from custom fields to the target site. The plugin doesn’t touch the images in the content and just uses hotlinks. It definitely makes sense in terms of performance when both of your sites are live websites. But in our case, our source site is a staging site, and more than that, it may even be closed with the HTTP authentication. So, no doubt, we need to copy all the images to the target site here.

It can be done quite easily – just visit the settings page once again, this time we need the “Fields” tab, scroll down a little bit, and activate the “Copy attachments” option:

Copy attachments from staging to live WordPress site.
You can find this setting on the “Fields” tab.

What is important to remember here is that the images in posts must be attached to those posts. For example, if you open the media library from the post and select in the media filter the “Uploaded to this post” option, you will see all the images attached to this post:

Images attached to a WordPress post

However, this filter option is only available in the classic editor, so you won’t find it in the block editor (Gutenberg) yet. Here are a couple of my recommendations that may help you:

  • Every image (or file) that is uploaded when editing a specific post is automatically attached to this post.
  • You can temporarily activate the “Classic Editor” plugin to use the filter I showed you in the screenshot above.
  • In the Media > Library page, when switched to a “list” view, you can find the “Uploaded to” column which indicates to which post an image is attached. It also allows you to manually attach an image.

Here it is:

WordPress media library list view

Step 4. Install a code snippet to replace the URLs in the content

The last but probably the most important step is to replace all URLs in the post content that point to the staging site.

By default, my plugin doesn’t do that, because as I already mentioned before, there is no need to do that for two live websites. However, maybe soon there is going to be an option, some kind of checkbox “Replace source site URL in the content with the target site URL”, but right now we need to use a simple code snippet for that purpose:

add_filter( 'rudr_swc_pre_crosspost_post_data', function( $post_data, $blog ) {
	
	$post_data[ 'content' ] = str_replace( 
		site_url(), // current (stanging) site URLs
		$blog[ 'url' ], // production site URLs
		$post_data[ 'content' ] // in the post content
	);
	return $post_data;
	
}, 10, 2 );

Some notes:

  • If you don’t know what to do with this snippet, don’t get upset, here is a detailed guide,
  • If you’re using WooCommerce or if you’d like to replace URLs in some custom fields as well, I’d recommend you check this support guide.

If you have any questions, you’re always welcome in the comments.

On this page, you can read more about and get my Simple WP Crossposting plugin.

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 15 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

Follow me on X