Developer documentation

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.

WordPress plugin WP 6.0 · PHP 8.0+ Direct ads only — no ad networks v2.0.0
01 · Overview

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.

CapabilityDetail
Ad types6 types: image banner, text card, HTML embed, video, sponsored content card, sticky bar
TargetingDevice, user role, category, country (geo-IP), frequency cap, date scheduling
RotationAd groups with random (weighted), round-robin, and A/B split-test rotation
AnalyticsImpressions, clicks, CTR, unique IPs, estimated revenue (CPM + CPC)
ZonesNamed placement areas with dimensions, fill priority, and AdSense fallback
PrivacyGDPR consent gate, frequency capping via localStorage, ad disclosure label
PlacementShortcode, PHP template functions, WP action hook, WP widget, auto-injection
Admin UIOrravo design system — dark/light, sticky header, matches OMailer/OForum
02 · Installation

Getting installed

1
Upload the oads/ folder to wp-content/plugins/.
2
Activate via Plugins → Installed Plugins.
3
On activation: creates 4 database tables, registers the oads_ad custom post type, adds oads_settings option with defaults, registers the click-tracking rewrite rule (/oads-click/{id}/), and flushes rewrite rules.
4
Navigate to OAds in the WP admin menu.
03 · Admin Interface

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.

TabDescription
Manage AdsCreate, edit, pause, duplicate, delete, preview ads
AnalyticsImpressions/clicks/CTR/revenue charts and per-ad table
ZonesDefine named placement zones with dimensions and fallback
Ad GroupsSet up rotation pools and A/B split tests
Tracking LogRaw impression and click log
SettingsGDPR, disclosure label, AdSense fallback pub ID
04 · Ad Types

Six ad types

🖼️
Image + Link
type: image
Standard banner. WP media library upload. Optional overlay headline. Click-tracked via redirect.
📝
Text Card
type: text
Headline + body copy + CTA button. Contextually relevant in article feeds.
💻
HTML Embed
type: html
Raw HTML or third-party script, passed through wp_kses_post. Use for rich media or custom units.
🎬
Video
type: video
Self-hosted MP4 (autoplay muted) or YouTube nocookie embed. IntersectionObserver starts/stops on scroll.
📰
Sponsored Content Card
type: sponsored
Looks like an editorial post card. Image + headline + description + CTA. Blends with content feed listings.
📌
Sticky Bar
type: sticky
Fixed top or bottom bar. Persists while user scrolls. Close (✕) button with smooth dismiss animation.

Ad meta keys

Meta keyUsed byDescription
_oads_image_urlimageImage URL
_oads_image_idimageWP attachment ID
_oads_text_headlineimage, textHeadline / overlay headline
_oads_text_bodytextBody copy
_oads_text_ctatextCTA button label (default: "Learn More")
_oads_destination_urlimage, textClick destination URL
_oads_html_embedhtmlRaw HTML content
_oads_video_urlvideoMP4 URL or YouTube URL
_oads_video_typevideoself or youtube
_oads_sponsored_headlinesponsoredCard headline
_oads_sponsored_descsponsoredCard description
_oads_sponsored_ctasponsoredCTA label
_oads_sponsored_img_urlsponsoredCard image URL
_oads_sticky_positionstickytop or bottom
05 · Targeting System

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 User role Category Country / Geo Frequency cap Date schedule

Device targeting (_oads_target_device)

ValueBehaviour
bothAll devices (default)
mobileMobile only — uses wp_is_mobile()
desktopDesktop 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 keyFormatDescription
_oads_start_dateYYYY-MM-DDAd won't show before this date
_oads_end_dateYYYY-MM-DDAd won't show after this date
06 · Placement & Shortcodes

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"]
AttributeDefaultOptions
sectionglobalAny section slug or zone slug
count1Integer
typecardcard, inline
zoneNamed 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.

ContextHow ads are injected
Singular postsAfter paragraph 3 and paragraph 7 (if post is long enough)
Archive / listing pagesCard ads inserted into the largest CSS Grid on the page, every N items
Sticky bar adsInjected into <body> via wp_footer
07 · Ad Groups & Rotation

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.

Random (Weighted)
Each ad drawn randomly, weighted by its _oads_weight value (1–10). A weight of 3 is 3× more likely than weight 1.
Round-Robin
Each ad takes a turn in strict sequence. Pointer stored in the oads_group_rr_pointers option.
A/B Split
Weighted random until any variant hits 100+ impressions. maybe_pick_winner() then compares CTR and locks in the winner exclusively.

Setting up a group

