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.