# Lead Capture Trigger Configuration Guide


**Last Updated:** 2026-03-28

## Overview

The lead capture popup uses an intelligent trigger system that adapts to different page types and user behavior. This guide explains how triggers work and how to customize them for specific pages.

## Blocking overlays (automatic triggers)

**Goal:** The callback popup must not open **on top of** other modals (demo/booking, template download, tool export email, enterprise/add-on, mobile nav). Automatic time, scroll, and exit-intent triggers **pause** while a blocking overlay is open and **resume fairly** (elapsed time excludes overlay duration).

**Implementation:**

| Piece | Role |
|-------|------|
| [`v2/js/ordio-lead-capture-blocking-registry.js`](../../v2/js/ordio-lead-capture-blocking-registry.js) | Loaded in [`v2/base/head.php`](../../v2/base/head.php) before Alpine. Exposes `window.ordioLeadCaptureBlocking`: `register(sourceId)`, `unregister(sourceId)`, `isBlocking()`, `getEffectiveElapsedMs(pageLoadTime)`, `resetTimerBaseline()`. Ref-counted: first `register` starts the pause, last `unregister` accumulates blocked ms. Dispatches `ordio:lead-capture-blocking` on transitions. Also sets `body[data-ordio-blocking-overlay]` and calls `HubSpotConversations.widget.close()` when the first overlay opens so the HubSpot chat iframe stays hidden behind primary modals (see below). |
| [`v2/js/lead-capture-triggers.js`](../../v2/js/lead-capture-triggers.js) | Time triggers use polling + effective elapsed ms (not a single `setTimeout`). Scroll/exit paths and `trigger()` bail out while `isBlocking()`. Exit-intent and analytics time-on-page use `getEffectiveTimeOnPageMs()`. |
| Alpine `Alpine.store('modal')` | `open` / `close` → `conversion-modal`; `openMobileNav` / `closeMobileNav` → `mobile-nav`. |
| Template modal | [`templates_template.php`](../../v2/pages/templates_template.php): `x-effect` on `showHubSpotForm` → `template-hubspot-download`. |
| Tools export modals | `x-effect` on export overlays (`tools-export-overlay`) or vanilla `register`/`unregister` in Mehrwertsteuer / Arbeitstage export helpers. |
| Enterprise / pricing add-ons | [`enterprise-box.php`](../../v2/sections/enterprise-box.php), [`pricing-addons.php`](../../v2/sections/pricing-addons.php). |
| Lead capture callback popup | [`lead-capture-popup.php`](../../v2/components/lead-capture-popup.php): `register`/`unregister` with source id `lead-capture-popup` when the overlay opens/closes (idempotent if `show()` is called while already open). |

### HubSpot chat during blocking overlays

Conversion and other modals often use a lower stacking order than the HubSpot “Customer Success” chat widget, so the chat could stay visible and interactive on top of a dimmed backdrop. While `ordioLeadCaptureBlocking` ref-count is greater than zero:

- `body` has `data-ordio-blocking-overlay` (empty attribute).
- [`v2/base/head.php`](../../v2/base/head.php) applies the same `#hubspot-messages-iframe-container { display: none !important; }` rule used for mobile nav, so the chat is not shown and cannot steal focus.
- The registry attempts `HubSpotConversations.widget.close()` once when the first blocking overlay opens (defensive collapse before hide).

**Do not register** for **embedded** lead forms (e.g. inline forms on `/download/...` gated LPs) — only **overlay** UIs.

**Manual triggers** (`manual-callback`, footer Rückruf) are unchanged: they do not go through the automatic `trigger()` blocking guard.

**QA checklist**

1. Homepage: open “Kostenlos testen” before the time trigger fires; confirm the callback popup does not auto-open until the demo modal is closed and the remaining delay has passed.
2. Template `/vorlagen/...`: open the download modal; same behavior.
3. After closing the overlay, scroll/exit-intent still work if the popup has not yet been shown.

## Trigger Types

