Documentation

Simple Site Ads

Self-hosted ad management for WordPress — banners, takeovers, in-content, sticky bars, slide-ins, and page skins, with real targeting, A/B testing, and diagnostics. No ad network, no bloat, no upsells.

Getting started

Four steps from activation to your first ad on the site.

  1. Activate the plugin. A Site Ads menu appears in wp-admin, with Overview as its first item — live/paused counts, today's impressions/clicks, total revenue, a breakdown by placement type, and the top issues from Diagnostics, all read from data the plugin already tracks.
  2. Go to Add New Ad and choose a Type — most people start with Banner or Full-page Takeover.
  3. Upload an image, animated GIF, or video at the exact size/aspect you want — there's no cropping wrapper, so what you upload is what renders.
  4. Set the click-through link, alt text, and "open in new tab" if you need it, then tick Active.
The Overview page in the WordPress admin
The Overview page — everything on it is read from data the plugin already tracks, nothing estimated.

Where do ads actually appear?That depends on Type. Banner needs a shortcode, widget, or block placed somewhere. Takeover, Sticky Bar, Slide-in, and Skin are site-wide — once Active, they just appear. In-Content and Between Posts insert themselves automatically into matching content. See each type below.

Creative formats

What you can upload, and the small settings that apply across every ad type.

Image, GIF & video

All three are picked through the same "Select Image or Video" media button — the plugin detects which one you chose automatically, no separate setting needed.

  • Animated GIFs work exactly like a normal image, no extra configuration.
  • Video files (mp4, webm, etc.) autoplay muted and looped by default, like a GIF. Untick "Autoplay it muted and looped" to show standard playback controls instead — useful for an ad you want the visitor to deliberately press play on.
  • Image ads (not GIFs) automatically get a srcset/sizes attribute using WordPress's normal responsive-image sizes, so phones download a smaller file than desktops. GIFs are deliberately excluded — WordPress's smaller generated copies of a GIF are static (first frame only), so applying srcset would silently stop it animating on smaller screens.

Recommended ad sizes

There's no cropping wrapper on any ad type — whatever you upload is what renders, at that exact size/aspect. A few types also have their own CSS constraints, noted below.

Ad typeRecommended sizeWhy
Banner300×250The most universal, recognizable size — works in sidebars and widgets. Use 728×90 instead for a wide content-top placement.
Full-page Takeover600×400Capped at max-height: 90vh in CSS, so go bigger and bolder than a banner — it's the whole screen.
In-Content300×250Matches the convention real publishers use for in-article ads; wider starts looking out of place mid-paragraph.
Between Posts300×250 or 1200×630300×250 for Banner style; 1200×630 (a common social-card ratio) for Native Card, to match your theme's post-thumbnail aspect.
Sticky Bar~700×70CSS caps the image at max-height: 70px — keep it short and wide, a tall image just gets squashed down.
Slide-in280×230Default max width is 280px — keep it compact/roughly square, not a wide banner shape.
Skin1920×1080+It's the page background/margins — needs to be large enough not to look pixelated filling empty space around your content column.

Disclosure label

Every ad has an optional "Show a small 'Advertisement' disclosure label above this ad" checkbox, with a custom-text option. Off by default. Applies to every ad type.

Duplicate-placement guard

If the same ad ends up placed twice on one page — the same shortcode pasted twice, or a widget and a shortcode both resolving to the same ad — only the first one renders; the second is silently skipped rather than showing the ad twice.

Doesn't apply to Takeover/Sticky Bar (already site-wide singletons), or to In-Content's "Evenly spaced" mode and Between Posts, where showing the same ad multiple times on one page is the intended result.

Ad types

Seven placements, each suited to a different spot on the page.

Full-page Takeover

A full-screen overlay. Only one shows at a time — the most recently published Active one. No placement needed; once Active, it just appears.

Trigger

  • After a delay — seconds before it appears.
  • On exit intent (desktop) — fires when the mouse leaves toward the top of the browser, as if heading for the tab/URL bar.
  • After scrolling — a chosen percentage down the page.

The delay field doubles as a minimum "arming" time for exit-intent/scroll, so it can't fire the instant the page loads.

Frequency & dismissal

A frequency cap (hours before the same visitor sees it again, via a cookie) controls repeat visits. Dismiss via the close button, clicking outside the image, or pressing Escape.

Never shown to logged-in users who can edit content (Contributor role and up) — you won't see your own takeover blocking the page while you work on the site. To turn it off for one specific page (checkout, contact), edit that page and tick "Full-page takeover" under "Hide site-wide ads on this page."

