How to Handle Theme Updates from your Custom Server
My tutorial about configuring self-hosted plugin updates became quite popular so I decided to publish a similar tutorial related to WordPress themes.
The whole tutorial consists of two steps – in the first step we are going to configure our custom update server, in the second step – create a theme actually.
Okay, let’s dive into it.
1. Configuring Update Server
What is an update server actually?
In reality it is much simpler that may sound. All we need to do is to create a couple files on your hosting, and that would be enough!
In the most of simplicity it could look like this:

I think it is needless to say what is misha-theme.zip
, the more interesting file for us is info.json
.
The content of info.json
file could be completely custom. But quite obviously it should contain the URL the theme archive, and the version of the fresh theme. And let’s add the URL of the page with theme update information.
{
"version": "2.0",
"details_url": "https://rudrastyh.com/themes/misha-theme/changelog.html",
"download_url": "https://rudrastyh.com/themes/misha-theme/2.0.zip"
}
Bonus. License check
I’ve just showed you a static info.json
file that already has everything we need but what if you would like to check if this particular update actually can be installed? For example, if a theme update license is expired.
In this case your info.json
file should be transformed into info.php
file.
$update = array(
'version' => '2.0',
'details_url' => 'https://rudrastyh.com/themes/misha-theme/changelog.html',
'download_url' => ''
);
if( ! empty( $_GET[ 'license_key' ] ) && license_check_logic( $_GET[ 'license_key' ] ) {
$update[ 'download_url' ] = 'https://rudrastyh.com/themes/misha-theme/2.0.zip';
}
header( 'Content-Type: application/json' );
echo json_encode( $update );
license_check_logic()
is a custom function here that allows to check if the license key $_GET[ 'license_key' ]
provided is correct and not expired. A little bit more info is in this tutorial.

Need some help implementing this custom logic? Me and my team are ready to help.
2. Get the Update Information and Do the Update from your Custom Server
This part is even simpler than the same part in the plugin updater. It is enough to use site_transient_update_themes
filter hook here.
Please keep in mind, that I don’t cover performance in this chapter. We will talk about performance later.
add_filter( 'site_transient_update_themes', 'misha_update_themes' );
function misha_update_themes( $transient ) {
// let's get the theme directory name
// it will be "misha-theme"
$stylesheet = get_template();
// now let's get the theme version
// but maybe it is better to hardcode it in a constant
$theme = wp_get_theme();
$version = $theme->get( 'Version' );
// connect to a remote server where the update information is stored
$remote = wp_remote_get(
'https://rudrastyh.com/wp-content/uploads/theme-updater/info.json',
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json'
)
)
);
// do nothing if errors
if(
is_wp_error( $remote )
|| 200 !== wp_remote_retrieve_response_code( $remote )
|| empty( wp_remote_retrieve_body( $remote ) )
) {
return $transient;
}
// encode the response body
$remote = json_decode( wp_remote_retrieve_body( $remote ) );
if( ! $remote ) {
return $transient; // who knows, maybe JSON is not valid
}
$data = array(
'theme' => $stylesheet,
'url' => $remote->details_url,
'requires' => $remote->requires,
'requires_php' => $remote->requires_php,
'new_version' => $remote->version,
'package' => $remote->download_url,
);
// check all the versions now
if(
$remote
&& version_compare( $version, $remote->version, '<' )
&& version_compare( $remote->requires, get_bloginfo( 'version' ), '<' )
&& version_compare( $remote->requires_php, PHP_VERSION, '<' )
) {
$transient->response[ $stylesheet ] = $data;
} else {
$transient->no_update[ $stylesheet ] = $data;
}
return $transient;
}
That already will be enough for your custom theme to receive updates from your custom server.

But, you may notice that all the admin pages started to load slower. Let’s talk about it in the next chapter.
Performance
The thing is that filter hook site_transient_update_themes
is running on every admin page and multiple times.
What can we do? Below is my own solution for that. Maybe it is not perfect, but I am open to your suggestions guys. I decided to use transient cache and also I used the current plugin version as a part of the cache key.
if( false == $remote = get_transient( 'misha-theme-update'.$version ) ) {
// connect to a remote server where the update information is stored
$remote = wp_remote_get(
'https://rudrastyh.com/wp-content/uploads/theme-updater/info.json',
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json'
)
)
);
// do nothing if errors
if(
is_wp_error( $remote )
|| 200 !== wp_remote_retrieve_response_code( $remote )
|| empty( wp_remote_retrieve_body( $remote ) )
) {
return $transient;
}
$remote = json_decode( wp_remote_retrieve_body( $remote ) );
if( ! $remote ) {
return $transient; // who knows, maybe JSON is not valid
}
set_transient( 'misha-theme-update'.$version, $remote, HOUR_IN_SECONDS );
}

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
Useful content as always, thanks!
I have an idea of a post: How to restrict a product from being combined with others in the WooCommerce cart. :)
Use case: a single-per-order, virtual product that must be sold separately from physical products.
Thank you so much!
Ok, I will keep it in mind ;)
Great, currently trying to implement. But one question – does theme folder needs to be in ZIP as well as you mention in plugin?
Like misha-update-checker.zip => ./misha-update-checker/style.css
OR it can be misha-update-checker.zip => style.css
I will operate with first option, as it seems more logical. But it creates quite a few problems with my flow, so if it could be done with only files in ZIP and not folder + files, it would be better.
Perfect. Works like a charm. One thing a needed, because I use parent + theme child, to change
$theme = wp_get_theme();
to$theme = wp_get_theme( $stylesheet );
Awesome!
Relate to your first question – yes, the first option should work out great, the second one is likely to cause theme duplication during the update.
And thank you for the comment.
Amazing post!
Will defiantly be implementing this!
Thank you!