Marketo-integrated page builders like Stensul & Knak publish your pages as Marketo LPs. Published LPs fully support webfonts hosted in Marketo Design Studio, since they’re on the same domain.
But what about inside the builder app, when you’re still previewing? There, you’ll have a problem. Even with the correct @font-face rule in your CSS, the font won’t load because of CORS restrictions:
❌ Access to font at 'https://pages.example.com/rs/123-ABC-456/images/entypo.woff' from origin 'https://app.stensul.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.This is reasonable, of course. Marketo isn’t in the business of providing an easily abused font CDN to the public!
There’s a slick workaround. You can’t control CORS headers, but you can (by definition) host a page with same-origin access to the fonts. By deploying such a page as a hidden IFRAME, you can shuttle the fonts to a parent page, even if the parent is on a different origin.
So create a simple “font server” webpage that just runs this JS (doesn’t even have to be a true Landing Page, it could be an .html file in Design Studio):
// substitute your page builder domain
const fontClientOrigin = "https://company.stensul.com";
const fontFamilies = new Set();
fontFamilies.add({
name: "famo",
url: "https://assets.codepen.io/250687/7d25ff4e-2756-4745-b0ef-ac83530b35b6.ttf"
});
fontFamilies.add({
name: "lano",
url: "https://assets.codepen.io/250687/39C54B_0_0.ttf",
descriptors: { style: "italic" }
});
/* ... more fonts */
for( const {name, url, descriptors} of fontFamilies ) {
fetch( url )
.then( (resp) => resp.blob() )
.then( (blob) =>
blob.arrayBuffer()
.then( (asBuffer) => window.parent.postMessage({ name, asBuffer, descriptors }, fontClientOrigin, [asBuffer]) )
);
}Then add this code to your landing pages:
function initXDFontClient(serverURL){
const fontServerURL = new URL(serverURL);
const fontServerIframe = document.createElement("iframe");
fontServerIframe.style.display = "none";
fontServerIframe.src = fontServerURL.href;
document.body.append(fontServerIframe);
window.addEventListener("message", ({ source, origin, isTrusted, data: font }) => {
if( source !== fontServerIframe.contentWindow || origin !== fontServerURL.origin || !isTrusted ) return;
const face = new FontFace(font.name, font.asBuffer, font.descriptors)
document.fonts.add(face);
});
}
// substitute your page builder domain
if( document.location.origin == "https://company.stensul.com" ) {
initXDFontClient("https://pages.example.com/iframe-font-server.html");
}The “font client” gets inbound messages containing a font name + binary font data and dynamically adds a corresponding FontFace.[1]
What I particularly like is both “server” and “client” parts use minimal serialization. The server converts the fetched Blob into an ArrayBuffer, but the AB is transferred directly to the client. The client instantiates the font directly from the AB. Not that serialization (i.e. to data: URIs) would be catastrophic but it’s nice to do without.
Yes, there’s a FOUT (Flash of Unstyled Text)
There’s a sub-second delay while Marketo-hosted webfont(s) are loading when fallback fonts are used.
That’s a natural side effect of (a) loading fonts asynchronously while not (b) hiding content until fonts are ready. The code isn’t running in production anyway, only for previews in the page builder, so shouldn’t be a problem![2]
Notes
[1] You only need one font format for this case, preferably WOFF2. Remember, the IFRAME isn’t used on your published pages.
[2] There are ways to eliminate the FOUT. One (lightly tested) way is caching data: URIs in LocalStorage for each font. Then add rules to a CSStyleSheet on page load.
Note using the Cache API to store constructed Responses, cool as that would be, won’t work here because it’s always asynchronous.