agentsclimarketplace

Wp multisite

Skill mralaminahamed/wp-dev-skills/skills/wp-multisite

WordPress plugin development skills for AI coding agents — Claude Code, Gemini CLI, Cursor, Windsurf, Cline, Codex, Copilot, opencode, and more

Install
npx -y skills add mralaminahamed/wp-dev-skills --skill wp-multisite

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

Use when building or adapting a WordPress plugin for Multisite/Network — network activation (register_activation_hook with $network_wide), network admin pages (network_admin_menu), per-site vs network-wide options (get_option / get_network_option / update_network_option), looping sites with get_sites() + switch_to_blog() / restore_current_blog(), super admin capabilities (is_super_admin(), manage_network, is_network_admin()), table prefix handling ($wpdb->prefix vs $wpdb->base_prefix), blog ID awareness (get_current_blog_id), or detecting multisite context (is_multisite(), is_plugin_active_for_network()). Triggers: "make my plugin multisite compatible", "network activate", "network admin page", "per-site settings", "switch_to_blog()", "restore_current_blog()", "super admin only feature", "why does my plugin break on multisite", "run this for every site in the network", "network-wide option", "plugin only on certain sites", "blog ID handling", "is_multisite()", "is_network_admin()", "is_plugin_active_for_network()", "get_network_option()", "wpdb base_prefix", "manage_network capability", "network options page", "multisite table prefix", "site-aware hook registration", "get_sites() loop". Not for: WP-CLI multisite ops; MU-plugins auto-load behaviour (no activation hooks fire).

SKILL.md

10.5 KB, as published. Nobody here has run it

WordPress Multisite Plugin Development

Model note: Adapting an existing plugin for multisite involves scattered conditional changes — requires reading across many files to find all get_option/update_option and activation hooks. Use sonnet; haiku may miss indirect callers. New builds with multisite in mind from the start are straightforward and can use haiku.

Adapt and build plugins that work correctly on WordPress multisite networks. Covers activation scope, option storage, network admin UI, capability model, and safe site-switching patterns.

When to use

  • "Make my plugin multisite compatible", "support network activation".
  • "Add a network admin settings page", "store a network-wide option".
  • "Loop over all sites and do X", "run a task on every blog".
  • "Why does my plugin break on multisite?", "fix table prefix issues".
  • "Check if a user is super admin", "restrict to network admin only".

Not for: WP-CLI multisite operations — use wp-wpcli-and-ops (official skill). General plugin architecture — use wp-plugin-development.

Method

1. Detection and guarding

// Is this a multisite network?
if ( is_multisite() ) { ... }

// Is the current screen the network admin?
if ( is_network_admin() ) { ... }

// Is the plugin network-activated?
if ( is_plugin_active_for_network( plugin_basename( __FILE__ ) ) ) { ... }

// Is the user a super admin?
if ( current_user_can( 'manage_network' ) ) { ... }  // preferred
if ( is_super_admin() ) { ... }                       // also fine

Never assume is_multisite() is false — always write code that handles both cases unless the plugin explicitly requires multisite.

2. Activation scope

A plugin can be:

  • Site-activated — active on one site, hooks run only on that site.
  • Network-activated — active on all sites, activation hook runs once on the network.
register_activation_hook( __FILE__, 'my_plugin_activate' );

function my_plugin_activate( $network_wide ) {
    if ( $network_wide && is_multisite() ) {
        // Run setup for every existing site
        $sites = get_sites( [ 'number' => 0, 'fields' => 'ids' ] );
        foreach ( $sites as $site_id ) {
            switch_to_blog( $site_id );
            my_plugin_setup_site();
            restore_current_blog();
        }
    } else {
        my_plugin_setup_site();
    }
}

// Also run setup when a new site is created (for network-activated plugins)
add_action( 'wp_initialize_site', function( WP_Site $new_site ) {
    if ( is_plugin_active_for_network( plugin_basename( __FILE__ ) ) ) {
        switch_to_blog( $new_site->blog_id );
        my_plugin_setup_site();
        restore_current_blog();
    }
} );

3. Option storage: site vs network

FunctionScopeStorage
get_option() / update_option()Current site{prefix}options (per site)
get_network_option() / update_network_option()Entire network{main_prefix}sitemeta
get_site_meta() / update_site_meta()Per-site record{main_prefix}blogmeta
// Network-wide setting (same value for all sites)
$api_key = get_network_option( null, 'my_plugin_api_key' );
update_network_option( null, 'my_plugin_api_key', sanitize_text_field( $key ) );

// Per-site setting (different value per site)
$setting = get_option( 'my_plugin_site_setting', 'default' );
update_option( 'my_plugin_site_setting', $value );

// Per-site metadata on the site object
$site_note = get_site_meta( get_current_blog_id(), 'my_plugin_note', true );
update_site_meta( get_current_blog_id(), 'my_plugin_note', sanitize_textarea_field( $note ) );

4. Network admin settings page

// Register under network admin menu
add_action( 'network_admin_menu', function() {
    add_menu_page(
        __( 'My Plugin Network', 'my-plugin' ),
        __( 'My Plugin', 'my-plugin' ),
        'manage_network',             // super admin only
        'my-plugin-network',
        'my_plugin_render_network_page'
    );
} );

