Conversion Surfaces
Feature description and testing guide for conversion surfaces — modals, banners, slide-ins, and inline prompts
1. What Are Conversion Surfaces?
Conversion Surfaces are promotional overlays that appear on the Reader Portal to encourage readers to take a desired action — like registering for a free account, subscribing to a paid plan, signing up for the newsletter, or visiting a campaign page.
Think of them as smart, configurable pop-ups that know:
- Who to show to (anonymous visitors, registered readers, subscribers)
- Where to show (which pages, which device types)
- When to show (after scrolling, after time on page, on exit intent)
- How often to show (once per session, once per day, once per week)
- What to say (custom title, description, image, call-to-action buttons)
- What to offer (linked coupon code, specific subscription plan)
They are NOT random pop-ups. Each surface is a deliberately crafted, targeted touchpoint in the reader's journey, designed to convert at the right moment without disrupting the reading experience.
2. Business Objectives
| Objective | How Conversion Surfaces Help |
|---|---|
| Grow registered reader base | Show registration prompts to anonymous visitors after they've engaged with content |
| Convert free readers to paid subscribers | Present subscription offers at high-intent moments (after finishing an article, on paywalled content) |
| Grow newsletter audience | Offer newsletter sign-up at contextually relevant points |
| Promote campaigns & events | Drive awareness for literary events, writing contests, and outreach campaigns |
| Drive e-commerce sales | Promote bookstore products to engaged readers |
| Recover abandoning visitors | Capture attention on exit intent before readers leave the site |
| Distribute discount codes | Deliver coupon codes through targeted, timed promotional surfaces |
| Measure marketing effectiveness | Track impressions, clicks, conversions, and attribution through GA4 and internal analytics |
3. Key Concepts & Terminology
| Term | Meaning |
|---|---|
| Surface | A single promotional overlay instance with its own content, targeting, and trigger rules |
| Surface Type | The visual format (modal, banner, slide-in, sticky notification, inline embed) |
| Conversion Goal | The desired reader action (register, subscribe, view campaign, etc.) |
| Targeting Rules | Conditions that determine who is eligible to see the surface |
| Trigger | The event that causes the surface to actually appear (scroll depth, time on page, etc.) |
| Frequency Cap | How often the same reader sees the same surface |
| Suppression | Rules that prevent a surface from appearing (after dismissal, after conversion) |
| Priority | When multiple surfaces are eligible, the highest priority one is shown |
| Impression | The surface was displayed to a reader |
| Click | The reader clicked the primary CTA |
| Dismiss | The reader closed/dismissed the surface |
| Conversion | The reader completed the desired action |
| Eligible | The surface passed all targeting checks and is ready to be triggered |
4. Surface Types — What Readers See

