Skip to content

feat: opt-in conversion layer — GA4 flight analytics, GHL lead capture, ElevenLabs scroll audio - #1

Open
sbyrd-create wants to merge 1 commit into
cth9191:mainfrom
sbyrd-create:conversion-layer
Open

feat: opt-in conversion layer — GA4 flight analytics, GHL lead capture, ElevenLabs scroll audio#1
sbyrd-create wants to merge 1 commit into
cth9191:mainfrom
sbyrd-create:conversion-layer

Conversation

@sbyrd-create

@sbyrd-create sbyrd-create commented Jul 12, 2026

Copy link
Copy Markdown

What

The generated page is currently a beautiful dead end — the finale CTA is href:'#', nothing is measured, nothing is captured. This adds three opt-in, backward-compatible layers to the engine so a shipped scroll-world actually converts. An existing config runs unchanged; every new field defaults off.

1. GA4 flight analytics — analytics:{ ga4, prefix, debug, params }

A scroll-scrubbed page is analytically blind by default (one pageview, no clicks until the end). The engine now emits:

Event When
sw_scene_view a section becomes the active scene
sw_scroll_depth whole-flight depth crosses 25/50/75/100% (once each)
sw_flight_complete visitor reaches the end (once)
sw_cta_click any CTA (finale or top bar)
sw_lead_submit inline capture form posts
sw_audio_toggle sound toggled

Routed to gtag() when present, else pushed to window.dataLayer (GTM Custom Event triggers). ga4:'G-…' auto-injects gtag; ga4:true reuses an existing snippet.

2. Lead capture on the finale — capture:{…}

  • mode:'embed' — drops in a GoHighLevel form/booking iframe (GHL owns submission + routing).
  • mode:'inline' — an on-brand form that POSTs name/email/phone + UTM/gclid/fbclid + source/brand/page to a GHL inbound webhook. Honeypot-guarded, required-field checked, no-cors fire-and-trust, success → sw_lead_submit + thank-you (+ optional redirect).
  • Security rule stated in-code: the webhook URL is the only (public) credential — no GHL API key ever goes client-side.

3. ElevenLabs scroll audio — audio:{…} + per-section audio:'…mp3'

Per-scene narration/ambient whose volume tracks the scene's opacity, so audio cross-fades with the flight (the audio analogue of the frame-locked seams). Muted until an explicit unmute tap (autoplay policy); never instantiated under prefers-reduced-motion / data-saver (preload:'none' → zero bytes for muted visitors).

Docs

  • New references/integrations.md: GA4 event schema + how to read the events as validation gates, GHL setup walk-through (both modes), ElevenLabs encoding recipe, Vercel/framework deploy notes.
  • SKILL.md: interview item 7 (ask for conversion wiring — costs zero generations) and Step 9 (conversion & deploy), plus a Step 8 QA bullet.
  • index-template.html shows the opt-in config; plugin.json 0.2.0 → 0.3.0; FORK-CHANGES §6.

Why opt-in / compat

Same doctrine as the rest of this fork: every new config field is optional, nothing changes for an upstream config, and the engine stays zero-dependency vanilla JS (the analytics/capture/audio code is ~200 lines inside the same IIFE-scope, namespaced sw- CSS).

Test plan

  • node --check on the engine
  • In-browser (Chromium): mount clean with all three blocks configured; no console errors
  • Inline capture: required-field validation → submit → sw_lead_submit fired + thank-you swap; honeypot field present and hidden
  • sw_cta_click (finale CTA), sw_audio_toggle (aria-pressed flips), sw_scene_view on scene change
  • data-sw-seo block still hidden on mount
  • Scroll-depth threshold logic verified deterministically: 25/50/75/100 fire once each + flight_complete once, idempotent under up/down scrolling
  • Base config (no analytics/capture/audio keys) → behavior identical to before; no new DOM, no new requests
  • A live GHL webhook end-to-end (needs a real location; the doc walks through mapping + first-lead verification)

🤖 Generated with Claude Code

… audio)

The generated page is currently a dead end — the finale CTA is href:'#',
nothing measured, nothing captured. Add three backward-compatible, opt-in
layers so a shipped scroll-world actually converts; an existing config runs
unchanged (every new field defaults off).

- Engine (scrub-engine.js):
  - analytics:{ga4,prefix,debug,params} -> emits sw_scene_view / sw_scroll_depth
    (25/50/75/100 once each) / sw_flight_complete / sw_cta_click / sw_lead_submit /
    sw_audio_toggle to gtag() or window.dataLayer (GTM). ga4:'G-…' auto-injects gtag;
    ga4:true reuses an existing snippet.
  - capture:{…} on the finale — mode:'embed' (GHL form/calendar iframe) or
    mode:'inline' (on-brand form -> GHL inbound webhook; honeypot, UTM/source
    passthrough, no-cors fire-and-trust, success -> sw_lead_submit + thank-you).
    No GHL API key client-side; the public webhook URL is the only credential.
  - audio:{…} + per-section audio: volume tracks scene opacity (cross-fades with
    the flight), muted until unmute tap, off under reduced-motion / data-saver
    (preload:'none' -> zero bytes for muted visitors).
- Docs: references/integrations.md (GA4 event schema + validation-gate mapping,
  GHL setup, ElevenLabs encoding, Vercel/framework deploy). SKILL.md: interview
  item 7 + Step 9 + QA bullet. index-template.html: opt-in config example.
- plugin.json 0.2.0 -> 0.3.0; FORK-CHANGES §6.

Verified: node --check; in-browser mount + capture submit -> sw_lead_submit +
thank-you, cta/audio/scene_view events, SEO block still hidden; scroll-depth
threshold logic (25→100 once each + flight_complete, idempotent under up/down
scroll) verified deterministically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sbyrd-create sbyrd-create reopened this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant