Adding Munchkin to a Wix website

Either I was a bit overconfident in this Marketo Community post from over a year ago about Munchkin & Wix, or Wix has changed something since. (Can't track changes on their side, not being a real Wix user myself.)

Either way, as of Spring '16, Wix makes you jump through a few hoops. But Munchkin tracking can be done! I don't think this recipe is available anywhere else on the web, so let's dig in.

The problem: Custom HTML is wrapped in IFRAMEs

Unlike other "prosumer" site builders such as SquareSpace, Wix seems deathly afraid of letting you add custom HTML (including JS) directly to your managed pages. Instead, any HTML you add using the Add More » HTML Code is always wrapped in an IFRAME before insertion.

screenshot .right

Therefore HTML Code isn't so different from the more explicit Add More » Embed a Site. They both end up adding an IFRAME, it's just that in the first case, the IFRAME is created automatically to wrap your code (without warning you!).

(Note: Embed a Site is the same as choosing HTML Code, then switching to the Website Address option in the popup — aliases for the same action.)

There is one very helpful difference for our purposes, though: while the automatic IFRAME you get with HTML Code is loaded from one of Wix's internal domains (like http://www.example.com.usrfiles.com, which is a subdomain of usrfiles.com), the IFRAME we can specify with Embed a Site can load from anywhere we want. Like — huge hint — a subdomain of our Wix domain.

Backing up: Munchkin and IFRAMEs

So what's the problem with loading Munchkin in an IFRAME, in general? Well, that depends on how much you can tweak your install, and how well you understand JS and the DOM (so non-developers aren't prepared to make it work perfectly).

If all you do is put the Munchkin embed code (whether you use Simple, as I usually advise, or one of the Async versions available in Marketo » Admin » Munchkin) in an IFRAME, here's what's going to happen:

  1. You'll log a Visit Web Page to the URL of the IFRAME, not the URL of the main page the person is looking at.
  2. Click Link activities on the main page will not be logged.
  3. You'll have a higher probability than usual of missing Munchkin hits, simply because there are more processes that need to complete before the person navigates away from your page.

There's not much that can be done about #3. The more levels of indirection + asychronous loading you add to a page, the more likely the user is to navigate away from the page before everything's finished. This is the definition of background/async processing, and as I've written about a lot before, pushing analytics scripts deep into the background doesn't make sense if you simultaneously expect all your hits to be logged. Those are essentially conflicting priorities: calling something important/mission-critical and treating that same thing as unimportant/disposable.

When you put HTML in an IFRAME, the IFRAME's inner document is always loaded asynchronously, so that means the <script> tags inside the IFRAME are, non-negotiably, fetched asynchronously relative to the main document. If you then load other scripts async and/or fetch tracking pixels via Ajax (like Munchkin does) that's just more stuff that may or may not finish loading before someone moves away from the main doc. Now, I'm not saying this is going to lower your accuracy below 99%. But it certainly can lower it to 99% from 99.9%.

Far more negotiable are #1 and #2. When you simply load and init() Munchkin from inside an IFRAME, it sees the IFRAME's location — its URL and query string — as the current document. It'll pass the main document (the one your lead is actually looking at and which appears in the browser's location bar) as the Referrer of the Visit Web Page activity, but the URL of the Visit Web Page is the IFRAME's URL.

Partial Fixes and Workarounds

Since the IFRAME URL is the same on every page with Wix, Marketo sees repeated visits to the same URL. But the Referrer will change for each visit, and the Referrer can, to a degree, be used in Smart Lists.

There's a crafty workaround for the problem of logging the IFRAME's URL as the visited URL. I won't detail it here since there's a complete solution below, but in all browsers but IE 8-9, you could coerce Munchkin into showing just the IFRAME's hostname but logging the pathname, querystring, and hash from the main doc. So if you visited http://www.example.com/myfancypage?product=hammers Marketo would log http://www.example.com.usrfiles.com/myfancypage?product=hammers. Which isn't so bad — you could use a Contains in a Smart List and do pretty well with this. But this still won't capture clicks, just page visits.

