It may happen that you need to distribute a plugin and its updates, but it is not part of the official WordPress plugin directory. No problem, the quantity of code required is very, very small.
First, your plugin must declare the Update URI, it’s the most recent way to hook into the update procedure (once upon a time, we filtered some transients…).
In your plugin’s main file, add this header keyword:
/*
Plugin Name: My Plugin
...
Update URI: my-plugin
*/
The URI must be unique, and it can be a URL, anyway, WP won’t call it. Now WP will trigger a filter so you can provide update details for your plugin.
add_filter('update_plugins_my-plugin', function ($update, $plugin_data, $plugin_file, $locales) {
...
return $update;
}
Now we need to build the returned $update object. We assume to have somewhere online the information and the assets we need. For example, under the https://example.com/my-plugin/… address.
What do we need? At least a zip file with the new version and the new version number. You can choose the approach that best fits you. For this example, we assume the new version is stored in the file plugin.json at the address https://example.com/my-plugin/plugin.json as:
{
"version": "1.2.3"
}
So our filter can be written as (without caching and error checking)
add_filter('update_plugins_my-plugin', function ($update, $plugin_data, $plugin_file, $locales) {
$response = wp_remote_get('https://www.satollo.net/repo/dispatcher/plugin.json');
$data = json_decode(wp_remote_retrieve_body($response));
$update = [
'version' => $data->version,
'slug' => 'my-plugin',
'url' => 'https://example.com/my-plugin-public-page',
'package' => 'https://example.com/my-plugin/plugin-zip',
];
return $update;
}
Pay attention to the plugin slug, and the zip package must contain a folder named my-plugin with the plugin files. If you dump the $plugin_data variable, you can find a lot of information, and you may simplify the code.
I generate the zip file, the JSON file, and upload them to the server using a phing task.
You may want to add more information, like an icon. Change the $update array as:
add_filter('update_plugins_my-plugin', function ($update, $plugin_data, $plugin_file, $locales) {
$response = wp_remote_get('https://www.satollo.net/repo/dispatcher/plugin.json');
$data = json_decode(wp_remote_retrieve_body($response));
$update = [
'version' => $data->version,
'slug' => 'my-plugin',
'url' => 'https://example.com/my-plugin-public-page',
'package' => 'https://example.com/my-plugin/plugin.zip',
'banners' => [
'low' => 'https://example.com/my-plugin/banner.png',
'high' => 'https://example.com/my-plugin/banner.png'
],
'icons' => [
'1x' => 'https://example.com/my-plugin/icon.png',
'2x' => 'https://example.com/my-plugin/icon.png'
]
];
return $update;
}
For the banner and icon sizes, you can see this page.
The plugin’s details pop-up
If you want the link “plugin details” to open a pop-up with information about the plugin, like what happens for the regular plugin, you can add another little piece of code.
add_filter('plugins_api', function ($res, $action, $args) {
if ($action !== 'plugin_information' || $args->slug !== 'my-plugin') {
return $res;
}
$res = new stdClass();
$res->name = 'My Plugin';
$res->slug = 'my-plugin';
$res->version = '1.2.3';
$res->author = '<a href="https://example">Author</a>';
$res->homepage = 'https://example.com/my-plugin-public-page';
$res->download_link = 'https://example.com/my-plugin/plugin.zip';
$res->sections = array(
'description' => '...',
'changelog' => '...'
);
$res->banners = [
'low' => 'https://example.com/my-plugin/banner.png',
'high' => 'https://example.com/my-plugin/banner.png'
];
$res->icons = [
'1x' => 'https://example.com/my-plugin/icon.png',
'2x' => 'https://example.com/my-plugin/icon.png'
];
return $res;
}, 20, 3);
I dynamically load the description and the changelog from my repo: you can set them into the plugin.json or on separated files. Or just hardcode in the snippet above.
When to load those snippets
Since I’m a fan of resource optimization, there is no need to load those snippets every time WP is active. You can load them conditionally:
if (is_admin() || defined('DOING_CRON') && DOING_CRON) {
require_once __DIR__ . '/includes/repo.php';
}
where repo.php is a file containing the code in the plugin subfolder includes. It could be even more optimized by loading it when there are Ajax calls (for the plugin details) and on specific admin pages.
That’s all. Or you can use one of the many powerful plugins that integrate with GitHub or create a real repository on your WP site.
