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:
- 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.
- 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
- Brand-fit popups. Visually consistent with the rest of the site because the markup is the theme's, not a third-party shadow DOM.
- Editing inside WordPress. No learning curve. Same edit experience as a page or post.
- Conversion tracking built in. Per-popup views, CTA clicks, dismissals, form submissions, conversion rate, last 30-day trend. All visible on the WP dashboard.
- Reusable across G&M clients. Plugin is theme-agnostic. Tailwind class output is filterable so each client's theme can swap classes.
- Licensable for future commercial use. v1 is foundational. v2+ adds layouts, triggers, integrations as licensed add-ons.
4. Non-goals (v1)
- Multiple popup layouts. v1 ships centered popup only. (Slide-over panel, bottom bar, full-screen takeover are roadmap.)
- Built-in form builder. v1 embeds whatever the client already uses (Ninja Forms, Gravity Forms, Mailchimp shortcode, etc).
- A/B testing engine that auto-rotates variants. v1 lets the client manually toggle popups on/off and compare numbers.
- Charts. v1 dashboard is plain HTML tables with totals and trend numbers. No chart library.
- GA4 integration. v1 owns its own data. Optional GA4 push is a v2 toggle.
- Multi-site / network admin support.
- Translations. v1 is English.
.potfile scaffolded for future translation.
5. Feature spec
5.1 Custom Post Type: gm_popup
- Top-level admin menu,
dashicons-megaphoneicon public => false,show_ui => truesupports => ['title']only- Capability:
manage_options(admins only by default; filterable)
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
- Plugin hooks
wp_footer. - Queries published
gm_popupposts wherepopup_active = trueand display rules match the current request. - 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 - 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.phpis a useful Alpine reference for transition timing and overlay handling but not a direct fork. - Tailwind classes filtered through
apply_filters('gm_popup_classes', $classes, $popup_id)so each theme can swap brand utility classes. - Frequency cap enforced client-side via
localStoragekeygm_popup_<id>_dismissed_at. IfDate.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
- Activation: create
wp_gm_popup_eventstable viadbDelta. Set version option. - Deactivation: nothing (popups remain configured, just stop rendering).
- Uninstall: drop table, delete options. CPT posts persist (WP convention).
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
- Site uses Ninja Forms + ninja-forms-mail-chimp. Existing eNewsletter popup form stays as-is, just embedded via
[ninja_form id=N]. - Theme already has
popup.cssenqueued atthemes/abri/theme/functions.php:195and a popup-script defer at line 590. Apply the same defer togm-popups'spopup.js. - Migrate the active eNewsletter popup as
gm_popuppost #1: same headline, body, CTA, form shortcode, centered layout. - Deactivate (don't delete) Popup Maker after migration. Keep ~2 weeks as fallback.
- Override centered template at
themes/abri/theme/gm-popups/centered.phponly if Abri-specific tweaks are needed (e.g.font-teodor,red-underlinebrand utility classes).
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
- Plugin slug.
gm-popupsis the working name. Final commercial name TBD if licensed. - Pricing model if licensed. One-time vs annual. Per-site vs unlimited.
- GPL vs proprietary license. GPL is required for WordPress.org distribution, but G&M can sell pro versions outside that ecosystem.
- Branding inside the dashboard. Discreet "Powered by Grain & Mortar" footer in the widget? Lead-gen funnel for other clients seeing the plugin on a peer's site.
- First non-Abri rollout. Which next G&M client gets it (KJP, FSL, others)?
- Documentation strategy. Short PDF for clients vs Loom walkthrough vs both.
11. Success metrics (Abri-specific)
- Aaron edits a popup himself within 2 weeks of handoff (proves admin UX is intuitive).
- Conversion rate on the eNewsletter popup is visible in his dashboard within 7 days of going live.
- Zero regressions on the existing eNewsletter form's Mailchimp delivery rate.
- At least one A/B comparison run by Abri using the dashboard numbers within 30 days.
12. Verification plan
- Build entire plugin against
~/Local Sites/abri/. Don't touch prod until Eric reviews. - Confirm Chrome debug on 9233; load homepage; verify migrated eNewsletter popup fires identically (copy, form, Mailchimp flow).
- Trigger tests: timer 5s, scroll 50%, exit-intent. Frequency cap test: dismiss → reload within window → confirms hidden.
- Click CTA, dismiss, submit form → verify three rows in
wp_gm_popup_eventswith correctevent_type. - WP admin → Dashboard widget shows events; per-popup stats sidebar shows numbers.
- Mobile (375) and desktop (1280) screenshots.
- Deactivate Popup Maker → confirm no JS errors / broken references.
- Loom walkthrough of admin UI for Aaron.
- Portability check: zip plugin folder, install on a clean Local site (no Abri theme), confirm CPT registers and dashboard renders without theme dependency.