4.1 Modal
A centered overlay that appears on top of the page content with a semi-transparent backdrop.
| Property | Value |
|---|---|
| Visual | Centered card with backdrop blur |
| Placement | Always center of viewport |
| Size | Medium card (max-width ~480px on desktop) |
| Mobile behavior | Slides up from bottom as a bottom sheet |
| Dismissal | Click backdrop, click X button, press Escape |
| Content | Title, subtitle, body text, image, primary CTA, secondary CTA, optional coupon code |
| Best for | High-priority prompts (subscription, registration), exit intent captures |
When to use: When you want to demand attention. The reader must interact with it (click CTA or dismiss) before continuing.
4.2 Sticky Notification
A small, persistent card that appears at a corner of the screen.
| Property | Value |
|---|---|
| Visual | Compact floating card with shadow |
| Placement | Bottom-right (default), bottom-left, top-right, top-left |
| Size | Small — does not block content reading |
| Mobile behavior | Compact card at bottom of screen |
| Dismissal | Click X button |
| Content | Title, short body text, primary CTA, optional coupon |
| Best for | Low-disruption prompts, newsletter sign-up nudges |
When to use: When you want to be present but not intrusive. The reader can continue reading while seeing the notification.
4.3 Slide-In Panel
A full-height panel that slides in from the right side of the screen.
| Property | Value |
|---|---|
| Visual | Full-height panel with backdrop |
| Placement | Slides from right edge |
| Size | ~400px wide panel |
| Mobile behavior | Full-width panel from bottom |
| Dismissal | Click backdrop, click X button, press Escape |
| Content | Full rich content area — title, subtitle, body, image, CTAs, coupon |
| Best for | Detailed offers, plan comparisons, event promotions |
When to use: When you need more space to present an offer or detailed information without navigating away from the current page.
4.4 Top Banner
A full-width strip at the top of the page.
| Property | Value |
|---|---|
| Visual | Full-width horizontal strip fixed to top of viewport |
| Placement | Above all content |
| Size | Thin horizontal bar (~60-80px height) |
| Mobile behavior | Same — full-width top bar |
| Dismissal | Click X button |
| Content | Title (compact), primary CTA button |
| Best for | Site-wide announcements, time-limited offers, simple CTAs |
When to use: When you want maximum visibility with minimum disruption. Good for announcements that every reader should notice.
4.5 Bottom Banner
Same as top banner but fixed to the bottom of the viewport.
| Property | Value |
|---|---|
| Visual | Full-width horizontal strip fixed to bottom of viewport |
| Placement | Below all content |
| Best for | Persistent subtle CTAs, cookie-banner-style promotions |
When to use: When you want presence without competing with the header navigation area.
4.6 Inline Embed
An embedded card that appears within the page content flow (between sections or after article content).
| Property | Value |
|---|---|
| Visual | Self-contained card rendered inline within the page |
| Placement | Within page content (position configured by the system) |
| Size | Full content width |
| Mobile behavior | Full-width card in content flow |
| Dismissal | Cannot be dismissed — it's part of the content |
| Content | Title, body text, CTA, optional coupon |
| Best for | Contextual promotions that feel like part of the editorial experience |
When to use: When you want the promotion to feel native to the page, not like an interruption.
5. Conversion Goals — What You Want Readers to Do
Each surface must be assigned one conversion goal. This determines what action counts as a "conversion."
| Goal | Description | Typical CTA | Target Audience |
|---|---|---|---|
| register | Get the reader to create a free account | "Create Free Account" / "Sign Up" | Anonymous visitors |
| login | Get a registered reader to sign back in | "Sign In" | Returning anonymous visitors |
| subscribe_paid | Get the reader to purchase a subscription | "Subscribe Now" / "View Plans" | Anonymous + registered (non-subscribers) |
| subscribe_newsletter | Get the reader to join the email newsletter | "Subscribe to Newsletter" | Readers not yet on the mailing list |
| view_campaign | Drive traffic to a marketing campaign page | "Learn More" / "Join the Event" | All or targeted audiences |
| view_event | Drive attendance to a literary event | "RSVP Now" / "View Event Details" | All audiences |
| redeem_coupon | Get readers to use a promotional discount code | "Get 20% Off" / "Claim Offer" | Targeted audiences |
| view_product | Drive traffic to a product in the bookstore | "Shop Now" / "View Book" | All audiences |
| visit_pricing | Drive readers to the subscription pricing page | "See Pricing" / "Compare Plans" | Non-subscribers |
| custom_cta | Any other action with a custom URL | Custom label | Any audience |
6. Targeting Rules — Who Sees a Surface
Targeting rules are the first layer of filtering. A reader must match ALL the targeting criteria to be considered eligible.
6.1 Audience Targeting
| Audience | Who It Includes |
|---|---|
| anonymous | Visitors who are not logged in |
| registered | Readers who have a free account but no active subscription |
| subscriber | Readers with an active paid subscription |
| newsletter_subscribed | Readers who are on the email newsletter list |
| newsletter_unsubscribed | Readers who have not subscribed to the newsletter |
Rule: If the surface targets ["anonymous", "registered"], it will show to both anonymous visitors AND logged-in free readers, but NOT to paid subscribers.
Important: If the audience list is empty [], the surface matches ALL audiences (no audience restriction).
6.2 Page Type Targeting
Restrict which types of pages the surface appears on:
| Page Type | Description |
|---|---|
| homepage | The main landing page |
| article | Individual article reading pages |
| section | Section listing pages (Fiction, Poetry, etc.) |
| archive | Archive and issue browsing pages |
| search | The search results page |
| tag | Tag listing pages |
| author | Author profile pages |
| subscribe | Subscription/pricing pages |
| shop | Bookstore pages |
| campaign | Campaign landing pages |
| issue | Magazine issue detail pages |
| newsletter | Newsletter archive pages |
Rule: If the page type list is empty, the surface matches ALL page types.
6.3 Device Type Targeting
| Device | Description |
|---|---|
| desktop | Screen width > 1024px |
| tablet | Screen width 768px - 1024px |
| mobile | Screen width < 768px |
Rule: If the device list is empty, the surface matches ALL devices.
6.4 Content Targeting (Advanced)
For article pages, surfaces can target based on content attributes:
| Rule | Description | Example |
|---|---|---|
| contentTypes | Article content types | ["fiction", "poetry"] |
| sections | Article section slugs | ["fiction", "essays"] |
| tags | Article tag slugs | ["south-asian-lit", "translation"] |
6.5 URL Targeting (Advanced)
| Rule | Description | Example |
|---|---|---|
| excludeUrls | Pages where the surface should never appear | ["/account/*", "/subscribe/checkout"] |
| includeUrls | Only show on these specific pages | ["/fiction/*", "/poetry/*"] |
6.6 UTM Targeting (Advanced)
| Rule | Description | Example |
|---|---|---|
| utmSource | Only show to visitors from specific sources | ["facebook", "twitter"] |
| utmMedium | Only show to visitors from specific mediums | ["social", "email"] |
| utmCampaign | Only show to visitors from specific campaigns | ["spring-promo"] |
7. Trigger Rules — When a Surface Appears
After a reader is deemed eligible (passes all targeting rules), the trigger determines the exact moment the surface appears.
| Trigger | How It Works | Configuration | Best For |
|---|---|---|---|
| page_load | Shows immediately when the page loads | No configuration needed | Urgent announcements, time-sensitive offers |
| time_on_page | Shows after the reader has been on the page for N seconds | triggerDelaySeconds: 30 (e.g., show after 30 seconds) | Engaged readers who are spending time reading |
| scroll_depth | Shows when the reader scrolls past a certain percentage of the page | triggerScrollPercent: 50 (e.g., show at 50% scroll) | Readers who are actively reading through content |
| exit_intent | Shows when the reader moves their mouse toward the browser's close/back button (desktop only) | No configuration needed | Last-chance conversion before the reader leaves |
| page_view_count | Shows when the reader has viewed N pages in the current session | triggerPageViews: 3 (e.g., show after 3 pages) | Engaged visitors who are browsing multiple pages |
| after_article_read | Shows after the reader has finished reading an article (scrolled to ~90% + spent sufficient time) | No configuration needed | High-intent moment after content consumption |
Important: Exit intent only works on desktop. On mobile, the system falls back to scroll_depth (75%) as an equivalent signal.
8. Frequency & Suppression — How Often a Surface Appears
8.1 Frequency Capping
Each surface has a frequency limit that controls how many times a reader sees it within a time window.
| Setting | Description | Example |
|---|---|---|
| frequencyLimit | Maximum number of times to show | 1 = show once |
| frequencyUnit | Time window for the limit | session, day, week, or month |
Examples:
frequencyLimit: 1, frequencyUnit: day= Show at most once per dayfrequencyLimit: 3, frequencyUnit: week= Show up to 3 times per weekfrequencyLimit: 1, frequencyUnit: session= Show once per browser session
8.2 Dismissal Suppression
When a reader dismisses (closes) a surface, you can configure how long before they see it again:
| Setting | Description |
|---|---|
| suppressAfterDismiss: 0 | Only suppress for the current session |
| suppressAfterDismiss: 1 | Suppress for 1 day after dismissal |
| suppressAfterDismiss: 7 | Suppress for 7 days after dismissal |
| suppressAfterDismiss: 30 | Suppress for 30 days after dismissal |
8.3 Conversion Suppression
| Setting | Description |
|---|---|
| suppressAfterConversion: true | If the reader clicks the CTA and completes the action, never show this surface again |
| suppressIfConverted: true | If the reader has already achieved the goal through ANY means (e.g., already registered), never show this surface |
Example: A "Register" surface with suppressIfConverted: true will never show to a reader who is already logged in, even if they've never seen this specific surface before.
8.4 How Frequency State Is Tracked
- Anonymous readers: Frequency state is tracked in the browser's
localStorage. Clearing browser data resets the frequency state. - Authenticated readers: Frequency state is tracked both in
localStorage(for speed) and in theConversionEventtable on the server (for cross-device accuracy). Server-side checks are performed for authenticated readers during the eligible API call.
9. Scheduling & Lifecycle — When a Surface Is Active
9.1 Status Lifecycle
Every surface goes through a lifecycle of statuses:
(cron: startsAt reached)
draft -------> scheduled --------------------------> active
| | |
| | | (manual)
v v v
archived archived paused
|
(cron: endsAt passed) | (manual)
active ---------> expired v
| active
v
archived| Status | Meaning | Visible to Readers? |
|---|---|---|
| draft | Being created/edited, not visible | No |
| scheduled | Has a future startsAt date, will auto-activate | No |
| active | Currently live and showing to eligible readers | Yes |
| paused | Temporarily disabled by admin | No |
| expired | Past its endsAt date, auto-deactivated | No |
| archived | Permanently retired, preserved for analytics | No |
9.2 Allowed Status Transitions
| From | Can Transition To |
|---|---|
| draft | active, scheduled, archived |
| scheduled | active, draft, archived |
| active | paused, archived |
| paused | active, archived |
| expired | archived |
| archived | (terminal state — no transitions) |
9.3 Automatic Transitions (Cron Job)
A background cron job runs every 15 minutes and:
- Activates surfaces where
status = scheduledANDstartsAt <= now - Expires surfaces where
status = activeANDendsAt < now
This means:
- A scheduled surface will go live within ~15 minutes of its start time
- An active surface will expire within ~15 minutes of its end time
9.4 Immediate Activation
If you don't set a startsAt date and change the status directly from draft to active, the surface starts showing to readers immediately.
10. Priority & Conflict Resolution — Which Surface Wins
When multiple surfaces are eligible for the same reader on the same page, only one is shown. The system resolves conflicts using priority ordering:
- Surfaces are ranked by
priority(higher number = higher priority) - If two surfaces have the same priority, the more recently created one wins
- Only one overlay surface (modal, slide-in, banner, sticky) is shown per page load
- Inline embed surfaces are an exception — they render within the content and do not compete with overlay surfaces
Example: If both a modal (priority 15) and a sticky notification (priority 10) are eligible, only the modal appears.
Best practice: Set subscription-focused surfaces at priority 15-20, registration surfaces at 10-15, and informational surfaces at 1-10.
11. Coupon Integration — Promo Codes on Surfaces
Surfaces can be linked to a discount coupon from the platform's coupon system.
What Happens When a Coupon Is Linked
- Display: The surface shows the coupon code prominently with a "Copy Code" button
- Copy tracking: When the reader clicks "Copy Code," a
coupon_copiedevent is recorded in both the internal analytics and GA4 - Auto-apply at checkout: If the surface CTA links to a checkout or subscribe page, the coupon code is automatically appended to the URL as
?coupon=CODE. The checkout page reads this parameter and auto-fills the promo code input field, applying the discount without the reader needing to type anything. - Validation: The eligible API checks that the coupon is:
- Currently active (
isActive = true) - Not expired (
validTo >= noworvalidTois null) - Not exhausted (
usedCount < maxUses) - If any check fails, the coupon is NOT shown to the reader
- Currently active (
Example Flow
- Admin creates a surface with a coupon "SPRING20" (20% off) linked to the Premium Annual plan
- Reader visits an article, scrolls to 50%, and the modal appears: "Subscribe and save 20%! Use code SPRING20"
- Reader clicks "Copy Code" — code is copied to clipboard,
coupon_copiedGA4 event fires - Reader clicks "Subscribe Now" — redirected to
/subscribe/checkout?plan=premium-annual&coupon=SPRING20 - Checkout page auto-fills the promo code field with "SPRING20" and shows the discounted price
12. Entity Linking — Connecting Surfaces to Platform Entities
Surfaces can be linked to existing platform entities to provide context and ensure data consistency:
| Entity | Field | Purpose | Example |
|---|---|---|---|
| Subscription Plan | planId | Links to a specific subscription plan; plan details shown on surface | Premium Annual plan |
| Discount Coupon | couponId | Links to a promo code; shown on surface with copy button | "SPRING20" coupon |
| Campaign | campaignId | Links to a marketing campaign; CTA links to campaign page | Literary Festival campaign |
| Event | eventId | Links to an event; CTA links to event RSVP | Author Reading event |
| Product | productId | Links to a shop product; CTA links to product page | New novel in bookstore |
| Newsletter | newsletterSlug | Links to a newsletter edition | Weekly fiction digest |
Important: When linked entities become inactive (e.g., a plan is deactivated, a coupon expires), the eligible API automatically excludes them from reader-facing responses.
13. Analytics & Tracking — Measuring Surface Performance

13.1 Internal Analytics (ConversionEvent Table)
Every interaction is recorded as a ConversionEvent with:
- Event type: impression, click, dismiss, conversion, coupon_copied
- Reader context: readerId (if authenticated), sessionId
- Page context: pageUrl, pageType, deviceType
- UTM context: utmSource, utmMedium, utmCampaign
- Metadata: trigger type, scroll percentage, and other event-specific data
13.2 Denormalized Counters
For quick access, each surface maintains running totals:
impressionCount— total times the surface was displayedclickCount— total CTA clicksdismissCount— total times the surface was dismissedconversionCount— total successful conversions
These counters are updated atomically when events are recorded.
13.3 GA4 Events
The system fires the following Google Analytics 4 events:
| GA4 Event | When Fired | Parameters |
|---|---|---|
conversion_surface_impression | Surface is displayed to a reader | surface_id, surface_type, conversion_goal, trigger_type, page_type |
conversion_surface_click | Reader clicks the primary CTA | surface_id, surface_type, conversion_goal, cta_url |
conversion_surface_dismiss | Reader closes/dismisses the surface | surface_id, surface_type, conversion_goal |
conversion_surface_conversion | Reader completes the desired action | surface_id, surface_type, conversion_goal |
coupon_copied | Reader copies the coupon code | surface_id, coupon_code |
13.4 Admin Stats Dashboard
The admin can view detailed analytics for each surface:
- Totals: Impressions, clicks, dismissals, conversions, coupons copied
- Rates: CTR (click-through rate), dismiss rate, conversion rate, engagement rate
- Time series: Daily/weekly/monthly breakdown of all metrics
- Breakdown by device: Desktop, mobile, tablet splits
- Breakdown by page type: Which pages drive the most conversions
- Breakdown by UTM source: Which traffic sources convert best
14. UTM Attribution — Tracking Marketing Source
All conversion surface CTA URLs are automatically enriched with UTM parameters:
| Parameter | Value | Purpose |
|---|---|---|
utm_source | conversion_surface | Identifies the traffic source as a conversion surface |
utm_medium | promo | Identifies the medium as promotional |
utm_campaign | cs_{goal}_{surfaceId} | Identifies the specific surface and its goal |
Rules:
- UTM params are only added to internal URLs (relative paths or same-origin URLs)
- If the URL already has UTM parameters, they are NOT overwritten
- This allows GA4 to attribute downstream conversions (subscriptions, registrations) back to the specific surface that drove them
15. Reader Portal Rendering — How It All Works Together
15.1 Architecture
RootLayout
|
+-- GlobalConversionSurfaces
|
+-- ConversionSurfaceProvider (fetches eligible surfaces)
|
+-- ConversionSurfaceRenderer (evaluates triggers, renders surfaces)
|
+-- ModalSurface
+-- StickyNotificationSurface
+-- SlideInSurface
+-- BannerSurface (top or bottom)
+-- InlineEmbedSurface15.2 Rendering Flow
- Page navigation detected:
GlobalConversionSurfacesdetects the current page type from the URL - Eligibility fetch:
ConversionSurfaceProvidercalls the eligible API with page context (page type, URL, device type, session ID) - Server-side filtering: The API evaluates all active surfaces against targeting rules and returns eligible ones, sorted by priority
- Client-side trigger evaluation:
ConversionSurfaceRendererlistens for the configured trigger event (scroll, time, exit intent, etc.) - Frequency check: Before displaying, the renderer checks localStorage for frequency caps and suppression state
- Surface display: The highest-priority eligible surface is rendered using the appropriate surface component
- Event tracking: Impression event is recorded (both internal API and GA4)
- Reader interaction: Click, dismiss, or conversion events are tracked as the reader interacts
15.3 Page Suppression
Conversion surfaces are automatically suppressed (never shown) on:
| Page Type | Reason |
|---|---|
Auth pages (/login, /register) | Reader is already in an auth flow |
Account pages (/account/*) | Reader is already logged in and managing their account |
Subscribe pages (/subscribe/*) | Reader is already in the subscription flow |
| Maintenance page | Site is in maintenance mode |
| Preview mode | Admin is previewing content |
16. Admin Console — Managing Surfaces
16.1 Navigation
Admin Console > Marketing > Conversion Surfaces
16.2 List Page
Shows all conversion surfaces with:
- Name, type (icon), goal, status badge
- Priority, impression/click/conversion counts
- Date range (starts at - ends at)
- Filter by: status, surface type, conversion goal
- Sort by: name, status, priority, impressions, clicks, conversions, created date
- Search by name
- Actions: Edit, Duplicate, Archive
16.3 Create/Edit Form
The form is organized into 7 sections:
Section 1: Basic Information
- Name (internal label, not shown to readers)
- Surface Type (modal, banner, etc.)
- Conversion Goal
- Priority (1-100, higher = shown first)
Section 2: Content
- Title (required, shown to reader)
- Subtitle (optional)
- Body Text (optional, rich text)
- Image URL (optional)
- Background Color (optional)
- Theme (light, dark, brand)
Section 3: Call-to-Action
- Primary CTA Label (required, e.g., "Subscribe Now")
- Primary CTA URL (required, e.g., "/subscribe")
- Secondary CTA Label (optional, e.g., "Maybe Later")
- Secondary CTA URL (optional, or auto-dismiss action)
Section 4: Entity Links
- Linked Coupon (dropdown of active coupons)
- Linked Subscription Plan (dropdown of active plans)
- Linked Campaign, Event, Product, Newsletter (optional)
Section 5: Targeting
- Audience segments (multi-select)
- Page types (multi-select)
- Device types (multi-select)
- Content types, sections, tags (advanced)
- URL include/exclude patterns (advanced)
- UTM source/medium/campaign filters (advanced)
Section 6: Trigger
- Trigger type (dropdown)
- Trigger configuration (delay seconds, scroll %, page view count)
Section 7: Frequency & Scheduling
- Frequency limit + unit
- Suppress after dismiss (days)
- Suppress after conversion (boolean)
- Suppress if already converted (boolean)
- Start date (optional, for scheduling)
- End date (optional, for auto-expiration)
16.4 Permissions (RBAC)
| Permission | Actions |
|---|---|
MARKETING_CONVERSION_SURFACES_READ | View list, view details, view stats |
MARKETING_CONVERSION_SURFACES_CREATE | Create new surfaces, seed templates |
MARKETING_CONVERSION_SURFACES_UPDATE | Edit surfaces, change status |
MARKETING_CONVERSION_SURFACES_DELETE | Archive surfaces |
17. Pre-Built Templates — Getting Started Quickly
The system includes 15 pre-built template surfaces covering all conversion goals. These are seeded via the /api/marketing/conversion-surfaces/seed endpoint and created in draft status so admins can customize them before activating.
| # | Template Name | Type | Goal | Trigger |
|---|---|---|---|---|
| 1 | Registration -- Exit Intent Modal | Modal | register | exit_intent |
| 2 | Registration -- After 3 Page Views Banner | Top Banner | register | page_view_count (3) |
| 3 | Subscribe -- After Article Read Modal | Modal | subscribe_paid | after_article_read |
| 4 | Subscribe -- Pricing Page Sticky | Sticky | visit_pricing | time_on_page (10s) |
| 5 | Subscribe -- 50% Scroll Slide-In | Slide-In | subscribe_paid | scroll_depth (50%) |
| 6 | Newsletter -- Bottom Banner | Bottom Banner | subscribe_newsletter | page_load |
| 7 | Newsletter -- Article Inline Embed | Inline Embed | subscribe_newsletter | (inline) |
| 8 | Campaign -- Event Promo Modal | Modal | view_campaign | time_on_page (15s) |
| 9 | Campaign -- Sticky Notification | Sticky | view_event | page_load |
| 10 | Coupon -- Limited Time Offer Modal | Modal | redeem_coupon | time_on_page (20s) |
| 11 | Coupon -- Flash Sale Banner | Top Banner | redeem_coupon | page_load |
| 12 | Product -- Book Promo Sticky | Sticky | view_product | scroll_depth (30%) |
| 13 | Pricing -- Comparison Prompt | Modal | visit_pricing | after_article_read |
| 14 | Login -- Returning Visitor Prompt | Sticky | login | page_view_count (2) |
| 15 | Custom CTA -- Generic Promo | Slide-In | custom_cta | scroll_depth (40%) |
18. Page Suppression — Where Surfaces Never Appear
| Suppressed Pages | Reason |
|---|---|
/login, /register | Reader is already in auth flow — showing a "register" surface here would be redundant and confusing |
/account/* | Reader is logged in and managing their account — surfaces would be intrusive |
/subscribe/* | Reader is already in the subscription flow — competing CTAs would disrupt checkout |
/maintenance | Site is under maintenance |
19. User Stories
For Marketing Team
| ID | As a... | I want to... | So that... |
|---|---|---|---|
| US-01 | Marketing manager | Create a modal that appears when anonymous readers try to leave the site | I can capture potential readers before they bounce |
| US-02 | Marketing manager | Schedule a promotional banner to run during our spring sale (Mar 15 - Apr 1) | The promotion runs automatically without manual activation |
| US-03 | Marketing manager | See how many readers clicked vs dismissed each surface | I can optimize surface content and timing |
| US-04 | Marketing manager | Link a 20% discount coupon to a subscription surface | Readers see the discount and it auto-applies at checkout |
| US-05 | Marketing manager | Target a surface only to readers coming from our Facebook campaign | I can create a consistent experience from ad to conversion |
| US-06 | Marketing manager | Duplicate a high-performing surface and tweak it for a new campaign | I can iterate quickly without recreating everything from scratch |
| US-07 | Marketing manager | Pause a surface temporarily during a site event | The surface doesn't interfere with the event experience |
| US-08 | Marketing manager | See which device types and traffic sources drive the most conversions | I can allocate marketing budget more effectively |
For Readers (Implicit)
| ID | As a... | I want to... | So that... |
|---|---|---|---|
| US-09 | Anonymous reader | Not see the same pop-up on every page | My reading experience isn't annoying |
| US-10 | Anonymous reader | Be able to dismiss a surface and not see it again for a while | I feel in control of my experience |
| US-11 | Registered reader | Not see "Create Account" prompts after I've already registered | The site recognizes who I am |
| US-12 | Subscriber | Not see subscription prompts at all | I'm not being asked to buy something I already have |
| US-13 | Mobile reader | See surfaces that work well on my phone screen | The promotion doesn't break my reading experience |
| US-14 | Reader with a coupon link | Have the discount auto-apply at checkout | I don't have to remember or type the code |
For Editors/Content Team
| ID | As a... | I want to... | So that... |
|---|---|---|---|
| US-15 | Editor | Promote our new poetry anthology to readers browsing the poetry section | The promotion reaches readers most likely to be interested |
| US-16 | Editor | Show an inline embed about our literary event within article pages | The promotion feels editorial, not like an ad |
20. Use Cases & Scenarios
Scenario 1: Converting Anonymous Visitors to Registered Readers
Setup: Create a modal surface with:
- Goal:
register - Audience:
anonymousonly - Trigger:
exit_intent(desktop) /scroll_depth 75%(mobile fallback) - Frequency: Once per day
- Suppress after dismiss: 3 days
- Suppress if converted: Yes
Reader experience:
- Anonymous reader visits the site and reads an article
- When they move their mouse to close the browser tab, the modal appears
- "Create your free account — Save articles, get personalized recommendations"
- If they register, the modal never appears again
- If they dismiss, it won't reappear for 3 days
Scenario 2: Subscription Conversion with Discount
Setup: Create a slide-in surface with:
- Goal:
subscribe_paid - Audience:
anonymous,registered - Trigger:
after_article_read - Linked coupon: "SAVE30" (30% off first year)
- Linked plan: Premium Annual
- Frequency: Once per week
- Suppress if converted: Yes
Reader experience:
- Reader finishes reading an article
- Slide-in appears: "Enjoyed this? Subscribe and save 30% — Use code SAVE30"
- Reader clicks "Copy Code" — code copied, GA4
coupon_copiedevent fires - Reader clicks "Subscribe Now" — redirected to
/subscribe/checkout?plan=premium-annual&coupon=SAVE30 - Checkout page auto-fills "SAVE30" in the promo code field
- Price updates to show 30% discount
Scenario 3: Time-Limited Campaign Promotion
Setup: Create a top banner surface with:
- Goal:
view_campaign - Audience: All
- Trigger:
page_load - Schedule: March 15 to April 1
- Linked campaign: "Spring Literary Festival"
- Frequency: Once per session
- Priority: 25 (high, to override other surfaces)
Lifecycle:
- Admin creates the surface on March 10 with status
draft - Admin reviews content and sets status to
scheduledwith startsAt = March 15 - On March 15, the cron job detects startsAt has passed and sets status to
active - Readers see the banner: "Spring Literary Festival starts now! Join 20+ sessions with leading authors"
- On April 1, the cron job detects endsAt has passed and sets status to
expired - Banner stops appearing automatically
Scenario 4: Newsletter Growth via Inline Embed
Setup: Create an inline embed surface with:
- Goal:
subscribe_newsletter - Audience:
newsletter_unsubscribed - Page types:
articleonly - Trigger: N/A (inline embeds render with the page)
- Frequency: Once per day
Reader experience:
- Reader opens an article
- After the article content, between the article body and related content, an inline card appears
- "Enjoy stories like this? Get our weekly fiction digest in your inbox"
- Reader clicks "Subscribe" — newsletter sign-up flow triggers
- The embed doesn't appear again for 24 hours
Scenario 5: UTM-Targeted Social Media Promotion
Setup: Create a sticky notification with:
- Goal:
subscribe_paid - Audience:
anonymous,registered - UTM targeting:
utmSource: ["facebook", "instagram"] - Trigger:
time_on_page(15 seconds) - Linked coupon: "SOCIAL25" (25% off)
Reader experience:
- Reader arrives from a Facebook ad (
?utm_source=facebook) - After 15 seconds of reading, a small sticky notification appears in the bottom-right
- "Welcome from Facebook! Get 25% off your subscription — SOCIAL25"
- Reader clicks "Copy Code" and proceeds to subscribe
- Readers arriving from Google, email, or direct visits never see this surface
| # | Scenario | Expected Result |
|---|---|---|
| A1 | Create surface with all required fields only | Surface created in draft status |
| A2 | Create surface with all fields filled | All fields persisted correctly |
| A3 | Attempt to create surface without required fields | 400 error with validation details |
| A4 | Edit surface — change title and priority | Updates saved, updatedBy recorded |
| A5 | Duplicate surface | New surface created with "(Copy)" suffix, stats reset to 0, status = draft |
| A6 | Archive surface | Status set to archived, no longer visible to readers |
| A7 | Attempt invalid status transition (archived -> active) | 400 error: "Invalid status transition" |
| A8 | Activate surface (draft -> active) | Surface starts appearing to eligible readers |
| A9 | Pause active surface | Surface stops appearing immediately |
| A10 | Resume paused surface | Surface starts appearing again |
| A11 | Create surface with linked coupon | Coupon details visible on surface |
| A12 | Seed template surfaces | 15 draft surfaces created (skips existing names) |
| # | Scenario | Expected Result |
|---|---|---|
| B1 | Schedule surface with future startsAt | Status = scheduled, not visible to readers |
| B2 | Wait for cron after startsAt passes | Status auto-transitions to active within ~15 minutes |
| B3 | Set endsAt on active surface, wait for cron | Status auto-transitions to expired |
| B4 | Activate surface without startsAt | Immediately active |
| B5 | Create surface with startsAt in the past | Should activate on next cron run |
| # | Scenario | Expected Result |
|---|---|---|
| C1 | Target audience: anonymous only | Not visible to logged-in readers |
| C2 | Target audience: registered only | Not visible to anonymous visitors or subscribers |
| C3 | Target audience: subscriber only | Not visible to anonymous or non-subscriber readers |
| C4 | Target page type: article only | Not visible on homepage, section pages, etc. |
| C5 | Target device: desktop only | Not visible on mobile or tablet |
| C6 | Target UTM source: facebook | Not visible to direct visitors or Google traffic |
| C7 | Exclude URL: /account/* | Not visible on any account page |
| C8 | Empty targeting rules (all fields empty) | Visible to ALL audiences, pages, devices |
| C9 | Target content section: fiction | Only appears on fiction article pages |
| # | Scenario | Expected Result |
|---|---|---|
| D1 | Trigger: page_load | Surface appears immediately on page load |
| D2 | Trigger: time_on_page (30s) | Surface appears exactly after 30 seconds |
| D3 | Trigger: scroll_depth (50%) | Surface appears when reader scrolls past 50% of page |
| D4 | Trigger: exit_intent (desktop) | Surface appears when mouse moves toward browser close/back area |
| D5 | Trigger: page_view_count (3) | Surface appears on the 3rd page view in session |
| D6 | Trigger: after_article_read | Surface appears after reader reaches ~90% scroll + has spent time |
| D7 | Exit intent on mobile | Fallback to scroll-depth trigger |
| # | Scenario | Expected Result |
|---|---|---|
| E1 | Frequency: 1/session | Surface appears once, then not again in same session |
| E2 | Frequency: 1/day | Surface appears once, then not again for 24 hours |
| E3 | Frequency: 3/week | Surface appears up to 3 times in 7 days |
| E4 | Dismiss then revisit (suppressAfterDismiss: 7) | Surface does not reappear for 7 days |
| E5 | Click CTA (suppressAfterConversion: true) | Surface never appears again for this reader |
| E6 | Already registered, surface goal=register (suppressIfConverted: true) | Surface never appears |
| E7 | Clear localStorage | Frequency state reset for anonymous readers |
| # | Scenario | Expected Result |
|---|---|---|
| F1 | Surface with active coupon | Coupon code displayed, "Copy Code" button visible |
| F2 | Click "Copy Code" | Code copied to clipboard, coupon_copied event recorded |
| F3 | Click CTA with coupon linked | URL has ?coupon=CODE appended |
| F4 | Arrive at checkout from surface CTA | Promo code field auto-filled with coupon code |
| F5 | Coupon expired | Surface shows without coupon (coupon filtered out) |
| F6 | Coupon exhausted (max uses reached) | Surface shows without coupon |
| F7 | Coupon inactive | Surface shows without coupon |
| # | Scenario | Expected Result |
|---|---|---|
| G1 | Two eligible surfaces with different priorities | Higher priority surface shown |
| G2 | Two eligible surfaces with same priority | More recently created surface shown |
| G3 | Modal (overlay) + Inline Embed both eligible | Both shown — inline does not conflict with overlay |
| G4 | Two modals both eligible | Only the higher-priority modal shown |
| # | Scenario | Expected Result |
|---|---|---|
| H1 | Surface impression | impressionCount incremented, GA4 impression event fired |
| H2 | CTA click | clickCount incremented, GA4 click event fired |
| H3 | Surface dismissed | dismissCount incremented, GA4 dismiss event fired |
| H4 | Conversion completed | conversionCount incremented, GA4 conversion event fired |
| H5 | Coupon copied | coupon_copied event recorded (internal + GA4) |
| H6 | View stats in admin | All metrics, rates, time-series, and breakdowns display correctly |
| H7 | UTM params on CTA URL | CTA URL contains utm_source, utm_medium, utm_campaign |
| H8 | Existing UTM params not overwritten | If URL already has UTM, surface UTM is NOT added |
| # | Scenario | Expected Result |
|---|---|---|
| I1 | Modal on desktop | Centered card with backdrop blur, X button, Escape key works |
| I2 | Modal on mobile | Bottom sheet slide-up |
| I3 | Sticky notification | Small card in configured corner, X to dismiss |
| I4 | Slide-in panel | Full-height panel from right, backdrop click to close |
| I5 | Top banner | Full-width strip at top, X to dismiss |
| I6 | Bottom banner | Full-width strip at bottom, X to dismiss |
| I7 | Inline embed | Card within page content flow, no dismiss button |
| I8 | Surface on /login page | NOT displayed (suppressed page) |
| I9 | Surface on /account page | NOT displayed (suppressed page) |
| I10 | Surface on /subscribe page | NOT displayed (suppressed page) |
| # | Scenario | Expected Result |
|---|---|---|
| J1 | No eligible surfaces | Nothing displayed, no console errors |
| J2 | API failure (network error) | Graceful degradation, no surface shown, no UI crash |
| J3 | Surface deleted while reader is on page | Already-displayed surface continues; next page load doesn't show it |
| J4 | Reader logs in after seeing anonymous surface | Next page load respects new audience state |
| J5 | Multiple rapid page navigations | No duplicate surfaces or stacking |
| J6 | Browser back button after dismissal | Suppression state preserved |
| Limitation | Description | Workaround |
|---|---|---|
| No A/B testing | Cannot split-test two versions of the same surface | Create two surfaces with same targeting but different priorities; rotate manually |
| No geographic targeting | Cannot target by reader's country or region | Use UTM targeting to segment traffic sources |
| No admin preview | No way to preview exactly how a surface looks on the reader portal from the admin console | Activate in draft on staging environment, or use low-priority test surface |
| Cron lag (up to 15 min) | Scheduled start/end times execute within ~15 minute windows | For exact-time activations, manually change status |
| Exit intent is desktop-only | No reliable exit intent signal on mobile browsers | System falls back to scroll_depth on mobile |
| Anonymous frequency is client-side | Clearing localStorage resets frequency caps for anonymous readers | Server-side tracking only available for authenticated readers |
| Single surface per overlay type | Only one modal/banner/slide-in/sticky per page load | Use priority to ensure the most important one wins |
| Term | Definition |
|---|---|
| CTA | Call to Action — a button or link prompting the reader to take an action |
| CTR | Click-Through Rate — percentage of impressions that result in a CTA click |
| Conversion Rate | Percentage of impressions that result in a completed conversion |
| Dismiss Rate | Percentage of impressions that result in the reader closing the surface |
| Engagement Rate | (Clicks + Conversions) / Impressions |
| Frequency Cap | Maximum number of times a surface is shown within a time window |
| GA4 | Google Analytics 4 — the analytics platform used for tracking reader behavior |
| ISR | Incremental Static Regeneration — Next.js caching mechanism |
| RBAC | Role-Based Access Control — permission system for admin users |
| Session | A single browser visit; ends when the browser tab is closed or after 30 minutes of inactivity |
| UTM | Urchin Tracking Module — URL parameters used to track marketing campaign traffic sources |