Force CTA Scripts to Run on History Change (for single-page applications)

I have a (new [not Beta anymore]) CTA on my website whose Hubspot-provided embed code looks like this: (though I swapped out some numbers to “anonymize” it)

<div class="hs-cta-embed hs-cta-embed-123456789123456789" style="max-width:100%; max-height:100%; width:315px;height:300px" data-hubspot-wrapper-cta-id="123456789123456789">
 <link rel="stylesheet" href="https://js.hscta.com/embeddable_cta_placeholder_v1.css">
 <div class="hs-cta-loading-dot__container">
 <div class="hs-cta-loading-dot"></div>
 <div class="hs-cta-loading-dot"></div>
 <div class="hs-cta-loading-dot"></div>
 </div>
 <div class="hs-cta-embed__skeleton"></div>
 <picture>
 <source srcset="data&colon;image/gif;base64,R0lGODlhAQABAAAABAAEAAAICTAEAOw==" media="(max-width: 480px)" />
 <img alt="Some alt text" loading="lazy" src="https://no-cache.hubspot.com/cta/default/0000000/interactive-123456789123456789.png" style="height: 100%; width: 100%; object-fit: fill" onerror="this.style.display='none'" />
 </picture>
</div>

When I embed the CTA on a page, it loads perfectly no problem on the first page load. BUT if I click through via my single-page application to go to the location of the embed (ie. I do a browser history change instead of a full pageview), the “CTA initializer” scripts fail to run, and the CTA does not “become” its true interactive self — it just sits as the png referenced above. The scripts must be set to execute only once (probably when the DOM is ready or when the window is loaded?).

I’ve tried re-injecting the script into the page via the below; this doesn’t work as I get the error “duplicate instance of web interactives app exists”.

var script = document.createElement('script');
script.src='https://js.hubspot.com/web-interactives-embed.js';
document.body.appendChild(script);

I’m looking for a clean way to re-run the CTA initializers (I guess the web-interactives-embed.js script? Or maybe https://js.hscta.net/cta/current.js ?), without forcing my users to refresh the page. There are precedents for this in other Hubspot tools, for example:

I have tried exploring the relevant JS objects that exist on the page via the browser console, but most of them are poorly documented (if at all), and I haven’t been able to find a way to do a sort of “CTA force refresh”. Some potentially relevant “keywords”:

  • hsCtasOnReady
  • __PRIVATE__HubspotCtaClient
  • hubspot_web_interactives_running
  • __hsWebInteractiveInstance
  • _hsp, _hsq, _hspb variations, _hstc variations
  • hubspot.form.api
  • window.hbspt.cta

Any help or ideas for alternate approaches are greatly appreciated. Ideally, I don’t need to do anything different on my back-end; There should be a client-side solution for this, I think? It doesn’t feel like it should be as hard as it is.

Links to similar/identical issues, with slightly different descriptions or angles of approach:

Hey, @baribeau :waving_hand: I saw your post with links to similar SPA-related issues in another post. I’ll take your post and share it with the team that owns the new, not a beta anymore, CTA tool and ask for some guidance.

I’ll post here with any updates. Thank you very much, including your details + the steps you’ve already taken. It’s really helpful to have those details when reaching out internally.

Talk soon! — Jaycee

Thanks Jaycee! If you’re engaging directly with the Product team, there’s also a deeper-dive non-anonymized video in support ticket 13778291

I’ve been fighting with the same problem, came across your post, and *maybe* got to a solution.

I’ve tried re-injecting the script into the page via the below; this doesn’t work as I get the error “duplicate instance of web interactives app exists”.


The key bit seems to be telling the Hubspot JS that web interactives are not running before re-injecting the script. It’s hacky but seem to work fine in my limited testing from the console.

window.hubspot_web_interactives_running = false;
var script = document.createElement('script');
script.src='https://js.hubspot.com/web-interactives-embed.js';
document.body.appendChild(script);

Thanks to jsegars for the insight that solved the problem. I’d been been putting off reviewing and implementing this for many months, but here’s how I solved it with a custom Tag in Google Tag Manager:

1. Create a GTM Trigger of the type “History Change” that fires on All History Changes.

2. Create a GTM tag with custom HTML, that fires based on the Trigger you set in Step 1.

3. Add a <script> to your Tag that creates a Mutation observer and logs your mutations:

<script>

var mutationObserver = new MutationObserver(function(mutations) { 
 mutations.forEach(function(mutation) {
 console.log(mutations);
 });
});

mutationObserver.observe(document.body, {
 attributes: false,
 characterData: false,
 childList: true,
 subtree: false,
 attributeOldValue: false,
 characterDataOldValue: false
});

</script>

4. Publish your updated GTM container.

5. Load a page and open the console. Then click to a new page on your website, and see what mutations take place on the page change in your web app. You need to find a mutation that is (1) consistent across all page changes, and (2) executed after the content on your page loads.

  • This latter point is critical, because if you execute the script before the Hubspot CTA element finishes loading, the script isn’t going to have anything to modify. (You could put the script on a timer instead of using a mutation observer, but that’s risky because you don’t know how long each user’s page load would take).
  • On my company’s website, I had to test a few options, but the most appropriate mutation was “the removal of the progress bar that visually cues page changes”. This progress bar on our site always has the HTML ID “nprogress” — so with my Mutation Observer in Javascript, I can check for it with mutation.removedNodes[0].id == “nprogress”

6. Update your GTM script to check for the relevant mutation, and when it finds it, to run jsegars’s script and disconnect the mutation observer.

  • You can safely disconnect the mutation observer, because your GTM implementation re-inserts it on every history change.
  • Your exact implementation logic is going to vary based on what you’re matching against. In my case, the nprogress node is always the only removed node in its mutation, and not every mutation has any removed nodes, so I also check for removedNodes.length > 0 to avoid throwing console errors:
<script>

var mutationObserver = new MutationObserver(function(mutations) { 
 mutations.forEach(function(mutation) {
 if (mutation.removedNodes.length > 0 && mutation.removedNodes[0].id == "nprogress") {
 window.hubspot_web_interactives_running = false;
 var CTArefresh = document.createElement('script');
 CTArefresh.src='https://js.hubspot.com/web-interactives-embed.js';
 document.body.appendChild(CTArefresh);
 mutationObserver.disconnect();
 }
 });
});

mutationObserver.observe(document.body, {
 attributes: false,
 characterData: false,
 childList: true,
 subtree: false,
 attributeOldValue: false,
 characterDataOldValue: false
});

</script>

7. Don’t forget to publish your updated GTM container again, and make sure that everything consistently works.

Thanks for sharing these solutions… I’m surprised there isn’t better support for this with SPA’s…
I incorrectly assumed that sorting out the page tracking would fix the CTA loading issue… alas… there’s even more work to do.
However, I believe this helps things along a lot now: