Sharing Sidebars Across a Multisite Network

Sidebars are great.  Multisite is great.  Doing something once so that it can be shared across an entire network is great, too.

So, it should stand to reason that sharing a specific sidebar, or set of sidebars, across an entire multisite network should be just as great (and easy), right?

Well, if it were, we wouldn’t be here right now.

I know what you may be thinking…

Just use switch_to_blog(), ya dingus!

Oh, how I wish it were that easy!  That was our first guess as well, but it unfortunately didn’t do the trick.  Apparently, switch_to_blog() is a trick that does not work when attempting to share a sidebar across a network.  Total bummer.

In our situation, though, we had a set of sidebars in our footer that we wanted the client to be able to manage but also share across every site within their network.  So, hardcoding the widgets in place wouldn’t have been a proper solution; neither would have been telling the client that they would need to manage three sidebars on 300+ sites.  Ouch.

We had our task: allow a set of sidebars to be displayed across the sites in a multisite network.

We had our problem: switch_to_blog() doesn’t work for this, so we needed to figure out another way to accomplish our goal.

Now we have our solution.

The first thing we want to do is build the output of our sidebars. That seems kind of important, right? Can’t see some sidebars if we’re not telling the sidebars to display. We don’t want to just display the sidebars and be done with it, though. What we want to do is store our sidebars in a transient. This transient will be deleted whenever a widget is updated in one of our three sidebars (you’ll see this later, be patient!). In order to store our set of sidebars as a variable, we need to wrap everything in an output buffer.

/**
 * Generate sidebar for footer across all sites
 */
add_action( 'init', 'wds_footer_widgets_all_sites' );
function wds_footer_widgets_all_sites() {

    if ( ! is_main_site() )
        return;

    if ( ! ( $wds_footer_widgets = get_site_transient( 'wds_footer_widgets' ) ) ) {
        // start output buffer
        ob_start();

        // Display our footer sidebars
        if ( ! dynamic_sidebar( 'sidebar-footer-1' ) ) :
                endif; // end sidebar widget area

        if ( ! dynamic_sidebar( 'sidebar-footer-2' ) ) :
        endif; // end sidebar widget area

        if ( ! dynamic_sidebar( 'sidebar-footer-3' ) ) :
        endif; // end sidebar widget area

        // grab the data from the output buffer and put it in our variable
        $wds_footer_widgets = ob_get_contents();
        ob_end_clean();

        // Put sidebar into a transient for 4 hours
        set_site_transient( 'wds_footer_widgets', $wds_footer_widgets, 4*60*60 );
    }
}

Next comes our theme’s footer. This is a bit more straight forward – we want to get the site transient that we established in our functions file above, then display its contents.

// Display our stored footer widgets
$show_footer_sidebar = get_site_transient( 'wds_footer_widgets' );
echo $show_footer_sidebar;

Now we should have our sidebars displaying, but we also want some additional magic to happen. We need to clear our transient when a widget is updated or saved in one of our three footer sidebars.  The first step of this process is to enqueue some scripts that we’re going to be using. I’m doing this in a separate file housing all of our AJAX-related commands, ajax-functions.php in our theme’s /inc/ directory.

/**
 * Enqueue and localize our scripts
 */
add_action( 'admin_enqueue_scripts', 'wds_enqueue_ajax_scripts' );
function wds_enqueue_ajax_scripts() {
    global $current_screen;

    // Only register these scripts if we're on the widgets page
    if ( $current_screen->id == 'widgets' ) {
        wp_enqueue_script( 'wds_ajax_scripts', get_stylesheet_directory_uri() . '/js/admin-widgets.js', array( 'jquery' ), 1, true );
        wp_localize_script( 'wds_ajax_scripts', 'wds_AJAX', array( 'wds_widget_nonce' => wp_create_nonce( 'widget-update-nonce' ) ) );
    }
}

In the same file, we want to create the AJAX function that runs when updating our sidebars. This function checks to make sure our nonce is present; if it is, it deletes the site transient.

/**
 * Register our AJAX call
 */
add_action( 'wp_ajax_wds-reset-transient', 'wds_ajax_wds_reset_transient', 1 );

