WordPress Multisite – global categories

Setting up a WP multisite instance – the client has an existing ontology / set of categories that they want to classify all content across the set of blogs. Also the desire is that any new categories would be added at the ‘network blog’ level and synced to the other blogs.

What’s the best way of doing this?

Related posts

Leave a Reply

5 comments

  1. function __add_global_categories( $term_id )
    {
        if ( get_current_blog_id() !== BLOG_ID_CURRENT_SITE || ( !$term = get_term( $term_id, 'category' ) ) )
            return $term_id; // bail
    
        if ( !$term->parent || ( !$parent = get_term( $term->parent, 'category' ) ) )
            $parent = null;
    
        global $wpdb;
    
        $blogs = $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id = '{$wpdb->siteid}'" );
        foreach ( $blogs as $blog ) {
            $wpdb->set_blog_id( $blog );
    
            if ( $parent && ( $_parent = get_term_by( 'slug', $parent->slug, 'category' ) ) )
                $_parent_ID = $_parent->term_id;
            else
                $_parent_ID = 0;
    
            wp_insert_term( $term->name, 'category',  array(
                'slug' => $term->slug,
                'parent' => $_parent_ID,
                'description' => $term->description
            ));
        }
    
        $wpdb->set_blog_id( BLOG_ID_CURRENT_SITE );
    }
    add_action( 'created_category', '__add_global_categories' );
    

    This will run whenever a category is added on the main site. A few caveats/points worth mentioning;

    • If you have a lot of blogs, this function may get pretty intensive.
    • On average, we are running anywhere between 5 to 8 queries (possibly more) per blog – depending on the speed of your database, this function may need to be chunked.
    • Only newly added categories are ‘synced’. Updating and deleting categories are not (code will need to be revised).
    • If a newly added category has a parent, and the parent cannot be found within the multisite blog in question, the category will be created with no parent (this should only be the case if the parent category was created before this function was installed).
  2. Oh, sunday procrastination…

    https://github.com/maugly/Network-Terminator

    • Alows to bulk add terms across
      network
    • You can select what sites will be
      affected
    • Works with custom taxonomies
    • Doesn’t delete
    • Doesn’t sync

    This is something I’ve done in a last few hours and I have no time for more testing now. Anyway – it works for me! .)

    Give it a try. There’s also a ‘test run’ feature implemented so you can check the result before actually doing something.

    Update -> Screenshots:

    Before action:

    Before action

    After test run:

    After test run

    The plugin linked above adds user interface but pretty much everything important happens in this function:

            <?php function mau_add_network_terms($terms_to_add, $siteids, $testrun = false) {
    
            // check if this is multisite install
            if ( !is_multisite() )
                return 'This is not a multisite WordPress installation.';
    
            // very basic input check
            if ( empty($terms_to_add) || empty($siteids) || !is_array($terms_to_add) || !is_array($siteids) )
                return 'Nah, I eat only arrays!';
    
            if ($testrun) $log = '<p><em>No need to get excited. This is just a test run.</em></p>';
            else $log = '';
    
            // loop thru blogs
            foreach ($siteids as $blog_id) :
    
                switch_to_blog( absint($blog_id) );
    
                $log .= '<h4>'.get_blog_details(  $blog_id  )->blogname.':</h4>';
                $log .= '<ul id="ntlog">';
    
                // loop thru taxonomies
                foreach ( $terms_to_add as $taxonomy => $terms ) {
    
                    // check if taxonomy exists
                    if ( taxonomy_exists($taxonomy) ) {
                        // get taxonomy name
                        $tax_name = get_taxonomy($taxonomy);
                        $tax_name = $tax_name->labels->name;
    
                        //loop thru terms   
                        foreach ( $terms as $term ) {
    
                            // check if term exists
                            if ( term_exists($term, $taxonomy) ) {
                                $log .= "<li class='notice' ><em>$term already exists in the $tax_name taxonomy - not added!</em></li>";
    
                            } else {
    
                                // if it doesn't exist insert the $term to $taxonomy
                                $term = strip_tags($term);
                                $taxonomy = strip_tags($taxonomy);
                                if (!$testrun)
                                    wp_insert_term( $term, $taxonomy );
                                $log .= "<li><b>$term</b> successfully added to the <b>$tax_name</b> taxonomy</li>"; 
                            }
                        }
                    } else {
                        // tell our log that taxonomy doesn't exists
                        $log .= "<li class='notice'><em>The $tax_name taxonomy doesn't exist! Skipping...</em></li>"; 
                    }
                }
    
                $log .= '</ul>';    
    
                // we're done here
                restore_current_blog();
    
            endforeach;
            if ($testrun) $log .= '<p><em>No need to get excited. This was just the test run.</em></p>';
            return $log;
        } ?>
    

    I will come back and edit this with more info later (if needed).

    It is far from perfect (read known issues in the plugin head).
    Any feedback appreciated!

  3. TheDeadMedic’s answer looks good, but I ended up taking a different approach to the problem. Instead of duplicating the same terms across the many sites, I instead made the other sites use the home site’s tables for terms.

    add_action('init', 'central_taxonomies');
    
    function central_taxonomies () {
      global $wpdb;
    
      $wpdb->terms = "wp_terms";
      $wpdb->term_taxonomy = "wp_term_taxonomy";
    }
    

    This replaces the table name wp_2_terms with wp_terms, etc. You should of course check in your database to make sure of the exact name of the tables, which might be different if you change your prefix.

    You can run this from either a plugin or a theme (though I recommend a plugin). I may get round to publishing a plugin to do this at some point. There are two downsides to this approach:

    • It’s only active on child sites that have the plugin activated. There’s no way to enforce this from the parent site.
    • It applies to all the taxonomies, not just selected ones.

    This approach is flexible – it can be adapted to pull categories from any blog, not just the central one.


    Update: I’ve made this into a plugin, which can be activated site-wide if you need it to be: MU Central Taxonomies

  4. Complementing @Marcus Downing answer, I’ve added a protection to prevent deleting the tables when deleting a MU site.

    This is intended to be used as a mu-plugin. If you want to use it elsewhere, change the “muplugins_loaded” hook to “plugins_loaded”.

    <?php
    
    /**
     * Plugin name: Shared Taxonomies for Multi-site
     * Version: 0.1
     * Plugin Author: Marcus Downing, Lucas Bustamante
     * Author URI: https://wordpress.stackexchange.com/a/386318/27278
     * Description: All instances of multi-site share the same taxonomies as the main site of the network.
     */
    
    $sharedTaxonomies = new SharedTaxonomies;
    
    add_action('muplugins_loaded', [$sharedTaxonomies, 'shareTaxonomies'], 1, 0);
    add_action('switch_blog', [$sharedTaxonomies, 'shareTaxonomies'], 1, 0);
    add_filter('wpmu_drop_tables', [$sharedTaxonomies, 'protectGlobalTables'], 1, 2);
    
    class SharedTaxonomies
    {
        /**
         * Multi-sites uses term tables from the main site.
         */
        public function shareTaxonomies()
        {
            /**
             * @var wpdb $wpdb
             * @var WP_Object_Cache $wp_object_cache
             */
            global $wpdb, $wp_object_cache;
    
            $wpdb->terms         = $wpdb->base_prefix . 'terms';
            $wpdb->term_taxonomy = $wpdb->base_prefix . 'term_taxonomy';
    
            $wpdb->flush();
            $wp_object_cache->flush();
        }
    
        /**
         * Prevent global tables from being deleted when deleting a mu-site
         *
         * @param array $tables The tables to drop when deleting a mu-site.
         * @param int   $siteId The ID of the mu-site being deleted.
         *
         * @return array The tables to drop when deleting a mu-site, excluding the protected ones.
         */
        public function protectGlobalTables($tables, $siteId)
        {
            $protectedTables = ['terms', 'term_taxonomy'];
    
            return array_filter($tables, function ($tableName) use ($protectedTables) {
                return !in_array($tableName, $protectedTables);
            }, ARRAY_FILTER_USE_KEY);
        }
    }