Embedding “View as Web Page” as a thumbnail-like preview on a standard Marketo LP

We’re building out an account-based referral program for a client. The program lets people invite colleagues to in-person events. It’s like “Forward to Friend” on PEDs.

An email preview needs to accompany the referral form, and rather than the huge time suck of manually creating thumbnail images, we realized the standard “View as Web Page” (VAWP) would be perfect if we could get it into an <iframe>:

Turned out to be net-easy — though, as with many such projects, 10% harder than you might think!

Set up the HTML and CSS

First, the <iframe> and some critical styles and attributes:

<style>
iframe#emailWebView {
  pointer-events: none;
  user-select: none;
}
</style>
<iframe id="emailWebView" scrolling="no" tabindex="-1"></iframe>

The HTML attributes scrolling and tabindex, together with those two CSS “event neutralizing” styles, make the <iframe> behave like a static image so it can’t be accidentally clicked or selected.*

Set width and height and other styles to fit your page. In the screenshot I even added filter: grayscale(1) because I thought it looked cool.😊

Populate the <iframe>

Here’s where that “10% harder” came in.

You might think you could just set the <iframe src> to the VAWP URL (https://landingpages.example.com/index.php/email/emailWebview) with the main document’s mkt_tok copied into its query string, e.g.:

    let currentURL = new URL(document.location.href);
    let mktTok = currentURL.searchParams.get("mkt_tok");

    let emailWebView = document.querySelector("#emailWebView");
    let iframeURL = new URL("/index.php/email/emailWebview", currentURL.origin);
    iframeURL.searchParams.set("mkt_tok", mktTok);

    emailWebView.src = iframeURL;

But this won’t work. Why? Because Marketo sends the HTTP header X-Frame-Options: DENY when you request the VAWP page even if you think you’ve turned that off in Admin.

That is, this valuable setting..

... works for regular LPs under https://landingpages.example.com, but the VAWP service doesn’t pay attention and keeps sending DENY. This is a longstanding bug oddity. Luckily, it’s easy to work around.

’stead of src, fetch into srcdoc

Obvious-when-you-think-about-it fact about X-Frame-Options is it can’t stop you from setting an <iframe srcdoc> (not src but srcdoc) to whatever HTML you want. The HTML might’ve been fetched as-is from a same-origin URL or even a CORS URL, but srcdoc has no idea where it originally came from.

And unless very rare measures (like, “never seen in real life” type of rare) are taken, you can fetch or XMLHttpRequest any public same-origin resource.

So we just need to GET the VAWP URL and then set the srcdoc to the raw response:

    let currentURL = new URL(document.location.href);
    let mktTok = currentURL.searchParams.get("mkt_tok");
        
    let emailWebView = document.querySelector("#emailWebView");
    let iframeURL = new URL("/index.php/email/emailWebview", currentURL.origin);
    iframeURL.searchParams.set("mkt_tok", mktTok);
    fetch(iframeURL)
      .then( resp => resp.text() )
      .then( body => emailWebView.srcdoc = body );

Presto!

Adding resilience

The code above works fine, but it’s a bit fragile. More resilient code is below.

One fragile part: if you inject that code via a tag manager (don’t if you can avoid it, since it just hurts performance!) there’s a race condition with the built-in JS that moves the mkt_tok query param into the __mktTokVal global variable. So better to look for the token in both places.

Another: if the end user refreshes the page, there won’t be a mkt_tok and the VAWP will respond with Cannot get email content (same way it always works). Persisting the mkt_tok in Session Storage takes care of that.

Finally: why not create a real live document (as in Document object) from the fetched HTML and then set the srcdoc from that doc? Helps you think about other modifications you might make to the HTML before populating the <iframe>.

So let’s use this:

    let currentURL = new URL(document.location.href);
    let mktTok = currentURL.searchParams.get("mkt_tok") || window.__mktTokVal || sessionStorage.getItem(`${currentURL.pathname}?mkt_tok`);
    sessionStorage.setItem(`${currentURL.pathname}?mkt_tok`, mktTok);
        
    let emailWebView = document.querySelector("#emailWebView");
    let iframeURL = new URL("/index.php/email/emailWebview", currentURL.origin);
    iframeURL.searchParams.set("mkt_tok", mktTok);
    fetch(iframeURL)
      .then( resp => resp.text() )
      .then( body => emailWebView.srcdoc = Document.parseHTMLUnsafe(body).documentElement.outerHTML );
Notes

* You could also sandbox it but that disables some cool stuff I’ll show in a future post.