GM Popups — Plugin PRD

Working name: gm-popups Owner: Eric Downs / Grain & Mortar First client: Abri Financial Status: Drafted 2026-05-06, awaiting Aaron Haggard greenlight on the custom-build pitch Related: Plan file at /Users/edowns/.claude/plans/stateful-skipping-wren.md


1. Problem

Clients want to run popups on their WordPress sites for conversion testing (newsletter signups, lead magnets, scheduling CTAs). The market plugin most of them land on is Popup Maker, which has two real problems for an agency-built site:

  1. Design fights you. Popup Maker's templates are stock and styling them to match a brand requires either a Pro license OR fighting an editor that wasn't built for design fidelity. The visual gap between the popup and the rest of the site is always noticeable.
  2. Conversion data lives somewhere else. Out of the box, you have to wire popup events into Google Analytics or HubSpot to actually see if a popup is converting. For clients who don't have GA properly configured (most of them), they're A/B testing blind.

Aaron Haggard at Abri is the immediate trigger. From his 2026-05-06 reply:

"We are trying to test out the Popups to see what is most effective in regards to conversions, so ideally, we would like to be able to edit them in the future. Is there a way we can create a theme that can be applied to the Popups, so that we can make minor changes from our end in the future?"

He wants to A/B test for conversions. Popup Maker can't give him a clean answer to that without a separate analytics setup.

2. Users

User Role What they need
Client admin (e.g. Aaron) Edits popups, reads analytics A WP admin screen as familiar as editing a page. Edit copy, swap CTAs, toggle on/off. See views and conversion rate without leaving the dashboard.
Agency dev (G&M) Installs, configures, theme-overrides templates A plugin that drops into any WP site. CPT registers cleanly. Templates overridable from the active theme. Brand styling pulled in via theme tokens, not hardcoded.
Site visitor Sees popups, dismisses or converts Fast, accessible, doesn't break the page. Frequency cap so they don't see the same popup on every visit.

3. Goals

4. Non-goals (v1)

5. Feature spec

5.1 Custom Post Type: gm_popup

5.2 ACF field group group_gm_popup

Stored at plugin/acf-json/group_gm_popup.json. Auto-syncs if ACF Pro is active.

Tab: Content | Field | Type | Notes | |-------|------|-------| | popup_headline | text | Required | | popup_body | wysiwyg | Optional, basic toolbar | | popup_image | image | Optional | | popup_cta_text | text | Required if CTA shown | | popup_cta_link | text | NOT url type (per CLAUDE.md — url blocks placeholders) | | popup_form_shortcode | text | Optional, e.g. [ninja_form id=3] |

Tab: Display | Field | Type | Notes | |-------|------|-------| | popup_layout | select | centered only in v1; field exists for future add-ons (slide-over, bottom-bar, full-screen) | | popup_trigger_type | select | timer, scroll, exit-intent | | popup_trigger_value | number | Seconds for timer, % for scroll, ignored for exit-intent | | popup_pages | select | all, homepage, specific | | popup_url_patterns | textarea | Conditional on popup_pages = specific. One pattern per line, supports * wildcard | | popup_frequency_days | number | Default 7. After dismissal, hide for N days. |

Tab: Schedule | Field | Type | Notes | |-------|------|-------| | popup_start_date | date_picker | Optional. If set, popup won't render before this date. | | popup_end_date | date_picker | Optional. If set, popup auto-disables after this date. Useful for time-bound campaigns ("Tax Season Prep" auto-off after April 15). |

Tab: Status | Field | Type | Notes | |-------|------|-------| | popup_active | true/false | Master on/off switch |

5.3 Front-end render

  1. Plugin hooks wp_footer.
  2. Queries published gm_popup posts where popup_active = true and display rules match the current request.
  3. For each match, includes the layout template: - First check: wp-content/themes/<active-theme>/gm-popups/<layout>.php - Fallback: wp-content/plugins/gm-popups/templates/<layout>.php
  4. Template uses Alpine.js for state. The v1 centered layout is built from scratch; the slide-over scaffolding in Abri's themes/abri/theme/page-popups.php is a useful Alpine reference for transition timing and overlay handling but not a direct fork.
  5. Tailwind classes filtered through apply_filters('gm_popup_classes', $classes, $popup_id) so each theme can swap brand utility classes.
  6. Frequency cap enforced client-side via localStorage key gm_popup_<id>_dismissed_at. If Date.now() - stored < frequency_days * 86400000, popup never renders.