In-Content

Inserts automatically into the body of matching posts — no shortcode or widget needed.

Setup

  • Choose which post types it inserts into, and optionally restrict to specific categories/tags — leave blank to match all.
  • After paragraph # — e.g. 3 drops it after the third paragraph; if the article has fewer, it's appended at the end instead.
  • After approximately N words — inserts once, at the first paragraph break at or past that word count.
  • Evenly spaced every N words — repeats the same ad throughout a long article (every 400 words in a 1600-word piece inserts it 4 times).

If more than one in-content ad could match the same page, Priority breaks the tie (higher wins; ties go to the most recently published ad). Can be turned off for one page via that page's "Hide site-wide ads on this page" box.

Between Posts

Distinct from In-Content: this inserts between individual posts in an archive/home/search-results loop, not inside a single post's own content. Set "Insert after every N posts" (e.g. 3 shows it after the 3rd, 6th, 9th post).

Theme dependencyWorks when the theme calls the_content() or the_excerpt() in its loop template, which most themes do. A fully custom loop template that calls neither won't pick it up — and a page-builder element that renders posts via its own secondary query (a WPBakery Post Grid, a Divi Blog module) won't trigger it either. It fails silently in both cases: no ad shown, nothing broken.

Display style

Banner (default) is a normal image ad. Native Card shows a title, optional excerpt, and optional thumbnail styled like a generic content card instead of a banner image, so it sits more naturally in a post grid. Native Card always shows a "Sponsored" label — not optional, since disclosure matters more for an ad deliberately styled to resemble regular content — and does not fall back to the site-wide fallback ad if unconfigured; it just doesn't render, rather than substituting a differently-shaped banner into a content grid.

Sticky Bar

A fixed bar, top or bottom of the screen. Same Trigger options as Takeover (delay / exit-intent / scroll), a frequency cap, and a background color. Always includes a close button. Only one shows per page — the most recently active one — and it doesn't need to be placed anywhere, it's site-wide like Takeover.

Same visibility rules as Takeover: never shown to logged-in staff, and can be hidden per-page.

Slide-in

A small dismissible box that slides in from a screen corner (bottom-right, bottom-left, top-right, top-left) — lighter-touch than a full-width Sticky Bar or a full-page Takeover. Same Trigger options, a frequency cap, a max width, and a background color. Always includes a close button; only the most recently published active one shows per page.

Skin

The ad image becomes the page background, visible in the margins around a centered-layout theme's content — sometimes called a "wallpaper" ad. Images/GIFs only; a video can't be a CSS background. Choose a Position (top center / top left / top right / center) and a fill color for beyond the image's edges.

