Working with profiles for new sites in WordPress Multisite

I wanted to take a quick moment to share a pattern I stumbled upon last week while building something for a client: this particular client runs a large, multisite WordPress network and often needs to be able to provision new sites quickly. In this case, we recently built an new theme designed to handle press sites for live events (photos, transcripts, live streams, etc.), and while I could automate a lot of the setup process (there’s literally a one-click “set all of the defaults for me” button on the dashboard), provisioning the new site still means creating the site as a Network Admin, assigning the theme, and clicking that button.

It’s good, but we can do better.

Enter new site profiles

I started playing around with the concept of “profiles” for new sites – a way that you can say “I want to create this kind of site in my network”; it’s not a new concept, but something that doesn’t really have a standard form in the WordPress ecosystem.

To make creating event sites even more straightforward for the client, I decided to look into the “Add a New Site” screen in the network administration area of WordPress to see if there was anything I could do. Fortunately, there’s a network_site_new_form action that lets us inject content into that page:

/**
 * Print the available site profiles on the "Add a new site" screen.
 *
 * The value of the "site-profiles-profile" input will determine what, if any, setup callback
 * gets executed within maybe_run_profile_callback().
 */
function display_site_profiles() {
  wp_nonce_field( 'site-profiles', 'site-profiles-nonce' );
?>

  <h3><?php esc_html_e( 'Profiles', 'site-profiles' ); ?></h3>
  <table class="form-table">
    <tr>
      <th scope="row"><?php esc_html_e( 'Site Profile', 'site-profiles' ); ?></th>
      <td>
        <fieldset>
          <legend class="screen-reader-text"><?php esc_html_e( 'Profile to use for this site', 'site-profiles' ); ?></legend>
          <label>
            <input name="site-profiles-profile" type="radio" value="" checked />
            <?php echo esc_html_x( 'No profile', 'new site profile', 'site-profiles' ); ?>
          </label>
          <br>
          <label>
            <input name="site-profiles-profile" type="radio" value="event" />
            <?php echo esc_html_x( 'Event site', 'new site profile', 'site-profiles' ); ?>
          </label>
        </fieldset>
      </td>
    </tr>
  </table>

<?php
}
add_action( 'network_site_new_form', __NAMESPACE__ . '\display_site_profiles' );

That function simply prints a new section on the form, labeled “Profiles”, and gives the user a list of available site profiles they can install (e.g. an events site, a standard blog, etc.).

The WordPress Network Admin "Add a new site" screen, with a new "Profiles" section appended with choices for "No profile" and "Event site"

Next, we need to actually handle that information when a new site is created. To do this, we’ll hook into the wpmu_new_blog action, just like we would if we were doing something on save_post:

/**
 * When a new site is created, run special installers.
 *
 * @param int $blog_id The ID of the newly-created blog.
 */
function maybe_run_profile_callback( $blog_id ) {
  if ( ! isset( $_POST['site-profiles-nonce'], $_POST['site-profiles-profile'] ) ) {
    return;
  }

  if ( ! wp_verify_nonce( $_POST['site-profiles-nonce'], 'site-profiles' ) ) {
    return;
  }

  // Callbacks should follow the pattern of "{profile}_site_callback()".
  $callback = sprintf(
    __NAMESPACE__ . '\%s_site_callback',
    sanitize_text_field( $_POST['site-profiles-profile'] )
  );

  // If we have a callback for this profile, run it.
  if ( function_exists( $callback ) ) {
    switch_to_blog( $blog_id );
    call_user_func( $callback );
    restore_current_blog();
  }
}
add_action( 'wpmu_new_blog', __NAMESPACE__ . '\maybe_run_profile_callback' );

That callback starts by verifying nonces and making sure we have the expected data, then tries to build the expected callback name, in the pattern of {profile}_site_callback(). If that function exists, it will switch to the newly-created site, execute the callback, and then restore the current site context.

Now, we need to write our event_site_callback() function, which will set up our new site. Things you’ll likely want to automate:

  • Setting the active WordPress theme
  • Activating plugins
  • Setting media sizes
  • Determining permalink structures
  • Pre-populate some content (default pages, taxonomy terms, etc.)

Fortunately, our callback can be very simple:

/**
 * Setup a new event site.
 *
 * Since there are already tools within the Events theme to set reasonable defaults, this callback
 * only has to load the setup.php file and trigger the appropriate action.
 */
function event_site_callback() {

  // First, set the active theme.
  switch_theme( 'my-clients-event-theme' );

  // Now, ensure the theme's setup file is loaded.
  require_once get_theme_root() . '/my-clients-event-theme/includes/setup.php';

  /**
   * Trigger any site profile setup actions registered within a site's theme.
   */
  do_action( 'site_profiles_setup_events_site' );
}

Since I had already scripted the setup actions for my “one-click installer”, it was a small change to take each of those functions and hook them onto the site_profiles_setup_events_site action. That lets me control the theme-specific setup actions from within the theme itself (in my case, in includes/setup.php), while my mu-plugin just switches the active theme, loads the includes/setup.php file, and executes the site_profiles_setup_events_site action.

Imagine the possibilities

This was just one way the concept of site profiles can be approached within WordPress, and I’m sure someone smarter than me will look at this and come up with an even easier way to implement it; it’s not necessarily something that many multisite networks would need, but if you, your company, or your client are regularly creating new sites in the network with a handful of different “types” of sites, profiles can be a great time-saver.

Leave a Reply