5.4 Triggers (JavaScript)

Trigger Behavior
timer Show after N seconds on page
scroll Show when user scrolls past N% of page height
exit-intent Show when cursor leaves viewport top edge (desktop) or after 30s of inactivity (mobile fallback)

Triggers register listeners on DOMContentLoaded, fire once per popup per session.

5.5 Tracking

DB table: wp_gm_popup_events

Column Type Index
id BIGINT auto-increment PK
popup_id BIGINT (FK posts.ID) yes
event_type VARCHAR(20) yes
session_hash CHAR(32) no
page_url VARCHAR(500) no
created_at DATETIME yes

event_type enum: view, cta_click, dismiss, form_submit.

Capture path: - View / CTA click / dismiss: AJAX action gm_popup_event, nonce-gated, no PII. Session hash is md5(IP + UA + daily_salt) so the same visitor across pages dedupes within a day. - Form submit: WordPress filter nf_after_submission for Ninja Forms, gform_after_submission for Gravity Forms. Match form ID against any gm_popup post with that shortcode → write form_submit event.

Events viewable in: WP admin Dashboard widget + per-popup edit screen sidebar. See section 5.6.

5.6 Admin experience

Top-level menu: "Popups" (under main admin nav)

List screen (edit.php?post_type=gm_popup): - Title, layout, trigger, active toggle, views (last 30 days), CTR, conversion rate - Quick toggle for active/inactive without opening the post

Edit screen (post.php?post=...&action=edit): - ACF fields fill main column - Right sidebar: stats panel showing this popup's lifetime + last 30-day numbers

Dashboard widget ("GM Popups"): - Top 3 performers by conversion rate - Total views / submits across all active popups, last 7 days - Link to full list screen

All numbers as plain HTML tables. No chart library. Numbers update on page load (no real-time JS).

6. Technical architecture

6.1 File structure

plugins/gm-popups/
├── gm-popups.php                          # Bootstrap, header, hook registration
├── readme.txt                             # WP-format readme
├── uninstall.php                          # Drop table on uninstall
├── includes/
│   ├── class-gm-popups-cpt.php            # Register CPT + ACF field group
│   ├── class-gm-popups-render.php         # wp_footer hook, template loader
│   ├── class-gm-popups-tracker.php        # AJAX endpoints + form-submit hooks
│   ├── class-gm-popups-admin.php          # Dashboard widget + list/edit columns
│   └── class-gm-popups-installer.php      # Activation hook → CREATE TABLE
├── acf-json/
│   └── group_gm_popup.json                # Field group definition
├── templates/
│   └── centered.php                     # Default layout
├── assets/
│   ├── popup.css                          # Layout structure, no brand colors
│   ├── popup.js                           # Alpine + trigger logic + event firing
│   └── admin.css                          # Dashboard widget styling
└── languages/
    └── gm-popups.pot

6.2 Plugin header

/**
 * Plugin Name: GM Popups
 * Plugin URI:  https://grainandmortar.com
 * Description: Custom popups with built-in conversion tracking. By Grain & Mortar.
 * Version:     1.0.0
 * Author:      Grain & Mortar | Eric Downs (eric@grainandmortar.com)
 * License:     Grain & Mortar
 * Requires at least: 6.0
 * Requires PHP: 7.4
 * Text Domain: gm-popups
 */

6.3 Activation / deactivation / uninstall

6.4 Theme override convention

Templates load via WP's standard pattern:

$theme_template = locate_template("gm-popups/{$layout}.php");
$plugin_template = plugin_dir_path(__FILE__) . "templates/{$layout}.php";
include $theme_template ?: $plugin_template;

A client theme that wants custom popup markup creates theme/gm-popups/centered.php. Plugin works without any theme changes if defaults are good enough.

6.5 Filters & actions for extensibility

Filter Purpose
gm_popup_classes Swap Tailwind utility classes per popup
gm_popup_should_render Bypass display rules (e.g. for previews)
gm_popup_track_ga Opt in to GA4 event push
gm_popup_session_hash Override visitor hashing logic
gm_popup_query_args Modify the WP_Query for active popups
Action Purpose
gm_popup_event_recorded Fires after every event write
gm_popup_form_submitted Fires when a popup form converts

6.6 Upsell talking points (sales / pitch reference)

For pitching this build to Abri or any future client. Ranked by how well they reframe the conversation away from "we'll restyle your popups" toward "the existing tool is the bottleneck."

