Whimsical Promo
Editor-managed promo cards (newsletter, app, custom CTAs) rendered at theme hooks or on exit intent, with client-side chaining, cookies, and dataLayer/gtag tracking.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/xwp/whimsical-promo/archive/refs/heads/main.zipReadme
Whimsical Promo
Editor-managed promo cards — newsletter sign-ups, app pitches, any CTA — rendered after the post body, at a theme hook of your choosing, or on exit intent: the cursor leaving the window on desktop, and optionally the end of the article on a coarse-pointer device. Copy, design and targeting all live in the block editor, so changes ship without a deployment.
Activate per site from the Plugins screen. Deactivating is the kill switch: no promo markup, no assets.
What it looks like
Both of these are the same plugin, authored entirely in the block editor — the copy, the button and the design are editor settings, not code.

Inline placement: the card renders at a theme hook inside the post, in the content column. A paragraph holding only a link becomes the button.

Exit-intent placement with the slide-down presentation: it arrives when the cursor
leaves the top of the window on desktop, and closes with the ×. Same authoring, different
placement and style.
Installation
The plugin lives at github.com/xwp/whimsical-promo and installs with Composer. From the site repo root:
composer config repositories.whimsical-promo vcs https://github.com/xwp/whimsical-promo
composer require xwp/whimsical-promo:dev-main
composer/installers reads the repo's installer-paths and drops it in the plugins
directory — plugins/whimsical-promo/ in the VIP Go mirror. There is no build step, so
nothing needs compiling afterwards.
Without Composer, clone the repo into wp-content/plugins/whimsical-promo/.
Requires PHP 8.1+.
Upgrading to 1.2
Nothing to do, and nothing to rewrite. Two behaviour changes worth knowing about before you see them in the numbers:
- Overlays are now rate-limited site-wide. After any exit-intent promo opens, none opens again for 15 minutes. On a site running a single overlay this is close to invisible; on a site running two it is the difference between a fallback and a nuisance. See The quiet period.
- Exit-intent chain order has a control of its own, Show this one first, instead of depending on Order and publication date. Existing promos are unflagged, so their order is exactly what it was.
Upgrading from 1.0
1.1 renames what the plugin puts on the page, at the client's request, so that nothing in the rendered document reads as advertising:
| 1.0 | 1.1 |
|---|---|
whim-promo, whim-promo__card, whim-promo--… |
whim-bogo, whim-bogo__card, whim-bogo--… |
#whim-promo-<id> |
#whim-bogo-<id> |
whimsical_promo event, promo_* parameters |
whimsical_bogo event, bogo_* parameters |
window.whimPromoCfg |
window.whimBogoCfg |
Nothing is rewritten for you, deliberately. Any Custom CSS stored on a promo, and
any theme CSS targeting .whim-promo…, has to be updated by hand — search for
whim-promo and replace with whim-bogo. A promo whose Custom CSS still uses the old
prefix keeps rendering, but with promo-base.css and nothing else — Custom CSS replaces
the selected style rather than adding to it, so once its selectors stop matching there is
no design left underneath. The damage is obvious rather than silent, which is the point.
(An automatic rewrite was tried and dropped: telling a
selector from a value that merely contains the same word means parsing the CSS, and
getting that wrong corrupts a working stylesheet — url("#whim-promo-filter") is a
value, not a selector.)
Analytics is not migrated either — rename the variables, trigger and parameters in
GTM/GA4 to match. The data-whim-* attributes and the whim_seen_<slug> cookies are
unchanged, so nobody's frequency capping resets.
The plugin directory and its asset filenames still say whimsical-promo, since
those are the repository's name. If the word has to disappear from request paths
too, that is a repo rename and a Composer package rename — say so and it can be
done as its own change.
How a promo works
- Create Promos → Add New. The title is for the admin list only; the body is the card content, edited with regular blocks.
- Fill in Promo Settings (below the editor).
- Publish. Drafts, pending and scheduled promos never render.
Promos on the same hook form a chain. The first promo the visitor has not already interacted with wins; the rest stay hidden. So the "already subscribed?" fallback is simply the next promo in the chain.
Chain order comes from the Order field (Page Attributes), and promos left on the same Order — which is all of them until you change it — queue newest first. Every exit-intent promo shares one chain regardless of hook, and for those there is a plainer control: tick Show this one first and that promo leads, whatever the dates say (see below).
A promo with Stop showing once the visitor clicks or submits unticked is always visible: it wins as soon as the chain reaches it, whatever the cookies say, and nothing after it can ever show. That is how you build a permanent fallback — and why it has to be last.
Promos on different hooks are not a chain. They are independent slots and all of them render, which is the usual reason a hand-off appears not to work.
Promo Settings reference
| Field | What it does |
|---|---|
| Placement | Inline renders at a theme hook. Exit intent renders in the footer and appears when the cursor leaves the top of the window (desktop). To turn a promo off, set its status back to Draft — only published promos are placed on the site. |
| Preview | The whim_preview=<slug> query string for this promo, with a Copy button. Add it to any page the promo appears on to open it on demand (see below). |
| Hook name | whim_after_content — the default — appends the promo to the post body, so a fresh install needs no theme work. Otherwise any action hook your theme fires inside the post. Suggestions come from the whimsical_promo_hooks filter and a new promo starts on the first one, so a theme that filters its own hooks to the front sets the default. A few hooks are refused (see below). |
| Show on these post types | Public post types the promo may appear on. Nothing checked = never renders. |
| Stop showing once the visitor clicks or submits | Sets a whim_seen_<slug> cookie, which hands off to the next promo in the chain on later visits. Inline promos set it on click or submit; exit-intent promos set it as soon as they open (see below). Unchecked = shows every visit, never hands off. |
| Remember for (days) | How long an interaction is remembered, counted from it. Only shown while the box above is ticked; emptying it or setting 0 unticks that box, which is how a promo becomes always-visible. Default 30. |
| Show this one first | Moves this promo to the front of the exit-intent queue, so a promoted overlay goes ahead of a campaign that runs all year without either being unpublished. Only one promo can hold it: ticking the box unticks it everywhere else. Exit intent only, off by default (see below). |
| Exit-intent presentation | slide-down, slide-up or modal (only modal dims the page). Exit intent only. |
| Also trigger on mobile… | Opens the promo when the reader reaches the end of the article, since a phone has no cursor to leave the page. Exit intent only, off by default (see below). |
| Animation | slide-up-spring, slide-down-spring, fade-rise or none. The spring pair differ only in which way the card travels — pick the one that matches the edge it enters from. Readers who ask for reduced motion always get a plain crossfade. |
| Style | The design this promo ships with (see below). Defaults to basic-1. |
| Custom CSS | This promo's CSS, replacing the style's own. Administrators only. Empty means the selected style ships as designed. |
| Colour and shape overrides | Any CSS value per token — hex, rgba(), var(--your-token), gradients. These land in the wrapper's style attribute, so they beat both the style and Custom CSS. |
Override values containing ;, {, }, <, >, @, \, url(,
expression(, a comment marker, a control character, or more than 200 bytes are
rejected on save: an admin notice names the fields, and the previous value is kept.
Hooks that are refused
wp_head, wp_footer, shutdown, template_redirect, wp_enqueue_scripts,
the_content, the_title and the_excerpt are cleared on save, and dropped from the
suggestion list even if a theme adds them. Each one breaks the page rather than the promo:
wp_head puts a card in <head>, wp_footer is where the exit-intent chain already
renders, shutdown prints after </html>, the two early actions print before the document
opens, and the three filters print above the value they filter rather than after it. Use
whim_after_content instead of the_content.
Every other name is allowed, including one your theme has not fired yet. A hook that never fires renders nothing, and a name that turns out to be a filter is handed its value back untouched — so a typo can cost you the promo, never the page.
Read the full README on GitHub →
Releases
These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.