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 →restoreas 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 want | Switching | Function to use |
|---|---|---|
| Fetch posts | Required | switch_to_blog() plus WP_Query |
| Get a site name or setting | Not needed | get_blog_option() |
| Build a URL | Not needed | get_site_url() / get_home_url() |
| List the sites | Not needed | get_sites() |
| Network-wide settings | Not needed | get_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 imagesWhen 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
- Fetch another site’s posts as one set:
switch_to_blog($id)→ fetch →restore_current_blog() - Build URLs and image HTML while switched. Display after switching back
- List sites with
get_sites(), and for a single value useget_blog_option()—no switching needed - 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.