// Network admin settings must use wp_redirect — Settings API not available in network admin
add_action( 'network_admin_edit_my_plugin_network_settings', function() {
    check_admin_referer( 'my_plugin_network_settings' );
    if ( ! current_user_can( 'manage_network' ) ) wp_die( -1 );

    update_network_option( null, 'my_plugin_api_key',
        sanitize_text_field( wp_unslash( $_POST['api_key'] ?? '' ) )
    );

    wp_redirect( add_query_arg( [ 'updated' => 'true' ], network_admin_url( 'settings.php?page=my-plugin-network' ) ) );
    exit;
} );

function my_plugin_render_network_page() {
    if ( isset( $_GET['updated'] ) ) {
        echo '<div class="notice notice-success"><p>' . esc_html__( 'Settings saved.', 'my-plugin' ) . '</p></div>';
    }
    $api_key = get_network_option( null, 'my_plugin_api_key', '' );
    ?>
    <div class="wrap">
        <h1><?php esc_html_e( 'My Plugin Network Settings', 'my-plugin' ); ?></h1>
        <form method="post" action="<?php echo esc_url( network_admin_url( 'edit.php?action=my_plugin_network_settings' ) ); ?>">
            <?php wp_nonce_field( 'my_plugin_network_settings' ); ?>
            <table class="form-table">
                <tr>
                    <th scope="row"><?php esc_html_e( 'API Key', 'my-plugin' ); ?></th>
                    <td><input type="text" name="api_key" value="<?php echo esc_attr( $api_key ); ?>" class="regular-text" /></td>
                </tr>
            </table>
            <?php submit_button( __( 'Save Settings', 'my-plugin' ) ); ?>
        </form>
    </div>
    <?php
}

5. Looping over sites

function my_plugin_run_for_all_sites( callable $callback ) {
    if ( ! is_multisite() ) {
        $callback();
        return;
    }

    $sites = get_sites( [
        'number'     => 0,     // no limit — consider chunking for large networks
        'fields'     => 'ids',
        'archived'   => 0,
        'deleted'    => 0,
        'spam'       => 0,
    ] );

    foreach ( $sites as $site_id ) {
        switch_to_blog( $site_id );
        try {
            $callback( $site_id );
        } finally {
            restore_current_blog(); // always restore, even on exception
        }
    }
}

// Usage
my_plugin_run_for_all_sites( function( $site_id ) {
    update_option( 'my_plugin_version', MY_PLUGIN_VERSION );
} );

Chunked loop for large networks:

$page  = 1;
$limit = 100;
do {
    $sites = get_sites( [ 'number' => $limit, 'offset' => ( $page - 1 ) * $limit, 'fields' => 'ids' ] );
    foreach ( $sites as $site_id ) {
        switch_to_blog( $site_id );
        my_plugin_process_site();
        restore_current_blog();
    }
    $page++;
} while ( count( $sites ) === $limit );

6. Custom DB tables on multisite

Each site has its own table prefix. Create per-site tables in the setup function (called per-site in activation):

function my_plugin_setup_site() {
    global $wpdb;
    $table_name      = $wpdb->prefix . 'my_plugin_data'; // $wpdb->prefix is site-specific
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        data longtext NOT NULL,
        PRIMARY KEY (id)
    ) {$charset_collate};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}

For network-wide tables (one table shared by all sites), use $wpdb->base_prefix:

$network_table = $wpdb->base_prefix . 'my_plugin_network_log';

7. Capabilities on multisite

// Super admin (network administrator) check
if ( current_user_can( 'manage_network' ) ) { ... }

// Site admin on the CURRENT site
if ( current_user_can( 'manage_options' ) ) { ... }

// Grant a cap only to super admins (can't remove built-in super admin caps)
add_filter( 'user_has_cap', function( $caps, $cap_to_check, $args, $user ) {
    if ( 'manage_network_plugins' === $cap_to_check && ! is_super_admin( $user->ID ) ) {
        $caps['manage_network_plugins'] = false;
    }
    return $caps;
}, 10, 4 );

Notes

  • switch_to_blog() is expensive — it changes $wpdb->prefix, flushes object cache per-site, and swaps several globals. Minimise calls; batch work per site.
  • Always wrap switch_to_blog() / restore_current_blog() in try/finally to guarantee restoration even on errors.
  • Avoid BLOG_ID_CURRENT_SITE constant — use get_current_blog_id() and get_main_site_id() instead.
  • Network admin pages can't use the WordPress Settings API (register_setting, add_settings_section) — handle saves via network_admin_edit_{action} hooks with manual wp_redirect.
  • On large networks (1000+ sites), avoid get_sites( [ 'number' => 0 ] ) — chunk with number/offset or use Action Scheduler (wp-background-processing) to process sites asynchronously.

References

  • references/multisite-patterns.md — detection helpers, site switching, options API, get_sites() params, table prefix, capabilities, activation hooks.
  • references/network-admin-patterns.md — network admin menu, network settings save via network_admin_edit_{action}, network option storage, network-wide transients.
  • references/multisite-testing.md — PHPUnit multisite bootstrap, data-isolation tests, network-activation tests, new-site hook tests, gotchas.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.