/**
 * AJAX Helper to delete our transient when a widget is saved
 */
function wds_ajax_wds_reset_transient() {

    // Delete our footer transient.  This runs when a widget is saved or updated.  Only do this if our nonce is passed.
    if ( ! empty( $_REQUEST['wds-widget-nonce'] ) )
        delete_site_transient( 'wds_footer_widgets' );
}

We’ll need a JavaScript file to handle some back-end functionality, and we’ll need to localize the script so we can interact with that file.  We’re passing a nonce in our script as well; this will ensure that our widget form is being updated properly.

Finally, we want to create the AJAX that is going to listen for an updated widget. With jQuery first, we’re listening for a click on the remove, close, or save links on individual widgets within one of our three specific sidebars. If one of those links is clicked and we’re in one of our specified sidebars, we want to run our AJAX function.

jQuery(document).ready(function($){

    function wds_reset_footer_transient() {

        // Run our AJAX call to delete our site transient
        $.ajax({
            type : 'post',
            dataType : 'json',
            url : ajaxurl,
            data : {
                'action' : 'wds-reset-transient',
                'wds-widget-nonce' : wds_AJAX.wds_widget_nonce
            },
            error: function ( xhr, ajaxOptions, thrownError ) {
                console.log( thrownError );
                console.log( ajaxOptions );
            }
        });
    }

    // If one of our update buttons is clicked on a single widget
    $( '.widgets-holder-wrap' ).on( 'click', '.widget-control-remove, .widget-control-close, .widget-control-save', function() {

        // Get our parent, or sidebar, ID
        var widget_parent_div = $(this).parents().eq(5).attr( 'id' );

        // And if our parent div ID, or our sidebar ID, is one of the following
        if ( widget_parent_div == 'sidebar-footer-1' || widget_parent_div == 'sidebar-footer-2' || widget_parent_div == 'sidebar-footer-3' ) {

            // Run our function
            wds_reset_footer_transient();
        }
    });
});

And that’s about it! Now you can manage a set of sidebars on the main site within your network and pass those sidebars to every site within your network. One set of widgets to manage for an entire network of sites. It wasn’t the solution that jumped out at us at first, but it’s the one we found and it’s the one that works for us. We’re pretty happy with the way it turned out, and we think the client is going to be happy only having to edit their widgets once and have those changes visible on 300+ sites.

This entry was posted in General, How To, Snippets, Tutorial, WordPress, WordPress Multisite. Bookmark the permalink.