### 1. Time-Based Trigger

**How it works:** Popup appears after user spends a certain amount of time on page.

**Default Delays:**
- **High Priority Pages:** 30 seconds (homepage, pricing, comparison, product)
- **Medium Priority Pages:** 30 seconds (industry, templates, downloads)
- **Low Priority Pages:** 60 seconds — *Only where standard automatic triggers are scheduled. Live URLs under `/tools` do **not** use automatic time/scroll/exit-intent triggers (manual CTA only; see [Page Exclusions](#page-exclusions)).*
- **Blog Posts:** 30 seconds (optimized for reading)

**Configuration:**
```javascript
// Custom time trigger
window.leadCaptureTriggers.addTrigger('custom-time', {
    condition: () => {
        return !window.leadCaptureTriggers.hasShown;
    },
    delay: 45000, // 45 seconds in milliseconds
    once: true // Only trigger once
});
```

### 2. Scroll-Based Trigger

**How it works:** Popup appears when user scrolls past a certain percentage of page.

**Default Thresholds:**
- **Comparison Pages:** 25% (lower threshold for shorter pages)
- **Blog Posts:** 35% (optimized for longer content)
- **Standard Pages:** 40% (default for most pages)

**Configuration:**
```javascript
// Custom scroll trigger
window.leadCaptureTriggers.addTrigger('custom-scroll', {
    condition: () => {
        return !window.leadCaptureTriggers.hasShown;
    },
    threshold: 0.5, // 50% scroll
    once: true
});
```

### 3. Exit-Intent Trigger

**How it works:** Popup appears when user shows intent to leave page.

**Desktop:** Mouse cursor leaves viewport (moves to top)
**Mobile:** Rapid upward scrolling (100px+ at once)

**Minimum Time Requirements:**
- **High Priority:** 5 seconds on page
- **Medium Priority:** 8 seconds on page
- **Low Priority:** 10 seconds on page — *Applies to pages that use the low-priority exit-intent branch; `/tools` URLs skip automatic exit-intent entirely.*
- **Blog Posts:** 15 seconds on page

**Minimum Scroll Requirements:**
- **High Priority:** 5% scrolled
- **Medium Priority:** 10% scrolled
- **Low Priority:** 20% scrolled
- **Blog Posts:** 25% scrolled

**Configuration:**
```javascript
// Custom exit intent trigger (use effective time so overlays don't skew thresholds)
window.leadCaptureTriggers.addTrigger('custom-exit-intent', {
    condition: () => {
        const timeOnPage = window.leadCaptureTriggers.getEffectiveTimeOnPageMs();
        const scrollPercent = window.leadCaptureTriggers.getScrollPercentage();
        return timeOnPage > 10000 && scrollPercent > 0.15 && !window.leadCaptureTriggers.hasShown;
    },
    once: true
});
```

### 4. Manual Trigger

**How it works:** Popup appears when explicitly triggered via JavaScript.

**Usage:**
```javascript
// Standard manual trigger (respects submission restrictions)
window.leadCapturePopup.show('manual');

// Manual callback trigger (bypasses submission restrictions)
window.leadCapturePopup.show('manual-callback');
```

**Common Use Cases:**
- CTA buttons ("Rückruf anfordern")
- Form abandonment recovery
- Special promotions
- A/B testing

## Page Exclusions

Some pages have **no automatic triggers** – the popup only opens via explicit CTAs (e.g. footer **Rückruf anfordern** / `showLeadCapturePopup()`).

| Path | Reason |
|------|--------|
| `/einladung` | Cello referral landing page – primary CTA is email capture; callback popup would distract from conversion. |
| `/kostenlos-testen` | Trial signup focus – manual "Rückruf anfordern" only. |
| `/tools`, `/tools/`, `/tools/*` | Calculator and tools hub – task-focused flows; avoid interrupting users with timed/scroll/exit-intent popups. Popup still loads with full copy detection; use manual trigger only. |

**Implementation:** `v2/js/lead-capture-triggers.js` returns early from `setupTriggers()` when `currentPage === '/einladung'` or `currentPage === '/kostenlos-testen'`, or when `isToolsPath()` is true (`/^\/tools(\/|$)/` on `pathname`).

### Analytics note

On `/tools` routes, automatic scheduling no longer fires `time`, `scroll-trigger`, or `exit-intent` trigger flows, so analytics that segment by **automatic** trigger type will show fewer such events on tools. Opens from **manual** CTAs (`manual-callback`) are unchanged. Flag for marketing if dashboards compare trigger mix before/after this change.

## Page Priority System

The trigger system automatically adjusts behavior based on page priority:

### High Priority Pages

**Examples:** Homepage, pricing, comparison, product pages

**Trigger Behavior:**
- Time delay: 30 seconds
- Scroll threshold: 25-40% (varies by page type)
- Exit intent: 5 seconds minimum, 5% scroll minimum

**Detection:**
```javascript
// Automatically detected by URL patterns:
// - /preise, /pricing, static_pricing_new.php (canonical /preise)
// - compare_*.php, /alternativen/
// - /schichtplan, /zeiterfassung, /payroll
```

### Medium Priority Pages

**Examples:** Industry pages, templates, downloads

**Trigger Behavior:**
- Time delay: 30 seconds
- Scroll threshold: 40%
- Exit intent: 8 seconds minimum, 10% scroll minimum

**Detection:**
```javascript
// Automatically detected by URL patterns:
// - /branchen/, industry_*.php
// - templates_*.php
// - download_*.php
```

### Low Priority Pages

**Examples:** Historically “tools” in the priority matrix; **production** canonical URLs under `/tools` **skip** `setupStandardTriggers()` entirely (see [Page Exclusions](#page-exclusions)). Other pages are rarely classified as `low` today.

**Trigger Behavior (when standard triggers run on a `low`-priority path):**
- Time delay: 60 seconds
- Scroll threshold: 40%
- Exit intent: 10 seconds minimum, 20% scroll minimum
- **Note:** Mobile rapid-scroll exit intent is not used on low-priority pages

**Detection in code:** `getPagePriority()` returns `"low"` when `isToolsPath()` matches (`/^\/tools(\/|$)/`). Automatic triggers are **not** started on those paths because `setupTriggers()` returns first.

### Blog Posts

**Examples:** Insights, ratgeber, lexikon articles

**Trigger Behavior:**
- Time delay: 30 seconds (gives readers time to engage)
- Scroll threshold: 35% (lower for longer content)
- Exit intent: 15 seconds minimum, 25% scroll minimum (gentler)

**Detection:**
```javascript
// Automatically detected by URL patterns:
// - /insights/, /ratgeber/, /lexikon/
```

## Custom Trigger Configuration

### Override Default Triggers

```javascript
// After page load, modify triggers
document.addEventListener('DOMContentLoaded', function() {
    if (window.leadCaptureTriggers) {
        // Remove default time trigger
        window.leadCaptureTriggers.triggers.delete('time');
        
        // Add custom time trigger
        window.leadCaptureTriggers.addTrigger('time', {
            condition: () => {
                return !window.leadCaptureTriggers.hasShown;
            },
            delay: 60000, // 60 seconds
            once: true
        });
        
        // Modify scroll threshold
        const scrollTrigger = window.leadCaptureTriggers.triggers.get('scroll-trigger');
        if (scrollTrigger) {
            scrollTrigger.threshold = 0.3; // 30%
        }
    }
});
```

### Disable Specific Triggers

```javascript
// Disable exit intent trigger
if (window.leadCaptureTriggers) {
    window.leadCaptureTriggers.triggers.delete('exit-intent');
}
```

### Add Custom Triggers

```javascript
// Add custom trigger based on user interaction
window.leadCaptureTriggers.addTrigger('button-hover', {
    condition: () => {
        // Check if user hovers over specific button
        const button = document.querySelector('.cta-button');
        return button && button.matches(':hover') && !window.leadCaptureTriggers.hasShown;
    },
    once: true
});

// Monitor button hover
document.querySelector('.cta-button')?.addEventListener('mouseenter', () => {
    window.leadCaptureTriggers.trigger('button-hover');
});
```

## Gated Content (ShiftOps Report)

For pages with gated content, triggers are disabled until content is unlocked:

```javascript
// Triggers disabled until unlock event
if (window.leadCaptureTriggers) {
    // Wait for unlock event
    document.addEventListener('shiftops:report-unlocked', () => {
        window.leadCaptureTriggers.enableAfterUnlock();
    }, { once: true });
    
    // Or check if already unlocked
    if (window.reportUnlocked) {
        window.leadCaptureTriggers.enableAfterUnlock();
    }
}
```

## Trigger Priority

If multiple triggers fire simultaneously, only the first one shows the popup:

1. **Manual triggers** (highest priority)
2. **Time triggers**
3. **Scroll triggers**
4. **Exit-intent triggers** (lowest priority)

## Session Management

### Preventing Multiple Popups

The system automatically prevents multiple popups:
- **Per Page:** Only one popup per page (tracked in sessionStorage)
- **Per Session:** Only one popup per session (tracked in sessionStorage)
- **After Submission:** No popup for 30 days (tracked in localStorage)

### Bypassing Restrictions

Manual callback triggers bypass submission restrictions:

```javascript
// This will show popup even if form was previously submitted
window.leadCapturePopup.show('manual-callback');
```

## Debugging Triggers

### Check Trigger Status

```javascript
// Check if triggers are active
console.log('Triggers:', window.leadCaptureTriggers.triggers);
console.log('Has shown:', window.leadCaptureTriggers.hasShown);
console.log('Page priority:', window.leadCaptureTriggers.getPagePriority());
```

### Test Triggers Manually

```javascript
// Manually trigger popup
window.leadCapturePopup.show('manual');

// Reset trigger state (for testing)
window.resetLeadCapture();
```

### Monitor Trigger Events

```javascript
// Add event listener to track trigger firing
const originalTrigger = window.leadCaptureTriggers.trigger.bind(window.leadCaptureTriggers);
window.leadCaptureTriggers.trigger = function(triggerName) {
    console.log('Trigger fired:', triggerName);
    return originalTrigger(triggerName);
};
```

## Best Practices

1. **Respect User Experience:** Don't trigger too aggressively
2. **Test Thoroughly:** Test triggers on different page types
3. **Monitor Performance:** Check if triggers affect page performance
4. **Mobile Considerations:** Remember mobile triggers behave differently
5. **Accessibility:** Ensure triggers don't interfere with accessibility
6. **Analytics:** Track which triggers convert best

## Common Patterns

### Delay Popup on High-Intent Pages

```javascript
// Delay popup on pricing pages (users need time to read)
if (window.location.pathname.includes('/preise')) {
    window.leadCaptureTriggers.triggers.delete('time');
    window.leadCaptureTriggers.addTrigger('time', {
        condition: () => !window.leadCaptureTriggers.hasShown,
        delay: 60000, // 60 seconds
        once: true
    });
}
```

### Disable Exit Intent on Mobile

```javascript
// Exit intent can be jarring on mobile
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
    window.leadCaptureTriggers.triggers.delete('exit-intent');
}
```

### Trigger After Specific User Action

```javascript
// Trigger after user interacts with specific element
document.querySelector('.pricing-table')?.addEventListener('click', () => {
    if (!window.leadCaptureTriggers.hasShown) {
        window.leadCapturePopup.show('manual');
    }
});
```

## Related Documentation

- [Architecture Overview](./ARCHITECTURE.md) - System architecture
- [Integration Guide](./INTEGRATION_GUIDE.md) - Adding popup to pages
- [Troubleshooting Guide](./TROUBLESHOOTING.md) - Common issues
- [Testing Checklist](./TESTING_CHECKLIST.md) - Testing triggers

