All case studies

Home services

Four click conversions that could never fire

A home-services site's call and booking conversions were built on jQuery. The site never loaded jQuery. I rebuilt them in vanilla JS and proved them on the live beacon.

// symptom
Four click conversions (call, book, finish booking, chat send) were requested, and the team asked whether the client needed to add jQuery.
// cause
The listeners were jQuery handlers on a Next.js site that never shipped jQuery, so they never bound. Three of the four selectors used jQuery-only syntax.
// fix
A vanilla JS click bridge that survives single-page route changes, plus a postMessage bridge for the scheduler's booking-complete signal.
// result
Call and Book Now verified live on the beacon, with no site-code changes. Chat send documented as vendor-side.
  1. 01 tag manager loaded ✓
  2. 02 typeof jQuery → undefined ✗
  3. 03 jQuery listeners bound: 0 ✗
  4. 04 scheduler iframe: cross-origin ⚠
  5. ── patch: vanilla click bridge ──
  6. 05 bridge bound 6/6 tel · 3/3 CTA ✓
  7. 06 booking-complete via postMessage built · not live-tested ⚠
  8. 07 conversion beacon · value ok ✓ 200
  • Tealium iQ
  • GTM
  • The Trade Desk
  • Next.js
  • Scheduling widget

The ask

The request looked simple: four click conversions for a plumbing company’s ads. Call Us, Book Now, Finish Booking, and the chat widget’s Send button. The first question from the team was whether the client needed to add jQuery to their site.

What I found

The site was built on Next.js. It had never loaded jQuery, not once. The page source had zero references to it, and at runtime window.jQuery was undefined.

That mattered because all four conversions had been built as jQuery event handlers. They weren’t broken. They could never have worked. Three of the four also used jQuery-only selectors like :contains(), so I couldn’t just copy them into plain JavaScript.

Two of the targets lived inside cross-origin iframes: the scheduler’s final booking step and the chat widget. You can’t attach a listener inside someone else’s iframe. So I read the vendors’ code to see what they tell the parent page. The scheduler posts a message when a booking completes. The chat widget only posts sizing messages.

The fix

  • A vanilla JS click bridge. It registers buttons by their text, binds per element, and re-scans with a MutationObserver, so the bindings survive the site’s client-side route changes.
  • A postMessage bridge that turns the scheduler’s booking-complete message into a conversion event.
  • Per-conversion data so each click sends the right value to the ad platform.

A second bug showed up mid-build. After the first publish, clicks sent nothing. An older extension that sorted ahead of the new ones was cancelling every link event. Moving it to run last fixed it.

How I proved it

On the live site, the bridge bound all 6 phone links and all 3 Book Now buttons, and still bound after navigating between pages. Each click sent the correct conversion beacon with the right value, one pair per click, not a double fire.

What I was straight about

The booking-complete conversion is built but not live-tested. Testing it for real would book a real job. The chat Send button stays out of reach: it’s inside the vendor’s iframe, and the vendor doesn’t expose a “message sent” event. I documented both, including who can fix the second one.

Seeing something like this?

Tell me what's not counting