19 Responses to Sharing Sidebars Across a Multisite Network

  1. Pingback: Share sidebars in WordPress Multisite with transients : Post Status

  2. The Frosty says:

    This is great!

    Such a simple work around that probably presented it’s self as a harder solution.

  3. Weston Ruter says:

    I’ve had to do quite a bit of wrestling with sidebars and widgets recently, and I found an unfortunate condition inside of wp_get_sidebars_widgets():

    // If loading from front page, consult $_wp_sidebars_widgets rather than options
    // to see if wp_convert_widget_settings() has made manipulations in memory.
    if ( !is_admin() ) {
        if ( empty($_wp_sidebars_widgets) )
            $_wp_sidebars_widgets = get_option('sidebars_widgets', array());
        $sidebars_widgets = $_wp_sidebars_widgets;
    } else {
        $sidebars_widgets = get_option('sidebars_widgets', array());
    }

    So you can see here that after the sidebar widgets are initially loaded, they will persist in that private global variable $_wp_sidebars_widgets.

    This was a problem for Widget Customizer because it needed to be able to override the sidebars’ widgets on the fly. This is what we did to get around the above behavior:

    add_filter( 'sidebars_widgets', function ( $sidebars_widgets ) {
        $sidebars_widgets = get_option( 'sidebars_widgets' );
        unset( $sidebars_widgets['array_version'] );
        return $sidebars_widgets;
    } );

    Would this workaround also allow switch_to_blog() to be able to render sidebars from other blogs on the network?

  4. Pingback: Re: Sharing Sidebars Across a Multisite Network | Weston Ruter

  5. Pingback: Weekly News Roundup No.3

  6. sebastien says:

    the js function wds_reset_footer_transient() is in the file /js/admin-widgets.js
    That’s it ?

    • Corey Collins says:

      You would need to include all of the pieces as outlined in the post. The JS function you’re referencing contains the AJAX which deletes the transient when a widget in one of our specified sidebars is saved/updated.

  7. Daniel Lamb says:

    I’m not entirely certain about where each chunk of code is supposed to go. Does chunk 1 go in the parent site’s function file, and then chunk 2 goes in the child site’s footer? My particular network has three sites (a parent and two children) that all use child themes of a parent theme. (none of the sites use the parent theme).

    • Corey Collins says:

      Other than chunk 2 (the get_site_transient() function used to display the sidebar), everything could go in your functions file. Typically you would want to make these changes to your child themes, not your parent theme; if you make changes to your parent theme’s functions file then update that theme, you would lose those updates.

  8. Chris Lomas says:

    Thank you so much, I’ve trawled around for hours to find this solution. You’ll have made a lot of developers happy!

    • Corey Collins says:

      I’m glad I could help! Anytime we can save one of our own some sanity is a major plus.

      • Supresh says:

        This is a good solution provided
        Everytime widgets are updated or added the parent site needs to be loaded to generate the transient value again,How to overcome this problem?

        Thanks,
        Supresh

        • Hi Supresh,

          I also had this problem. I tried to call wds_footer_widgets_all_sites direct but that did nothing :)

          But instead of adding a notice to refresh the main blog, I just did a request to the main site :)


          // This is the current network's information; 'site' is old terminology.
          global $current_site;
          $response = wp_remote_get( 'http://' . $current_site->domain );

          Work like a charm!

        • Corey Collins says:

          I’m not seeing this issue at all. Upon updating/saving a widget, the site transient should be deleted. This means that the very next time we load the sidebar (whether on the main site or a sub site), we’re loading completely fresh.

          I just tested this myself and was able to update a widget on the main site then refresh on another site within the network and see the updated widget.

  9. John Bevan says:

    How do you apply this to multiple sidebar instances?

    I’m running

    function register_sidebars () {
    register_sidebar (array(
    register_sidebar(array(
    ‘name’ => esc_html__(‘Product Page Sidebar’, MBT_LANG),
    ‘id’ => ‘product-page-sidebar’,
    ‘description’ => esc_html__(‘Shown on Single Product Page’, MBT_LANG),
    ‘before_widget’ => ”,
    ‘after_widget’ => ”,
    ‘before_title’ => ”,
    ‘after_title’ => ”,
    ));

    … repeating the same 6x to register different ID sidebars in different positional locations – I need all sidebars to be transiented and display across all network sites.

    How can I output and transient each sidebar array?

    Thanks
    J

    • Corey Collins says:

      Hey John – the first chunk of code in the post is what dictates which sidebars are saved in the transient. For multiple instances of this, you would just need to create a new transient for each sidebar you would like to share across your network. Basically, copy the code in our first snippet and replace the guts to call the sidebar you’d like in each of your six instances, while also giving each instance a uniquely named transient.

      You would then use each transient name to call those sidebars throughout the rest of your site.

  10. Anthony says:

    Awesome solution, but I may have found a limitation to this solution. I was trying to do this for a pretty complicated plugin (Event Calendar Pro Mini Calendar Widget). The issue was that all of the data is loaded via AJAX. I may have just not been doing something correctly, but I couldn’t get the information to load (literally everything else was perfect). My thought was that because the information was being loaded via AJAX that there was no way for the widget to “go get” the new data from month to month.

    Although it was totally hack-y, the only solution I could find for my problem was to create a totally vanilla child theme and Iframe the widget in. Not pretty, but it worked (and sometimes thats all that matters).

    Let me know if my assumptions are incorrect. Always open to learning a better way of doing things.

    Thank you,
    Anthony

  11. Mercedes says:

    Thank you very much for this solution. But I do not work well .
    I would Since the code in functions.php and not as I show sidebars put all the blogs. I put the call to the sidebar as always

    Is this correct or should I put something else?

    regards
    Mercedes

Comments are closed.