Updating without dot org
As you may have read, we’re not releasing our first WooCommerce plugin Shop Health on wordpress.org, even though the plugin is (and will forever be) completely free.
In this blogpost I’ll take you through the technical process of how we managed to make our plugin updateable in the regular WordPress fashion, but without any of the WordPress repositories interference.
Some research
When we pulled the plug on not releasing on dot org, we first started doing some research. Now we plan on releasing premium plugins in the future, so some of the groundwork had already been done and rather quickly we came up with three possible options:
- Use a service like Freemius
- Update the plugin through wooping.io
- Update Wooping Shop Health through GitHub.
Using a service like Freemius While we love what Vova and his team at Freemius have built, this platform was really created with freemium models in mind. So while it would be great for our premium plugins, it didn’t feel right to run free plugins through their service. Additionally, we were looking for a few extra technical features (like composer support) that simply weren’t present on the platform.
Updating through Wooping.io Hosting our own updates made some sense to us. It would alllow us full control over what we could offer our users and it would, eventually, be great to host premium plugins this way as well. That being said, if any of them would ever be massively successful, it would mean dealing with lots of bandwidth.
Updating Through Github Made a lot of sense for a free plugin. We wouldn’t be on the hook for the bandwidth and since the source code of the plugin was already hosted on Github, we’d be able to ship straight from there. There was one problem with this approach though; our git repository contains files for testing, compiling assets and bringing in various composer packages, so to ship all of this cruff with the main plugin wasn’t going to work well.
Want to optimize your WooCommerce store?
Wooping has launched it’s first plugin: Shop Health!
Shop Health is your go-to tool to analyse and optimize your WooCommerce webshop in a heartbeat!
The solution we landed on
Eventually we landed on a mix of hosting it on wooping.io and hosting it on Github. We decided that having Github deal with the bandwidth was a good thing, but that the rest should be in our hands. So we started looking into solutions for this. We found out that there are some great tools available if you decide to go this route, like Andy Fragen’s Github Updater plugin, which just requires you to add two simple lines to your plugins frontmatter:
* GitHub Plugin URI: https://github.com/MindblownHQ/wooping-shop-health
* Primary Branch: main
*/
That being said, this solution requires you to also have the Github Updater plugin installed and since we were making something aimed at shop owners, that solution simply wouldn’t fly.
We then stumbled upon this excellent PHP library by Yahnis Elsts called the Plugin Update Checker. It allows you to really quickly tie your custom code to a repository or a custom endpoint, and check for updates there (and despite the name, it also works on themes!). Since Shop Health is 100% compatible with composer, for us this was as simple as running composer require yahnis-elsts/plugin-update-checker
and adding the following code to our plugin:
PucFactory::buildUpdateChecker(
'https://github.com/MindblownHQ/wooping-shop-health',
\SHOP_HEALTH_FILE,
'shop-health'
);
This did exactly what we wanted! We added it into our plugin straight away. The only problem was that all of the files required to test the plugin, create the javascript and css assets and bring in all the PHP packages we use, were still being shipped. Enter Github Actions. A great way to clean up a github repository and serve it as a plugin to people. Marinus openly shared how he managed to pull this off, linking to our own code for anybody to copy, fork and play with.
Wooping.io
While serving updates straight from Github worked perfectly, it didn’t give us the full control we wanted. Since WordPress through dot org, brings in lots of meta data (like compatibility between versions, changelogs and reviews) that we couldn’t offer through Github. So eventually we ended up creating a REST endpoint on this website for each plugin. That REST endpoint just links to the latest release on Github with regards to the zip-file, but it allows us to inject all that good meta-data so that the experience for the end user is exactly the same as a dot org plugin:
/**
* Build a release json file for the plugin
*
* @param WP_REST_Request $request The request object (unused but required by interface)
* @return WP_REST_Response Response containing array of plugin update info
*/
public function updates( WP_REST_Request $request ): WP_REST_Response {
// Get the product by slug from the request
$post_id = get_page_by_path($request->get_param('slug'), OBJECT, 'product');
if ($post_id) {
$product = wc_get_product($post_id);
// Get the latest file from downloadable files from the product
$updateController = new UpdateController();
$download_url = $updateController->download_url($product->get_id());
return new WP_REST_Response([
'name' => $product->get_title(),
'version' => $product->get_meta('_github_version'),
'download_url' => $download_url,
'homepage' => $product->get_permalink(),
'requires' => $product->get_meta('_minimum_wp_version') ?: '5.0',
'tested' => $product->get_meta('_tested_wp_version') ?: '6.4',
'last_updated' => $product->get_date_modified()->date('Y-m-d H:i:s'),
'upgrade_notice' => $product->get_meta('_upgrade_notice')
]);
}
}
This REST endpoint will eventually also be able to handle license keys, so we can start serving our premium plugins out to people. That’s why the meta-data is tied to a WooCommerce product.
The beauty of the Plugin Update Checker code we’ve added is that you can also supply your own endpoint, so a simple change in our plugins code was enough:
PucFactory::buildUpdateChecker(
'https://wooping.io/wp-json/...etc',
\SHOP_HEALTH_FILE,
'shop-health'
);
Composer support
One of the things we where looking to achieve is get composer support right out of the box, ideally also with support for licenses later. For dot org plugins, there is wppackagist, which basically automatically turns your dot org hosted plugin into a composer package, but since dot org was out of the question, we needed to figure this out ourselves. Currently we run 3 simple endpoints on composer.wooping.io
that look like this:
/**
* Registers the REST API endpoints for retrieving plugin updates via composer
*
* @return void
*/
public function register_endpoints(): void {
// Anyone can check regular info
\register_rest_route( 'wooping/v1', '/composer', array(
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'composer' ],
'permission_callback' => '__return_true'
) );
// Anyone can check for packages.
\register_rest_route( 'wooping/v1', '/composer/packages.json', array(
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'packages' ],
'permission_callback' => '__return_true'
) );
// For downloads, we check license.
\register_rest_route( 'wooping/v1', '/composer/download/(?P<plugin>[a-zA-Z0-9-]+)/(?P<version>[0-9.]+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'download' ],
'permission_callback' => [ $this, 'check_permission' ],
) );
}
Now, obviously I’m not going to show you how the check_permission
function in this snippet is currently working. The first endpoint just sends out our global composer data. The second endpoint serves the possible packages, that one is also public. It’s just a composer-compatible list of packages including their versions.
In our implementation a WooCommerce product is tied to a plugin. It contains some metadata that we include in this second response. The download_url is dependent on wether or not it’s a free plugin in WooCommerce. If it’s free, we just serve the release url in Github and that’s that. The code in this example is not what we have running, but it’s close:
/**
* Get the available update.
*
* @param WP_REST_Request $request The request object (unused but required by interface)
* @return WP_REST_Response Response containing array of plugin update info
*/
public function packages( WP_REST_Request $request ): WP_REST_Response {
$args = array(
'post_type' => 'product',
'posts_per_page' => -1,
'meta_query' => array(
array(
'key' => '_github_url',
'compare' => 'EXISTS'
)
)
);
$products = get_posts($args);
$packages = [];
foreach ($products as $product) {
$slug = get_post_meta($product->ID, '_plugin_slug', true);
$version = get_post_meta($product->ID, '_plugin_version', true);
$download_url = get_post_meta( $product->ID, '_github_url', true);
$key = 'wooping/' . $slug;
$packages[$key] =
[$version => [
'name' => $key,
'version' => $version,
'type' => 'wordpress-plugin',
'dist' => [
'url' => $download_url,
'type' => 'zip'
],
"require" => [
"composer/installers" => "~1.0 || ~2.0"
]
]
];
}
return new WP_REST_Response(['packages' => $packages]);
}
The third REST endpoint creates a custom download url for a plugin. In case you’d want fine grained control on what zip you serve out, but that shouldn’t be necessary with free plugins.
In conclusion, your honor
Updating without dot org isn’t the easiest thing to do in WordPress. The project notoriously makes it hard on developers to skip dot org entirely. But thanks to projects like the Plugin Update Checker, it becomes a whole lot easier. I’d urge you to give it a try if you have a free plugin available from wordpress.org. I know discoverability is a big part of the platform, but when plugins get threatened with a hostile take over, it’s smart to have a backup plan.
Additionally; if you’re interested in our solution, we’re looking at turning this into a premium plugin. Please let us know if you’d be interested in something like that in the comments or on our socials.
Leave a Reply