Programmatically install and activate a design template
For an add-on that bundles a companion template and installs it on first activation, a CLI installer, or a migration bringing a template across from another site. Same validation and extraction as the admin uploader.
Four functions cover the whole flow — benecaster_install_template(), benecaster_activate_template(), benecaster_get_active_template() and benecaster_get_installed_templates().
Notes
Installing does not activate — the two are separate on purpose. A freshly installed template appears in the list and changes nothing until someone selects it. The guard above is what keeps an add-on update from silently restyling a live site, which is not a good surprise for a podcaster.
A fresh site is not “no active template”, so do not write that guard as a null check. The plugin ships two built-in templates, default-light and default-dark, and default-light is active from the moment it is installed — which is what the Templates admin screen shows. benecaster_get_active_template() reports it, so null !== $active is true on every ordinary site and a null-based guard never installs anything.
built_in is the field that answers “has the owner chosen?” — true means the site is on a shipped default and replacing it overrides nobody’s decision.
null means exactly one thing: a dangling slug. The stored active template matches nothing installed or built in — one uninstalled around a stale option, say. It is not an error, because the question a caller is asking is “is a template in force right now?”, and a slug pointing at nothing is not one. A WP_Error means the lookup could not be performed at all. Both are falsy, so test is_wp_error() first: a bare if ( ! $active ) treats “I could not read this site’s state” as “this site has no template” and installs over it.
To check for one specific slug rather than “any template at all”, use benecaster_get_installed_templates(). It returns every template the site can activate — the two built-ins first, then anything installed from a zip — and is the right guard for an add-on that must not unpack its zip on every boot:
foreach ( benecaster_get_installed_templates() as $template ) {
if ( 'my-template' === $template['slug'] ) {
return; // already installed
}
}
Authorisation is the caller’s job on all four functions. They write to the uploads directory and to options and perform no capability check. Core’s own REST route checks manage_options before calling them; the functions cannot, because a CLI command or a background installer has no current user to check. If anything a web request can reach calls these, do the capability and nonce checks yourself first — see benecaster_rest_permission_admin().
Related
Code
<?php
add_action( 'benecaster_boot', function (): void {
// Never override a choice the site owner has already made.
$active = benecaster_get_active_template();
// WP_Error means "could not tell" — never install on a failed read.
if ( is_wp_error( $active ) ) {
return;
}
// A non-built-in template active means the owner picked something.
// A built-in one means the site is on an untouched default, which is
// not a choice and is safe to replace.
if ( null !== $active && ! $active['built_in'] ) {
return;
}
$manifest = benecaster_install_template(
plugin_dir_path( __FILE__ ) . 'assets/my-template.zip'
);
if ( is_wp_error( $manifest ) ) {
// Every failure arrives this way: unreadable zip, over the 50 MB
// ceiling, missing or invalid manifest, extraction failure.
error_log( 'Template install failed: ' . $manifest->get_error_message() );
return;
}
$activated = benecaster_activate_template( $manifest['slug'] );
if ( is_wp_error( $activated ) ) {
error_log( 'Template activation failed: ' . $activated->get_error_message() );
}
} );
Hooks Used
Need this built rather than just documented? See our services →