Manage banners across WordPress Multisite: a plugin you can build today
· 4 min read
WordPress Multisite networks often need to show the same banner in the same place across every sub-site: a seasonal sale header, a network-wide notice, a sponsor placement. The obvious approach is to update each site individually, which works until there are twenty sites and the same banner lives in thirty widget areas.
The plugins that solve this tend to be full ad servers with CPM dashboards and reporting, or general-purpose ad managers where Multisite support arrived late. Most multisite operators need something smaller: create a banner once on the main site, drop a shortcode on any sub-site, and change the banner in one place when the campaign ends.
WordPress Multisite stores all sites in a single shared database. That means a shortcode on a sub-site can read the main site's options table directly, with no sync job and nothing to keep in step. The plugin described here takes about five minutes to build.
What one build gives you
- Admin screen on the main site to create, name, and delete banners
- A [network_banner name="..."] shortcode that works on every sub-site in the network
- Banner content stored in the main site's options table, read by sub-sites via switch_to_blog
- wp_kses_post filtering on both save and output
What it does not do
- Impression counting or click-through tracking
- Scheduling banners to show between specific dates
- Rotating between multiple creatives for the same placement
- Per-site overrides or excluding specific sub-sites from a banner
- JavaScript-dependent ad tags such as full AdSense embed codes
Advanced Ads does these. This covers the part most sites use.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called Network Banner Manager. Plugin Name: Network Banner Manager. Description: Manage shared HTML banners on the main Multisite site and display them on any sub-site via shortcode. Single PHP file. No dependencies. On admin_menu: if is_main_site(), add a top-level admin menu page with page title "Network Banners", menu title "Network Banners", capability "manage_options", slug "network-banners", callback render_network_banners_page. Store banners in the main site's options table under the key "network_banners" as a JSON-encoded array. Each element: id (integer), name (string), html (string). Register two admin_post actions, both using check_admin_referer: admin_post_save_network_banner: read POST banner_name and banner_html. Sanitize banner_name with sanitize_text_field, banner_html with wp_kses_post. Load existing banners via get_option("network_banners", "[]"), json_decode to array. Compute next id as max existing id + 1 (or 1 if array is empty). Append new entry. json_encode and update_option. wp_redirect to the manage page and exit. admin_post_delete_network_banner: read POST banner_id (intval). Load banners, filter out the entry with that id, update_option. wp_redirect to manage page and exit. render_network_banners_page: output h1 "Network Banners". Output add-banner form (method POST, action admin_url("admin-post.php"), hidden field action=save_network_banner, wp_nonce_field("save_network_banner")): text input name=banner_name (label "Name", required, placeholder "e.g. header-sponsor"), textarea name=banner_html (label "HTML", rows=4, required), submit button "Add Banner". Below the form, load banners and output an HTML table with columns: Name, Shortcode, Delete. Shortcode column: a code element containing the literal text [network_banner name="{name}"]. Delete column: form with method POST, action admin_url("admin-post.php"), hidden fields action=delete_network_banner and banner_id={id}, wp_nonce_field("delete_network_banner_{id}"), submit button "Delete". If no banners exist, output a p "No banners yet." Register shortcode tag "network_banner". Accept attribute "name" (default empty string), sanitized with sanitize_text_field. If name is empty, return empty string. If is_main_site(): $banners = json_decode(get_option("network_banners", "[]")). Else: switch_to_blog(get_main_site_id()), $banners = json_decode(get_option("network_banners", "[]")), restore_current_blog(). Find first entry where entry->name === $name. If found, return wp_kses_post(entry->html). Else return empty string. No settings page beyond the manage screen. No AJAX. No JavaScript. No frontend CSS. No custom database tables.
What the admin screen does
The manage page appears under the admin menu on the main site only. It shows a list of all saved banners with their names, the copy-ready shortcode for each, and a delete button. A form above the list adds new banners: a name field and an HTML textarea.
The name is a short identifier used in the shortcode attribute. If the banner is called header-sponsor, the shortcode is [network_banner name="header-sponsor"]. You can place that shortcode in a Shortcode block, a Text widget, or a template file via do_shortcode anywhere on any site in the network.
Banners are stored as a JSON array in a single row of the main site's wp_options table. No custom database tables, no migration to run.
- Add banners with a name and an HTML body
- Copy-ready shortcode displayed next to each banner
- Delete removes the banner; sub-sites showing it return an empty string
- Visible in the admin menu on the main site only
How sub-sites read banners from the main site
WordPress Multisite runs all sites on the same database installation. Each site has its own prefix (wp_2_options, wp_3_options, and so on), but WordPress provides switch_to_blog() to temporarily read another site's data. The shortcode calls switch_to_blog(get_main_site_id()), reads the network_banners option, then calls restore_current_blog() before returning.
This is a single database query per shortcode occurrence. On a page with one banner in a sidebar, that is one extra query. If the site uses a persistent object cache like Redis, the option will be cached and the query will not run on most requests.
The shortcode returns an empty string if the name does not match any saved banner. A typo in the name produces a blank space, not an error. Check for active shortcodes before deleting a banner that sub-sites are displaying.
What you can put in the HTML field
The field saves and outputs through wp_kses_post, the same filter WordPress uses for post content. Images, links, headings, inline styles, figure elements, and most HTML5 tags pass through. Script tags do not.
Most direct-sponsor placements are an image wrapped in a link, which works without restriction. Third-party ad tags from Google AdSense or Carbon Ads typically include an inline script block in their embed code. The image and anchor portion will display; the script will be stripped. For tags that require JavaScript to function, a follow-up build can enqueue the script separately and use the banner field for the container element only.
For a static sponsor logo with a link to the sponsor's site, or a promotional image pointing to a sale page, the HTML field handles everything without modification.
When a full ad management plugin makes more sense
This plugin does not count impressions, track clicks, rotate between multiple creatives, schedule banners between dates, or target by site or audience. If you need any of those, Advanced Ads has Multisite support and covers the full range. Broadstreet is built specifically for local media networks running Multisite and handles programmatic inventory. Google Ad Manager is the right tool for large-scale programmatic.
The plugin described here fits one shape: a small number of banners managed manually by a person, shown consistently across every sub-site. A university displaying a term-break notice across department sites, a franchise network sharing a seasonal campaign, or an agency managing a network of client sites all fit that shape. A news network selling CPM inventory does not.
If the person adding banners needs to be a non-developer with no WordPress admin access, that is also outside this build. The admin screen requires manage_options on the main site.
Questions
- Do I need super admin access to add banners?
No. The manage screen requires manage_options on the main site. On a standard Multisite setup, that means the main site's administrator. Super admins have it automatically, but a main-site admin can manage banners without network-wide access.
- Can I use this on a single-site WordPress install?
The shortcode will work, but the admin screen only appears on the main site, which on a single-site install is the only site. The switch_to_blog path is never used. For a single site, a text widget or hard-coded HTML is simpler.
- What happens if I delete a banner a sub-site is still displaying?
The shortcode returns an empty string and nothing is shown. No error appears. Before deleting, search for the banner name in widgets, posts, and templates on your sub-sites so you can remove or update those shortcodes first.
- Can I show a different banner on one sub-site and a different one on another?
Yes. The banners are a shared library. Each sub-site chooses which to display using the corresponding shortcode name. Place [network_banner name="promo-A"] on one sub-site and [network_banner name="promo-B"] on another, then update each banner's content from the main site whenever needed.
The prompt
Loads into the composer so you can edit it first. Nothing is built, and nothing is charged, until you send it.
Build a WordPress plugin called Network Banner Manager. Plugin Name: Network Banner Manager. Description: Manage shared HTML banners on the main Multisite site and display them on any sub-site via shortcode. Single PHP file. No dependencies. On admin_menu: if is_main_site(), add a top-level admin menu page with page title "Network Banners", menu title "Network Banners", capability "manage_options", slug "network-banners", callback render_network_banners_page. Store banners in the main site's options table under the key "network_banners" as a JSON-encoded array. Each element: id (integer), name (string), html (string). Register two admin_post actions, both using check_admin_referer: admin_post_save_network_banner: read POST banner_name and banner_html. Sanitize banner_name with sanitize_text_field, banner_html with wp_kses_post. Load existing banners via get_option("network_banners", "[]"), json_decode to array. Compute next id as max existing id + 1 (or 1 if array is empty). Append new entry. json_encode and update_option. wp_redirect to the manage page and exit. admin_post_delete_network_banner: read POST banner_id (intval). Load banners, filter out the entry with that id, update_option. wp_redirect to manage page and exit. render_network_banners_page: output h1 "Network Banners". Output add-banner form (method POST, action admin_url("admin-post.php"), hidden field action=save_network_banner, wp_nonce_field("save_network_banner")): text input name=banner_name (label "Name", required, placeholder "e.g. header-sponsor"), textarea name=banner_html (label "HTML", rows=4, required), submit button "Add Banner". Below the form, load banners and output an HTML table with columns: Name, Shortcode, Delete. Shortcode column: a code element containing the literal text [network_banner name="{name}"]. Delete column: form with method POST, action admin_url("admin-post.php"), hidden fields action=delete_network_banner and banner_id={id}, wp_nonce_field("delete_network_banner_{id}"), submit button "Delete". If no banners exist, output a p "No banners yet." Register shortcode tag "network_banner". Accept attribute "name" (default empty string), sanitized with sanitize_text_field. If name is empty, return empty string. If is_main_site(): $banners = json_decode(get_option("network_banners", "[]")). Else: switch_to_blog(get_main_site_id()), $banners = json_decode(get_option("network_banners", "[]")), restore_current_blog(). Find first entry where entry->name === $name. If found, return wp_kses_post(entry->html). Else return empty string. No settings page beyond the manage screen. No AJAX. No JavaScript. No frontend CSS. No custom database tables.
Steem