Tips・WordPress multisite

Fetching another site's posts in multisite (switch_to_blog)

How to read another site's posts and settings in multisite. Covers switch_to_blog and cleaning up after it, a worked example gathering every site's latest posts, and the caching that keeps it from getting slow.

In multisite, you can fetch posts by temporarily switching to another site with switch_to_blog(). It’s the basis of cross-site displays like “show head office announcements on branch sites” and “gather every site’s latest posts on the front page.”

<?php
switch_to_blog(1);                 // ← the target site's ID (1 is the main site)

$query = new WP_Query([
  'posts_per_page' => 5,
]);
$items = [];
while ($query->have_posts()) {
  $query->the_post();
  $items[] = [
    'title' => get_the_title(),
    'url'   => get_permalink(),     // ← retrieving this while switched matters
    'date'  => get_the_date('Y.m.d'),
  ];
}
wp_reset_postdata();

restore_current_blog();            // ← always switch back
?>

Two things to get right—always call restore_current_blog(), and retrieve URLs and images while you’re switched.

Output after you’ve switched back

Collecting the data into an array before displaying it, as above, keeps the template side clean.

<?php if ($items) : ?>
  <ul class="cross-site-news">
    <?php foreach ($items as $item) : ?>
      <li>
        <time><?php echo esc_html($item['date']); ?></time>
        <a href="<?php echo esc_url($item['url']); ?>"><?php echo esc_html($item['title']); ?></a>
      </li>
    <?php endforeach; ?>
  </ul>
<?php endif; ?>

The trick is to build the featured image HTML as a string while you’re still switched (so the URLs belong to that site).

$items[] = [
  'title' => get_the_title(),
  'url'   => get_permalink(),
  'thumb' => get_the_post_thumbnail(get_the_ID(), 'medium'),   // keep the whole HTML
];

Listing the sites (get_sites)

<?php
$sites = get_sites([
  'number'  => 100,          // how many to fetch (default 100)
  'public'  => 1,            // public sites only
  'archived' => 0,
  'deleted'  => 0,
]);

foreach ($sites as $site) {
  echo esc_html($site->blog_id) . ' / ';
  echo esc_html($site->domain) . esc_html($site->path) . ' / ';
  echo esc_html(get_blog_option($site->blog_id, 'blogname')) . '<br>';
}
?>

get_sites() returns an array of WP_Site objects holding blog_id, domain and path. Site names and settings come from get_blog_option() (no switching needed).

Example: gathering every site’s latest posts

<?php
/**
 * Collect the newest posts across the whole network.
 * It's heavy work, so cache the result.
 */
function mynet_network_recent_posts($limit = 6) {
  $cache_key = 'mynet_network_recent_' . $limit;
  $cached = get_site_transient($cache_key);
  if ($cached !== false) {
    return $cached;
  }

  $items = [];
  foreach (get_sites(['number' => 50, 'public' => 1, 'archived' => 0, 'deleted' => 0]) as $site) {
    switch_to_blog((int) $site->blog_id);

    $q = new WP_Query(['posts_per_page' => $limit, 'ignore_sticky_posts' => true]);
    while ($q->have_posts()) {
      $q->the_post();
      $items[] = [
        'title'     => get_the_title(),
        'url'       => get_permalink(),
        'timestamp' => get_the_time('U'),
        'site_name' => get_bloginfo('name'),
      ];
    }
    wp_reset_postdata();

    restore_current_blog();
  }

  // sort newest first and return only the top ones
  usort($items, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);
  $items = array_slice($items, 0, $limit);

  set_site_transient($cache_key, $items, 10 * MINUTE_IN_SECONDS);
  return $items;
}

Three key points.

  • Loop switch → fetch → restore as one set per site
  • Sort after you’ve collected everything (cross-site sorting happens on the PHP side)
  • Cache the result (set_site_transient() is a network-wide cache)

Referencing the main (parent) site

<?php
$main_id = get_main_site_id();       // the main site's ID
$is_main = is_main_site();           // are we displaying the main site now?

if (!$is_main) {
  switch_to_blog($main_id);
  $notices = get_posts(['numberposts' => 3, 'category_name' => 'important']);
  $data = array_map(fn($p) => [
    'title' => get_the_title($p),
    'url'   => get_permalink($p),
  ], $notices);
  restore_current_blog();
}
?>

“Show head office’s important announcements on every branch site’s front page”—the single most requested pattern in multisite.

What you can get without switching

When you only need a single value, switch_to_blog() isn’t necessary—and this is lighter.

<?php
echo esc_html(get_blog_option(3, 'blogname'));      // site 3's name
echo esc_url(get_site_url(3));                      // site 3's URL
echo esc_url(get_home_url(3, '/contact/'));         // any path on site 3
$details = get_blog_details(3);                     // domain, path and more together
?>
What you wantSwitchingFunction to use
Fetch postsRequiredswitch_to_blog() plus WP_Query
Get a site name or settingNot neededget_blog_option()
Build a URLNot neededget_site_url() / get_home_url()
List the sitesNot neededget_sites()
Network-wide settingsNot neededget_site_option()

A note about image URLs

Images are stored per site under uploads/sites/<ID>/, so their URLs differ per site. Pass an ID to wp_get_attachment_image() without switching and it looks for the image on the current site and doesn’t find it.

Build the image HTML while switched and carry it around—stick to that and you won’t have accidents.

TipsDisplaying featured images, and fallback imagesThe basics of outputting featured images

When you’re aggregating across every site

Work like “count the posts on every site” costs sites × queries, so the load is high.

  • Avoid doing it while rendering an admin screen—aggregate in a WP-Cron or manual batch job and save the result in a site_option
  • Have the display side simply read the saved value

Summary

  1. Fetch another site’s posts as one set: switch_to_blog($id) → fetch → restore_current_blog()
  2. Build URLs and image HTML while switched. Display after switching back
  3. List sites with get_sites(), and for a single value use get_blog_option()—no switching needed
  4. Cross-site aggregation is heavy—cache it with set_site_transient()

Once cross-site displays are within reach, you get the full benefit of multisite’s “run them all together” promise.

FAQ

How do I fetch another site's posts in multisite?
Switch to the target site with switch_to_blog($blog_id), fetch with WP_Query or similar, and always come back with restore_current_blog(). Permalinks and image URLs you retrieve while switched are generated with that site's URL.
What happens if I forget restore_current_blog()?
Everything after it runs in the other site's context, so links and displayed content get swapped around. Always write switch_to_blog and restore_current_blog one-to-one.
Which function lists the child sites?
get_sites(). It returns an array of WP_Site objects containing blog_id, path and domain. Pass a count like 'number' => 100 (the default is 100).
What if I only want another site's name or a single setting?
get_blog_option($blog_id, 'blogname') and get_site_url($blog_id) fetch a single value without switch_to_blog. Switching has a cost, so avoiding it for a single value is more efficient.