If the background doesn't show upyour theme likely wraps its content in a div with its own solid background, which sits on top of the page's own background and hides the skin entirely — very common with boxed or full-width themes. Right-click that area, choose Inspect, find the wrapper element (often something like #page or .site-wrapper), and enter its selector in the ad's "Also make transparent" field. The plugin will force that element transparent too.

The Skin Settings panel
The Skin Settings panel — the "Also make transparent" field is where a theme's wrapper selector (here, #page) goes.

Not dismissible and has no frequency cap — it's page chrome, not an interruption. Optionally "Make the background clickable" (off by default): clicks in the visible margins open the Click-through URL, while clicks on your site's actual content still work normally. This relies on your theme's main content wrapper not having its own explicit CSS position/z-index — true for most themes, but verify it actually works on yours before relying on it.

Automatic targeting

Show the right ad on the right content without clicking into every article.

  1. Edit a banner ad, tick "Enable automatic targeting" under Banner Settings.
  2. Select one or more categories and/or post types — the ad automatically shows in any "Auto" widget on matching content.
  3. If more than one ad's rules match a page, the one with the higher Priority number wins (ties go to the most recently published ad).

A manual per-post override, set on the post's own edit screen, always beats these rules.

Device, country, visibility, roles & URL parameters

Available on Automatic Targeting, In-Content, Between Posts, and Skin ads, these narrow down a match rather than replace it:

RuleHow it works
DeviceDesktop / Mobile / Tablet
Country2-letter codes (e.g. US, CA, GB) — reads a header from your host/CDN (Cloudflare's CF-IPCountry and similar), not an external GeoIP lookup. If no such header is present, country rules are skipped rather than guessed.
VisibilityEveryone / logged-in only / logged-out only, optionally narrowed to specific roles
URL parametere.g. only match when ?utm_source=newsletter is present

Caching caveatUnlike category/post-type targeting (which depends on the page), all four of these depend on the visitor or the specific request. A full-page cache will serve whichever match was true when a given page was cached, until that cache entry expires or is purged. If this matters for your traffic, exclude those pages from your caching plugin, or avoid combining the two.

Scheduling & dayparting

Any ad can have an optional Start date / End date next to the Active checkbox — outside that range it automatically stops showing, no need to remember to untick Active when a campaign ends.

Dayparting (any ad type) restricts to specific days of the week and/or a time-of-day window, e.g. weekdays only, 9am–5pm. Leave blank for no restriction. Uses the site's timezone; an end time earlier than the start time wraps past midnight.

A/B testing (rotation groups)

Run two or more banner variants against each other and let the data pick a winner.

  1. Edit a banner ad, under Banner Settings give it a Group name (e.g. homepage-sidebar) and a Weight (higher = shown more often relative to others in the group).
  2. Give a second ad the same group name to create a variant.
[site_ad group="homepage-sidebar"]

Picks one live ad from the group on each page load, weighted. Widget mode: Fixed → "Rotation group (A/B test)," enter the group name.

Each variant's own Impressions/Clicks/CTR (visible in the Ads list) is your test result — no separate reporting screen needed. Diagnostics adds an "A/B Test Results" section once a group has enough data: a real statistical comparison (95% confidence) between the group's two most-shown variants, so you don't call a winner while the gap is still normal noise. It says plainly when there isn't enough data yet, rather than guessing.

Ad grid (several at once)

Distinct from [site_ad group="..."] (one ad at a time, for A/B testing) — the grid shows several members of a rotation group simultaneously, e.g. a "Sponsors" row.

[site_ad_grid group="homepage-sponsors" count="4" columns="4"]

Shows up to count distinct live ads from the group, weighted, chosen without replacement (no repeats within one grid). If the group has fewer live members than count, all of them show. Resolved client-side, same as single-ad rotation, so a full-page cache can't freeze the same subset for every visitor.

Shortcode only for now — no widget or block yet.

Stats & revenue

What's tracked automatically, and what you enter by hand.

Stats

The Ads list shows Impressions, Clicks, and CTR per ad, tracked via a lightweight AJAX beacon — no external service, no cookies for tracking itself.

  • Automatically excludes logged-in users who can edit content and recognized bot/crawler traffic (a lightweight User-Agent signature list, not an external service), so your own browsing and search-engine crawls don't inflate the numbers.
  • Rate-limited: the same visitor/IP firing the same event for the same ad again within 10 seconds is ignored, and a single IP is capped at 60 tracking events per minute across all ads.

Revenue & RPM

Revenue is manual — each ad has an optional "Revenue" field where you log what a sponsor or affiliate has actually paid. The plugin has no way to know this automatically; it's self-hosted, not an ad network. Once entered, the Ads list and Diagnostics show revenue per 1,000 impressions (RPM) so you can compare ads on a like-for-like basis.

Impression cap

Each ad has an optional cap — once its lifetime impressions reach that number, it automatically stops showing without unticking Active, so you can tell a capped ad apart from a manually paused one (the Ads list shows "Capped," Diagnostics flags it). Useful for a sponsor deal paid for a fixed number of impressions.

Diagnostics

Checks every ad's current settings and tracked data, and flags problems in plain English — a banner not referenced by any shortcode/widget/rule, a takeover with no frequency cap, an ad live for a week with zero impressions, a schedule that's already ended. Checks run fresh each page load; nothing is cached or emailed.

A Traffic Trend chart shows site-wide daily impressions for the last 14 days, plus a this-week-vs-last-week comparison once there's enough data. Tracked separately from each ad's lifetime totals — no historical backfill for data from before you updated to a version that includes it.

Placement Insights breaks real, tracked CTR down by ad type, and — only once each type has at least 100 impressions — flags if one is meaningfully outperforming another on your own site. Your actual data, not a prediction.

Managing many ads

  • Bulk actions — select multiple ads on the Ads list and choose Activate or Pause to change them all at once.
  • Pause All Ads Now (Site Ads → Settings) unticks Active on every ad immediately, without deleting anything — for an emergency or a client request to take everything down at once. Reactivate individually afterward.
  • Duplicate — hover a row on the Ads list and click Duplicate to clone it, all settings/targeting/rotation-group copy over. The copy starts paused with stats reset to zero, so you can safely tweak it before reactivating.
  • Import / Export (Site Ads → Settings) moves ad configuration between sites as JSON. Impressions, clicks, and revenue are deliberately never exported — this moves settings, not tracked data. Imported ads always start paused with their image cleared, since attachment IDs and category IDs are site-specific.
  • Fallback ad (Site Ads → Settings) shows instead of nothing whenever a banner, in-content, takeover, or sticky-bar slot would otherwise render empty. Only kicks in when something was actually supposed to show and couldn't — a page with no matching ad at all still correctly shows nothing.

Preview mode

Logged-in users who can edit content get a "Site Ads Preview" toggle in the front-end admin bar. While on, Takeover and Sticky Bar ads become visible again (normally hidden for logged-in staff) and the schedule/frequency-cap cookie are bypassed for your session — so you can check how an ad actually looks without waiting out its cap or clearing cookies. Doesn't affect what real visitors see.

Developer reference

For theme/child-theme developers who want to place an ad directly in a template file:

ssa_display_ad( $id, $context = 'banner' );
ssa_get_ad( $id, $context = 'banner' );          // returns instead of echoing

ssa_display_group_ad( $group );
ssa_get_group_ad( $group );

$context is 'banner' or 'incontent' (affects sizing). The group functions resolve immediately, server-side.

Prefer the [site_ad group="..."] shortcode/widget/block on normal cached pages instead of the group template tags — those resolve client-side and stay cache-safe. Use ssa_get_group_ad() only where client-side resolution isn't practical (a page you know is never cached).

Caching & compatibility notes

  • Compatible with full-page caching plugins (WP Rocket, W3TC, Cloudflare, etc.) for rotation-group ads — they're picked fresh in the visitor's browser on every page load rather than baked into cached HTML.
  • Visitor-dependent targeting (device/country/visibility/roles/URL param) is the exception — see the caching caveat above.
  • In-Content's "Evenly spaced" mode and Between Posts can render the same ad several times on one page by design. The tracking beacon's rate limiting means only the first of those repeats counts as an impression — correct behavior, not a bug, if tracked impressions look lower than the number of times an ad visually appears.
  • Consent-aware: if a plugin implementing the WP Consent API is active (Complianz, Real Cookie Banner, and others), the takeover/sticky frequency-cap cookies only set once the visitor has granted "functional" consent. The ad can still show, it just won't remember the visitor without permission. Impression/click tracking itself sets no cookie either way.
  • Does not touch REST API responses or expose ad data publicly beyond what's rendered.

FAQ

My skin ad isn't showing up. Why?

Almost always your theme wraps its content in a div with its own solid background, sitting on top of the page's own background. See the fix in the Skin section.

Will this work with my caching plugin?

Yes for rotation groups (resolved client-side). Visitor-dependent targeting rules are the one caveat — see Device, country, visibility, roles & URL parameters.

What happens if I uninstall the plugin?

Deleting it from the Plugins screen (after deactivating) removes all ads and their tracked data. Deactivating alone keeps everything in place — safe to do while testing.

Can I pause an ad without deleting it?

Yes — untick "Active" on the ad's edit screen, or use bulk actions/Pause All Ads Now for many at once.

Does it work with WPBakery or Divi?

Banner, Takeover, Sticky Bar, Slide-in, and Skin don't depend on how your content is built. In-Content and Between Posts insert into the rendered page content and generally work, with two known limits: In-Content falls back to appending at the end of the page on layouts built entirely from non-text builder modules (no real paragraphs to anchor to), and Between Posts won't fire inside a builder's own custom post-grid query — see the Between Posts notes.

Changelog

2.11.1

Set plugin author credit to WP Ad Pilot.

2.11.0

WordPress plugin standards pass: GPLv2 license, complete plugin/readme headers, security hardening (nonce added to the Preview Mode toggle, validated file upload on Import), and an uninstall fix so Slide-in/Skin per-page opt-out data is actually cleaned up.

2.10.0

Added the Overview page — live/paused counts, today's impressions/clicks, total revenue, a breakdown by placement type, and the top Diagnostics issues, all in one place.

2.9.0

Skin ads: added the "Also make transparent" field for themes that hide the skin behind their own wrapper background.

2.8.6

Fixed in-content ad insertion corrupting markup on page-builder layouts (WPBakery/Divi rows built from non-text modules) with no paragraph tags.

2.7.0

Added text-link ads, the ad grid shortcode, and Native Card styling for Between Posts.

Earlier

Initial feature set: Banner, Takeover, In-Content, Sticky Bar, Slide-in, and Skin ad types; targeting; weighted rotation groups with A/B statistical significance testing; scheduling, dayparting, impression caps; tracking, revenue/RPM, and Diagnostics.

Simple Site Ads — documentation by WP Ad Pilot