OAds v2.0.0
Native ad manager for WordPress — serve direct ads without sharing revenue with ad networks. Six ad types, full targeting, A/B rotation, impression and click tracking, and revenue analytics.
What OAds does
OAds is a native ad manager for direct ad sales. It is not a Google AdSense wrapper or ad network proxy — it manages ads you sell directly to advertisers and keeps 100% of the revenue.
| Capability | Detail |
|---|---|
| Ad types | 6 types: image banner, text card, HTML embed, video, sponsored content card, sticky bar |
| Targeting | Device, user role, category, country (geo-IP), frequency cap, date scheduling |
| Rotation | Ad groups with random (weighted), round-robin, and A/B split-test rotation |
| Analytics | Impressions, clicks, CTR, unique IPs, estimated revenue (CPM + CPC) |
| Zones | Named placement areas with dimensions, fill priority, and AdSense fallback |
| Privacy | GDPR consent gate, frequency capping via localStorage, ad disclosure label |
| Placement | Shortcode, PHP template functions, WP action hook, WP widget, auto-injection |
| Admin UI | Orravo design system — dark/light, sticky header, matches OMailer/OForum |
Getting installed
oads/ folder to wp-content/plugins/.oads_ad custom post type, adds oads_settings option with defaults, registers the click-tracking rewrite rule (/oads-click/{id}/), and flushes rewrite rules.Admin interface
Follows the exact Orravo design system — sticky 2-row header positioned below the WP admin bar at top: 32px. Theme preference stored in localStorage under oads_theme.
| Tab | Description |
|---|---|
| Manage Ads | Create, edit, pause, duplicate, delete, preview ads |
| Analytics | Impressions/clicks/CTR/revenue charts and per-ad table |
| Zones | Define named placement zones with dimensions and fallback |
| Ad Groups | Set up rotation pools and A/B split tests |
| Tracking Log | Raw impression and click log |
| Settings | GDPR, disclosure label, AdSense fallback pub ID |
Six ad types
wp_kses_post. Use for rich media or custom units.Ad meta keys
| Meta key | Used by | Description |
|---|---|---|
_oads_image_url | image | Image URL |
_oads_image_id | image | WP attachment ID |
_oads_text_headline | image, text | Headline / overlay headline |
_oads_text_body | text | Body copy |
_oads_text_cta | text | CTA button label (default: "Learn More") |
_oads_destination_url | image, text | Click destination URL |
_oads_html_embed | html | Raw HTML content |
_oads_video_url | video | MP4 URL or YouTube URL |
_oads_video_type | video | self or youtube |
_oads_sponsored_headline | sponsored | Card headline |
_oads_sponsored_desc | sponsored | Card description |
_oads_sponsored_cta | sponsored | CTA label |
_oads_sponsored_img_url | sponsored | Card image URL |
_oads_sticky_position | sticky | top or bottom |
Targeting system
Targeting is evaluated server-side in OAds_CPT::passes_targeting() before an ad is served. Six independent targeting dimensions — all are optional and additive.
Device targeting (_oads_target_device)
| Value | Behaviour |
|---|---|
both | All devices (default) |
mobile | Mobile only — uses wp_is_mobile() |
desktop | Desktop only |
User role targeting (_oads_target_roles)
Comma-separated role slugs. Special values: logged_in (any authenticated user), logged_out (unauthenticated visitors), or any WP role slug such as subscriber, editor. Example: logged_in, subscriber
Category targeting (_oads_target_categories)
Comma-separated category IDs. Ad shows only when the current page belongs to one of those categories. Works on singular posts and category archive pages. Example: 3, 7, 12
Country targeting (_oads_target_countries)
Comma-separated ISO 3166-1 alpha-2 codes. Requires a geo-IP lookup at the theme level — OAds reads the constant or option OADS_VISITOR_COUNTRY. Leave blank to show to all countries.
Frequency cap (_oads_freq_cap)
Maximum impressions per user per day. Implemented in JavaScript using localStorage (key: oads_fc_{ad_id}). Set to 0 to disable.
Date scheduling
| Meta key | Format | Description |
|---|---|---|
_oads_start_date | YYYY-MM-DD | Ad won't show before this date |
_oads_end_date | YYYY-MM-DD | Ad won't show after this date |
Placement & shortcodes
Shortcode
SHORTCODE[oads]
[oads section="blog" count="2"]
[oads type="inline" section="shop"]
[oads zone="sidebar-top"]
[oads placement="sidebar" section="global" count="1"]
| Attribute | Default | Options |
|---|---|---|
section | global | Any section slug or zone slug |
count | 1 | Integer |
type | card | card, inline |
zone | — | Named zone slug (uses zone config) |
PHP template functions
PHP// Show 1 card ad in blog section
oads_show( 'blog', 1 );
// Show 2 inline ads
oads_show_inline( 'global', 2 );
// Get raw HTML
$html = oads_get( 'homepage', 1 );
// Inject an ad every 6 items in a WP_Query loop
foreach ( $items as $i => $item ) {
// … render item …
oads_inject_in_loop( 'blog', 6, $i );
}
// Render a named zone
oads_zone( 'sidebar-top', 'card' );
WP action hook
PHPdo_action( 'oads_zone', 'blog', 'inline' );
Automatic injection
OAds auto-injects ads without any template code — respects a cap of 3 ads per page. Ads in header and footer elements are automatically hidden via a <style> injection.
| Context | How ads are injected |
|---|---|
| Singular posts | After paragraph 3 and paragraph 7 (if post is long enough) |
| Archive / listing pages | Card ads inserted into the largest CSS Grid on the page, every N items |
| Sticky bar ads | Injected into <body> via wp_footer |
Ad groups & rotation
Pool multiple ads together and serve them according to a rotation policy. Set the Ad Group field on each ad to assign it to a pool.
_oads_weight value (1–10). A weight of 3 is 3× more likely than weight 1.oads_group_rr_pointers option.maybe_pick_winner() then compares CTR and locks in the winner exclusively.Setting up a group
[oads] normally — the group's rotation logic picks which ad to serve.Ad zones
Named, configurable placement areas. Instead of hardcoding section names in templates, zones let you define dimensions, fill priority, and an AdSense fallback per zone.
| Field | Description |
|---|---|
| Name | Human-readable label |
| Slug | Used in shortcode: [oads zone="slug"] |
| Max Width / Height | Informational dimensions constraint |
| Fill Priority | direct (sold ads) → house (promotional) → remnant (fallback) |
| AdSense Fallback Code | Shown when no direct ad fills the zone |
SHORTCODE[oads zone="sidebar-top"]
PHPoads_zone( 'sidebar-top', 'card' );
// or via action hook:
do_action( 'oads_zone', 'sidebar-top', 'inline' );
Analytics & revenue
Dashboard metrics
| Metric | How calculated |
|---|---|
| Impressions | Unique page views where an ad was observed (IntersectionObserver ≥ 50% threshold) |
| Clicks | Tracked via server-side redirect through /oads-click/{id}/ |
| CTR | (clicks / impressions) × 100 |
| Unique IPs | Distinct visitor IPs in the selected period |
| Est. Revenue | CPM + CPC rates set per ad (see formula below) |
Revenue formula
Set CPM and/or CPC rates per ad in the ad editor. Total revenue is the sum across all ads for the selected period.
Time periods
| Key | Range |
|---|---|
7d | Last 7 days |
30d | Last 30 days |
90d | Last 90 days |
all | Since 2020-01-01 |
CSV export
Click Export Impressions CSV or Export Clicks CSV on the Analytics or Log tab. AJAX action: oads_export_csv. Parameters: type (impressions|clicks), period (7d|30d|90d|all).
WP widget
OAds registers a WP Widget — OAds — Ad Zone — for placing ads in sidebar widget areas.
| Field | Default | Description |
|---|---|---|
| Title | (blank) | Widget title shown above ads |
| Section | sidebar | Which ad section to pull from |
| Number of ads | 1 | 1–5 |
| Format | card | card or inline |
Privacy & GDPR
Consent gate
Enable Settings → Privacy & GDPR → Require consent before tracking. The frontend JS checks localStorage.getItem('oads_consent') === '1' before firing any impression or click tracking. If consent is absent, the ad still displays but no data is recorded.
Your CMP (cookie banner) should set this when the user consents:
JSlocalStorage.setItem('oads_consent', '1');
Data stored
| Table | Data recorded |
|---|---|
wp_oads_impressions | ad_id, page_url, section, visitor_ip, user_agent, timestamp |
wp_oads_clicks | ad_id, page_url, section, visitor_ip, user_agent, referrer, timestamp |
record_impression() via the oads_save_ad_meta action or by forking the tracker class.Ad disclosure label
Every ad carries a disclosure label (default: Sponsored). Override per-ad in the editor. Ensures compliance with FTC guidelines and similar requirements.
Database schema
4 custom tables created on activation.
wp_oads_impressions
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
ad_id BIGINT UNSIGNED NOT NULL
page_url VARCHAR(500)
section VARCHAR(100)
visitor_ip VARCHAR(45)
user_agent VARCHAR(500)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_ad_id (ad_id)
INDEX idx_created (created_at)
INDEX idx_section (section)
wp_oads_clicks
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
ad_id BIGINT UNSIGNED NOT NULL
page_url VARCHAR(500)
section VARCHAR(100)
visitor_ip VARCHAR(45)
user_agent VARCHAR(500)
referrer VARCHAR(500)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_ad_id (ad_id)
INDEX idx_created (created_at)
wp_oads_zones
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
name VARCHAR(100)
slug VARCHAR(100) UNIQUE
max_width INT UNSIGNED
max_height INT UNSIGNED
fill_priority ENUM('direct','house','remnant')
adsense_code TEXT
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
wp_oads_groups
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
name VARCHAR(100)
rotation_type ENUM('random','roundrobin','ab')
ab_winner BIGINT UNSIGNED DEFAULT 0
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
PHP API reference
OAds_CPT
PHP// Get active ads for a section
OAds_CPT::get_ads( string $section, int $limit, string $placement, array $ctx ): array
// Get all meta for an ad
OAds_CPT::get_ad_data( int $ad_id ): array
// Targeting check — returns false if ad should not be shown
OAds_CPT::passes_targeting( int $ad_id, array $ctx ): bool
OAds_Display
PHPOAds_Display::get_ads_html( string $section, int $count ): string
OAds_Display::get_inline_ads_html( string $section, int $count ): string
OAds_Display::render_ad( WP_Post $ad, string $section, string $format ): string
OAds_Display::detect_section(): string
OAds_Tracker
PHPOAds_Tracker::record_impression( int $ad_id, string $page_url, string $section ): void
OAds_Tracker::record_click( int $ad_id ): void
// Per-ad stats — returns [ impressions, clicks, ctr, revenue ]
OAds_Tracker::get_ad_stats( int $ad_id, string $period ): array
// Overview stats for all ads
OAds_Tracker::get_overview_stats( string $period ): array
// Raw log entries
OAds_Tracker::get_recent_log( int $limit, string $type ): array
// CSV export (exits)
OAds_Tracker::export_csv( string $type, string $period ): never
OAds_Zones
PHPOAds_Zones::get_all(): array
OAds_Zones::get_by_id( int $id ): ?object
OAds_Zones::get_by_slug( string $slug ): ?object
OAds_Zones::save( array $data ): int|false
OAds_Zones::delete( int $id ): bool
OAds_Groups
PHPOAds_Groups::get_all(): array
OAds_Groups::get_by_id( int $id ): ?object
OAds_Groups::get_ads_in_group( int $group_id ): array
OAds_Groups::pick_ad( int $group_id ): ?WP_Post
OAds_Groups::maybe_pick_winner( int $group_id ): void
OAds_Groups::save( array $data ): int|false
OAds_Groups::delete( int $id ): bool
Hooks & filters
Actions
| Hook | Args | Description |
|---|---|---|
oads_zone | string $section, string $format | Render an ad zone — call via do_action('oads_zone', 'blog', 'card') |
oads_save_ad_meta | int $ad_id, array $post_data | Fires after an ad is saved via AJAX. Use to save custom meta fields. |
oads_admin_type_options | — | Add <option> tags to the ad type select in the modal. |
oads_admin_modal_type_fields | — | Add custom field sections to the ad editor modal. |
Filters
| Filter | Args | Description |
|---|---|---|
oads_render_ad_card | string $html, WP_Post $ad, string $section, array $data | Override or extend rendered card HTML for unknown types |
oads_render_inline_ad | string $html, WP_Post $ad, string $section, array $data | Override or extend rendered inline ad HTML |
oads_ad_js_data | array $data, int $ad_id | Extend ad data passed to the admin JS oadsAdData object |
Settings reference
Settings stored in get_option('oads_settings'). Access via helper: oads_setting( 'key', $default ).
| Key | Type | Default | Description |
|---|---|---|---|
gdpr_consent | '0' / '1' | '0' | Require localStorage consent before tracking |
disclosure_label | string | 'Sponsored' | Default ad label text shown on every ad |
freq_cap_default | int | 0 | Global default frequency cap — 0 = off |
adsense_pub_id | string | '' | Global AdSense publisher ID for zone fallbacks |
Clean uninstall
Deactivating preserves all data. Deleting the plugin via WP admin runs the uninstall hook and removes everything permanently.
wp_oads_impressionswp_oads_clickswp_oads_zoneswp_oads_groupsoads_settings and oads_group_rr_pointers optionsoads_ad posts and their post metaHow OAds stacks up
| Feature | OAds | Advanced Ads | AdSanity | Ad Inserter |
|---|---|---|---|---|
| Internal ad CPT | ✓ | ✓ | ✓ | — |
| 6 ad types | ✓ | partial | — | — |
| Impression/click tracking | ✓ | paid add-on | ✓ | — |
| Revenue tracking (CPM/CPC) | ✓ | — | — | — |
| A/B split auto-optimize | ✓ | paid add-on | — | — |
| Ad group rotation | ✓ | ✓ | — | — |
| Category targeting | ✓ | paid add-on | — | ✓ |
| Device targeting | ✓ | paid add-on | — | ✓ |
| Role targeting | ✓ | paid add-on | — | ✓ |
| Geo targeting hook | ✓ | paid add-on | — | ✓ |
| Frequency capping | ✓ | paid add-on | — | paid |
| Named zones | ✓ | ✓ | — | — |
| Analytics chart | ✓ | — | basic | — |
| CSV export | ✓ | — | — | — |
| GDPR consent gate | ✓ | partial | — | — |
| WP Widget | ✓ | ✓ | ✓ | ✓ |
| Modern admin UI | ✓ | — | partial | — |
| Single price, no add-ons | ✓ | — | — | partial |
Got a question about OAds?
Reach out directly — Kenneth replies within 24 hours.