1
Go to OAds → Ad Groups → New Group and set the rotation type.
2
Edit each ad and set the Ad Group field to this group.
3
Set Weight per ad (1–10) for weighted or A/B rotation.
4
Use [oads] normally — the group's rotation logic picks which ad to serve.
08 · Ad Zones

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.

FieldDescription
NameHuman-readable label
SlugUsed in shortcode: [oads zone="slug"]
Max Width / HeightInformational dimensions constraint
Fill Prioritydirect (sold ads) → house (promotional) → remnant (fallback)
AdSense Fallback CodeShown 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' );
09 · Analytics & Revenue Tracking

Analytics & revenue

Dashboard metrics

MetricHow calculated
ImpressionsUnique page views where an ad was observed (IntersectionObserver ≥ 50% threshold)
ClicksTracked via server-side redirect through /oads-click/{id}/
CTR(clicks / impressions) × 100
Unique IPsDistinct visitor IPs in the selected period
Est. RevenueCPM + CPC rates set per ad (see formula below)

Revenue formula

CPM revenue
revenue += (impressions ÷ 1,000) × cpm_rate

CPC revenue
revenue += clicks × cpc_rate

Total (per ad)
total_revenue = cpm_revenue + cpc_revenue

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

KeyRange
7dLast 7 days
30dLast 30 days
90dLast 90 days
allSince 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).

10 · WP Widget

WP widget

OAds registers a WP Widget — OAds — Ad Zone — for placing ads in sidebar widget areas.

FieldDefaultDescription
Title(blank)Widget title shown above ads
SectionsidebarWhich ad section to pull from
Number of ads11–5
Formatcardcard or inline
11 · Privacy & GDPR

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

TableData recorded
wp_oads_impressionsad_id, page_url, section, visitor_ip, user_agent, timestamp
wp_oads_clicksad_id, page_url, section, visitor_ip, user_agent, referrer, timestamp
ℹ️IP addresses are stored as-is (IPv4/IPv6). For GDPR IP anonymization, hash the IP before storage by filtering 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.

12 · Database Tables

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
13 · PHP API

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
14 · Hooks & Filters

Hooks & filters

Actions

HookArgsDescription
oads_zonestring $section, string $formatRender an ad zone — call via do_action('oads_zone', 'blog', 'card')
oads_save_ad_metaint $ad_id, array $post_dataFires after an ad is saved via AJAX. Use to save custom meta fields.
oads_admin_type_optionsAdd <option> tags to the ad type select in the modal.
oads_admin_modal_type_fieldsAdd custom field sections to the ad editor modal.

Filters

FilterArgsDescription
oads_render_ad_cardstring $html, WP_Post $ad, string $section, array $dataOverride or extend rendered card HTML for unknown types
oads_render_inline_adstring $html, WP_Post $ad, string $section, array $dataOverride or extend rendered inline ad HTML
oads_ad_js_dataarray $data, int $ad_idExtend ad data passed to the admin JS oadsAdData object
15 · Settings Reference

Settings reference

Settings stored in get_option('oads_settings'). Access via helper: oads_setting( 'key', $default ).

KeyTypeDefaultDescription
gdpr_consent'0' / '1''0'Require localStorage consent before tracking
disclosure_labelstring'Sponsored'Default ad label text shown on every ad
freq_cap_defaultint0Global default frequency cap — 0 = off
adsense_pub_idstring''Global AdSense publisher ID for zone fallbacks
16 · Uninstallation

Clean uninstall

Deactivating preserves all data. Deleting the plugin via WP admin runs the uninstall hook and removes everything permanently.

1
Drops wp_oads_impressions
2
Drops wp_oads_clicks
3
Drops wp_oads_zones
4
Drops wp_oads_groups
5
Deletes oads_settings and oads_group_rr_pointers options
6
Permanently deletes all oads_ad posts and their post meta
⚠️This is irreversible. Export your analytics CSVs from the Analytics tab before uninstalling if you need historical data.
17 · Competitive Positioning

How OAds stacks up

FeatureOAdsAdvanced AdsAdSanityAd Inserter
Internal ad CPT
6 ad typespartial
Impression/click trackingpaid add-on
Revenue tracking (CPM/CPC)
A/B split auto-optimizepaid add-on
Ad group rotation
Category targetingpaid add-on
Device targetingpaid add-on
Role targetingpaid add-on
Geo targeting hookpaid add-on
Frequency cappingpaid add-onpaid
Named zones
Analytics chartbasic
CSV export
GDPR consent gatepartial
WP Widget
Modern admin UIpartial
Single price, no add-onspartial
✦ Need help?

Got a question about OAds?

Reach out directly — Kenneth replies within 24 hours.