In contrast to a certain not-at-all-great American, I like [hostnames and clicks] that did get captured. And if you still support IE < 10 for browsing, you should support it for analytics, too. So read on to get a full cross-browser solution.

Prelude to a fix: the same-domain policy for IFRAMEs

If you've done any serious web development, you've encountered the same-origin policy with Ajax. Simultaneously frustrating and understandable, the same-origin security policy, by default, prevents documents from different domains from communicating with each other using standard XMLHTTPRequest(2). It's enforced to varying degrees depending on the type of communication requested — GETing (reading) vs. PUTing/POSTing (writing) — and whether each domain opts in to the transaction (see CORS) and other details.

What you might not know, especially if you're a young'un who started developing after the debut of CORS, is that IFRAMEs are subject to a much looser same-domain policy. There's a policy, to be sure, and when you violate it you ain't getting anywhere. But with the right setup, you can make IFRAMEs and main documents communicate with each other securely and flexibly.

While "origin" in "same-origin" includes the full hostname (www.example.com and host.example.com are different origins, even if you control both servers) and even the port (https://www.example.com and http://www.example.com are different origins), the looser IFRAME policy allows two documents merely from the same registered domainwww.example.com and host.example.com share the same registered domain example.com — to talk to each other.

And same-domain communication allows an IFRAME on host.example.com to tell the Wix website www.example.com to load Munchkin.

See, we don't want to load Munchkin in the IFRAME, because that leads to the problems and gaps above. We want to use the IFRAME to load Munchkin in the context of the main document.

The fix

The fix takes the form of a tiny HTML page that you host in your Marketo instance. You insert this page as an explicit IFRAME in your Wix site (Add More » Embed a Site).

My Wix website is www.vaneckdemos.com and one of my Marketo domain aliases is assets.vaneckdemos.com:

screenshot

I've added the page to Marketo as munchkin-injector.html but it could have any page name you want. Just use that page name in Wix:

screenshot

Here's the entirety of munchkin-injector.html for you to copy and paste:

<html>
<head>
<title>Marketo Munchkin Injector Shim</title>
<script>
document.domain='vaneckdemos.com';

var parentDocument = window.parent.document,
    MunchkinEl = parentDocument.createElement('script');

MunchkinEl.text = "(function() {                             \
  var didInit = false;                                       \
  function initMunchkin() {                                  \
    if(didInit === false) {                                  \
      didInit = true;                                        \
      Munchkin.init('123-ABC-456');                          \
    }                                                        \
  }                                                          \
  var s = document.createElement('script');                  \
  s.type = 'text/javascript';                                \
  s.async = true;                                            \
  s.src = '//munchkin.marketo.net/munchkin-beta.js';         \
  s.onreadystatechange = function() {                        \
    if (['complete','loaded'].indexOf(this.readyState) > -1) \
      initMunchkin();                                        \
  };                                                         \
  s.onload = initMunchkin;                                   \
  document.querySelector('head').appendChild(s);   \
})();";

parentDocument.querySelector('head').appendChild(MunchkinEl);
</script>
</head>
<body>
</body>
</html>

You need to customize the Munchkin.init() line to use your Munchkin ID (you probably figured that out) and the document.domain line must be changed to your registered domain. That's it! And yes, the backslashes are part of the file.

Why does it work?

Since the IFRAME document on assets and the main document on www share a parent registered domain (vaneckdemos.com), they're allowed to talk if they want. To "opt in," each side needs to change its active domain to the parent domain. That's what the document.domain line does in munchkin-injector.html, and all Wix sites already have the same code.

Once there's open communication between the two, the IFRAME code can connect directly to the main doc and add the Munchkin bootstrap loader to the main doc. Munchkin downloads and initializes within the main doc, attaching its click events there and tracking the main doc's URL. It will function exactly as if you were able to add the script directly to the Wix page.

P.S. To fend off any finger-pointing about the embedded script text: that's Marketo's Async Munchkin loader, and I didn't feel like rewriting it for this exercise! Of course it could be refactored, but it works, and I think it's good to use familiar code so people don't get overwhelmed. I only wish we could use the the Simple Munchkin loader — which I recommend in all other cicrumstances — instead, but because the IFRAME is loaded async, no such luck.