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!
Hi Misha,
Thanks for your post =)
I have a problem with my theme update :
Json –> OK
ZIP –> OK
Both accessible
I checked the transient content and the array is correctly set with right information.
I have the red dot icon on updates but it look like it’s reset on every page load.
And I don’t have any update notice on theme screen.
Do you have any ideas ?
Is the filter has changed since ?
Thanks your your help
Hey, I followed this guide but once I update the theme, my site crashes and I am getting the following error:
`Attempt to modify property “no_update” on bool in /var/www/website/wp-content/themes/vmc/functions.php:95`
the 95th line is `$transient->no_update[ $stylesheet ] = $data;`
Thanks for tutorial, it’s working perfectly.
I’m tested with file from https://github.com/rudrastyh/misha-theme-updater/blob/main/functions.php
But i got warning:
“Warning: Creating default object from empty value… on line 71”
I’m added new line before:
And warning has gone, it’s right way?
I think it would be better to check it it doesn’t exist first, like with
if( empty( $transient->response ) )
Hi,
I have problem with this code. In local server this code works fine, but in real website this code does not work. Problem is when you are clicking to update? wordpress gives message that theme is updated but in real it is not, theme version is same as previous. how I can fix this one, why in local server it works perfectly but in real website works show everything is ok and updated, but in real it is not?
and I have same problem in updating my plugins as well. WordPress shows it is updated, but it is not updated. if you refresh the page it is again sayas it is required to update your theme or plugin. it happens in real website, but in local works fine
Hi,
I have tested it right now (not on localhost) and seems like everything is working for me.
Thanks for tutorial, I am doing something similar now.
Maybe about performance.
I am thinking to use wp cron to run info.json and save values into database.
Then inside site_transient_update_themes filter check only these stored values.
It should help
Thanks again for your job
Hey, Ive got an error on
$transient->response[ $stylesheet ] = $data;
Attempt to modify property “response” on bool
What am i doing wrong?
Fixed my problem, don’t forget to add the “requires and requires_php” in the json file. otherwise it doesnt work.
Hi, I am facing similar issue, can you please share your fix here?
you ned to test with
if(!empty($transient->response)) OR if($transient != false)
Hello,
When using child theme, you can add this code to keep main theme version cheking
Hope this help
Thanks for this fantastic tutorial. I use a custom block theme, but have a problem with the folder renaming itself. Example: theme folder name is “block-theme” but after the update it provides a kind of hash, then it is called “block-theme-hzl6AS”. And then after another update it has a different name, for example “block-theme-RUs5f7h”.
How can I ensure that this name simply remains “block-theme” without values afterwards.
Hey Paul, it is described here (at the end of the chapter).