Make native Pre-Fill work across refreshes with a few lines of JS

Marketo’s hardcore rules for native Pre-Fill make sense, in particular stripping the mkt_tok query param so people don’t accidentally send their own personalized link to someone else.

Many users augment the native feature using my SimpleDTO library, which enables Pre-Fill even on non-Marketo pages. (With infinite scale, since it doesn’t use the REST API.) SimpleDTO also covers more cases on Marketo LPs. But if you’re not ready for that move, there’s one hiccup in the native implementation — a bug in my view, since it has no privacy or security benefit — you can fix with a little JS.

What’s the bug? Well, even when Pre-Fill works perfectly on an initial pageview:[1]

Just refreshing the page breaks it:

Now, the technical reason for this is clear: the always-added stripmkttok.js removed the mkt_tok for safety. (Which, again, is a good thing.) So upon refresh, the page no longer meets the prereqs for Pre-Fill.

But the UX is needlessly punitive. The person refreshing the page in their browser — the page that just included their data — isn’t expecting the data to be gone. Merely re-pre-filling the form after refresh doesn’t pose any risk, long as there aren’t any side effects.

You may think I’m about to suggest using session cookies or session storage to preserve the data. Nope! You don’t need either of those, and since they’re subject to cookie consent policies, they aren’t reliable anyway. (Even with consent, they also represent a “side effect” in the sense that the data would persist across other pageviews in the session unless deliberately deleted.)

All you need is history.state.

history.state is the narrowest client-side storage mechanism (thus perfect here)

While used extensively by Single Page Applications (SPAs), the History API is almost never used on landing pages. Even with SPAs, it’s usually tapped by frameworks under the hood, so coders rarely understand it.[2]

In brief, with history.state you add JS data to browser session history so when someone returns to a previously visited entry (including refreshing) the data is restored as well. A state isn’t readable from any other position in the navigation history. And it’s deleted completely when the browser closes or history is deliberately cleared.[3]

So that’s perfect for attaching Pre-Fill data only to the current entry without risk of leaks. Note I really mean the current entry: history.state attached to https://pages.example.com/mypage.html isn’t even available on a separate visit to https://pages.example.com/mypage.html, let alone on https://pages.example.com/mypage.html?somethingelse. It’s locked down to one history entry!

The catch: we must attach history.state to the URL without the mkt_tok, i.e. after the stripping happens. Trickier than it sounds, since stripmkttok.js is injected at the very bottom of the document. Our code will thus always run first; if we merely add a load listener, that’ll run before their load listener, too.

Luckily, there’s a way to force our stuff to run last: in a load listener, call setTimeout with a delay of 0. (No, this is not an arbitrary wait, which you know I hate from other posts. It’s an exact order of operations!)

Put this code in a <script> at the very top of the <body>:[4]

if( window.mktoPreFillFields ) {
  window.addEventListener("load", (e) => {
    const state = Object.assign({}, history.state, { mktoPreFillFields });
    setTimeout( () => history.replaceState(state , "", document.location.href), 0);
  });
} else {
  window.mktoPreFillFields = history.state?.mktoPreFillFields;  
}

Enjoy!

Notes

[1] The form in the screenshot also uses custom code to set Email to read-only if it’s Pre-Filled.

[2] Plus it isn’t even possible to introspect in DevTools, making it very black-boxy.

[3] And after periods of inactivity, depending on the browser.

[4] window.mktoPreFillFields must be available when forms render lower in the <body>.