# Angle Why it lands
1 "Tool is in your way" reframe The "12 popups built, 1 actively running" pattern is a tell. The tool is fiddly enough that they don't iterate. Simpler tool means more popups they actually run, more conversion data, faster learning.
2 Built-in conversion tracking No GA setup required. Views / clicks / conversion rate per popup land directly on the dashboard. Real numbers to A/B test against.
3 Performance / Core Web Vitals Popup Maker loads its full JS+CSS on every page even when no popup fires. Removing it is measurable speed win. Site Kit will catch the bump.
4 Smarter per-page targeting Show the Divorcees popup only on Divorcees. Schedule-a-call CTA only after scrolling 75% of About. Popup Maker can do this but the config is fiddly. Custom = trigger logic that fits the site.
5 Form fields styled to match Currently popup form is Popup Maker styles + Ninja Forms styles + theme styles, layered. We can make form inputs actually look like the rest of the site.
6 Editing inside WordPress Same edit experience as page content. No separate tool, no learning curve.
7 Cleaner admin Popup Maker constantly nags about Pro upgrades inside the admin (their business model). Removing it = quieter admin for daily users.
8 One fewer plugin to maintain Each plugin is an attack surface and an update obligation. Replacing one third-party plugin with one G&M owns reduces long-term maintenance risk.
9 Time-bound campaigns Built-in start/end dates. "Tax Season Prep" popup auto-disables April 16 with no one having to remember.
10 Future analytics depth Once we own the data, dashboard can grow. Performance by traffic source, by device, by referring page. Popup Maker can't show that without separate analytics.
11 Localization-ready .pot file scaffolded. Ready for Spanish / French / etc when client wants them.

Used in the 2026-05-06 Abri pitch

The reply to Aaron uses #1 (reframe), #2 (tracking), #3 (performance), #4 (per-page targeting), #5 (form fields match), and #6 (editing inside WP). #7–#11 held in reserve for follow-up conversations or other clients.


7. Abri-specific implementation notes

8. Scope & timeline

Task Hours
Plugin scaffold + activation hook + DB table 0.5
CPT + ACF field group 0.5
Slide-over template + Alpine port from page-popups.php 1.0
Trigger JS (timer / scroll / exit-intent / frequency cap) 0.75
Tracker AJAX + Ninja Forms hook + dashboard widget 1.5
Migrate eNewsletter popup, deactivate Popup Maker, QA 0.75
Subtotal popup work 5.0

Abri T&M cap is 5 hours total for all four change-request items. Items 1–3 (bullet sizing, Divorcees advisor, team photo) target ~1.5h. Realistic combined estimate is ~6.5h. Eric to flag Aaron mid-project if items 1–3 close at expected pace, before exceeding cap.

9. Roadmap (post-v1, licensable add-ons)

Add-on Description
Layouts pack Slide-over panel, bottom bar, full-screen takeover, top notification bar
Smart triggers Inactivity, repeat visitor detection, referrer-based, UTM-based
A/B engine Auto-rotate variants of the same popup, declare winners by conversion rate
Audience targeting Show to logged-in users only, exclude returning customers, geo-target
Charts dashboard Replace HTML tables with proper trend charts
GA4 / HubSpot push Mirror events to external analytics
Form library Built-in form builder so clients don't need a separate form plugin

Each roadmap item is a self-contained add-on plugin that requires gm-popups as a dependency.

10. Open decisions

11. Success metrics (Abri-specific)

12. Verification plan

  1. Build entire plugin against ~/Local Sites/abri/. Don't touch prod until Eric reviews.
  2. Confirm Chrome debug on 9233; load homepage; verify migrated eNewsletter popup fires identically (copy, form, Mailchimp flow).
  3. Trigger tests: timer 5s, scroll 50%, exit-intent. Frequency cap test: dismiss → reload within window → confirms hidden.
  4. Click CTA, dismiss, submit form → verify three rows in wp_gm_popup_events with correct event_type.
  5. WP admin → Dashboard widget shows events; per-popup stats sidebar shows numbers.
  6. Mobile (375) and desktop (1280) screenshots.
  7. Deactivate Popup Maker → confirm no JS errors / broken references.
  8. Loom walkthrough of admin UI for Aaron.
  9. Portability check: zip plugin folder, install on a clean Local site (no Abri theme), confirm CPT registers and dashboard renders without theme dependency.