Multiple Marketo-tracked links in Velocity

Little-known — and therefore little-hated! — this bug applies only to Marketo's email-specific Velocity setup, and not to the language in general.

If you mentioned it to someone who's built webpages using VTL, they'd rightly look at you like you're crazy. And the necessary workaround also goes against every coding principle I've ever endorsed here.

In other words, please don't take any general-purpose Velocity guidance from this post, but do use the code for this specific goal.

What's the problem?

Even when you follow the prescription for generating tracked links in Velocity (namely, output fully-formed <a> tags from VTL, as opposed to outputting bare URLs and trying to plug them into <a>s in the outer email content) you still won't be able to output multiple tracked links in some common cases.

Take a straightforward #foreach loop:

#foreach( $link in $links )
<a href="http://${link}">Click me</a>
#end

Or a list that you pick items off:

<a href="http://${links[2]}">Click me</a>
<a href="http://${links[4]}">Click me too</a>

These can lead to unexpected results, in ascending order of badness:

  • links working, but not being tracked (best of the bad outcomes, all things considered)
  • links being rewritten to go via your tracking domain, but all pointing to the same target URL, either the first or last one (pretty awful)
  • links being rewritten, but all pointing to broken URLs with literal Velocity code in them, like http://${links[4]} (totally bad)

The (likely enough) cause

The bug has been explained by Marketo as “Links don't work in #foreach loops” but that isn't quite right, as it doesn't cover all failure cases.

My best explanation for this behavior is that the code that rewrites token-emitted links stores the literal $variable name somewhere, and continues overwriting the value whenever it sees a variable of the same name, even if that variable has a different value over time.

See, what the above snippets have in common is that they use the same variable name. In one case $link is a scalar (String) and in the other case $links is a vector (ArrayList), but they're still referring to the same $variable across different iterations/lines.

Toward a workaround

If you can use a different $variable name then the problem is avoided.

Problem is, the mighty advantages of loops and arrays are reduced code length + complexity, reusable variables, built-in iteration methods, automatic first and last item boundaries, and so on. Velocity is crazy verbose already and I always endorse arrays as a best practice.

There's a direct collision, then, between best practices and… well, working practices. We have to round down to working, but still want to use loops as much as possible. This means initializing new dynamic variables... in a loop. It's kinda fun.

The code

The code below comes in part from a Community question posted by GM. It's not totally anonymized, but I figured he wouldn't mind some extra props for his case studies!

GM is parsing a field downloadcenter that's populated, via a Marketo form, with a typical semicolon-delimited value list, like BPDCS;IVCS;KDSCS. Then he's matching those acronyms against a dictionary of link URLs and their respective friendly names.

So far, all good. The problem is outputting the links. As you can see over on his Nation post (I won't reprint the non-working code here) he tried looping over the matched links, only to find that tracking didn't work.

I'll break it down afterward, but here's working code that loops over a dictionary, matches keys, and then is able to output multiple tracked links:

#set( $allDownloadLinks = {
  "BPDCS":{
    "text":"BPD Case Study",
    "href":"infos.example.com/rs/123-IYJ-456/images/examplepartners-casestudy-digitalisation-marketing-systeme-information-marketing-commercial.pdf"
},
  "DACS":{
    "text":"DA Case Study",
    "href":"infos.example.com/rs/123-IYJ-456/images/examplepartners-casestudy-denyall-marketing-automation.pdf"
},
  "IVCS":{
    "text":"IV Case Study",
    "href":"infos.example.com/rs/123-IYJ-456/images/examplepartners-casestudy-infovista-systeme-information-marketing.pdf"
},
  "EBSCS":{
    "text":"ESSEC Case Study",
    "href":"infos.example.com/rs/123-IYJ-456/images/examplepartners-casestudy-essec-conception-deploiement-strategie-acquisition-nurturing.pdf"
},
  "KDSCS":{
    "text":"KDS Case Study",
    "href":"infos.example.com/rs/123-IYJ-456/images/examplepartners-casestudy-kds-nouvelles-pratiques-generation-demande-automatisee.pdf"
}
})
#foreach( $key in $lead.downloadcenter.split(";") )
  #set( $downloadLink = $allDownloadLinks[$key] )
  #evaluate( "${esc.h}set( ${esc.d}downloadLink_${foreach.count} = ${esc.d}downloadLink )" )
#end
#if( $downloadLink_1 )
<a href="http://${downloadLink_1.href}" target="_blank">${downloadLink_1.text}</a>
#end
#if( $downloadLink_2 )
<a href="http://${downloadLink_2.href}" target="_blank">${downloadLink_2.text}</a>
#end
#if( $downloadLink_3 )
<a href="http://${downloadLink_3.href}" target="_blank">${downloadLink_3.text}</a>
#end
#if( $downloadLink_4 )
<a href="http://${downloadLink_4.href}" target="_blank">${downloadLink_4.text}</a>
#end
#if( $downloadLink_5 )
<a href="http://${downloadLink_5.href}" target="_blank">${downloadLink_5.text}</a>
#end

Yep, that's right. You have to add separate conditions for up to the maximum N links. There won't be any unwanted output (nonexistent $variables are just skipped) but so much for looping over an auto-sized array in just a couple of lines.

Luckily, in an email context you'd wouldn't want to output a limitless number of links right after each other. Having placeholders for, say, 20 possible links is not the worst possible thing. I'm not happy about this but have not been able to find an alternative (maybe one of you brave souls can).

A little how-it-works bit

This line:

  #evaluate( "${esc.h}set( ${esc.d}downloadLink_${foreach.count} = ${esc.d}downloadLink )" )

uses Velocity's #evaluate directive to dynamically create and execute a code snippet. (I use this method to cool effect when parsing JSON in VTL.)

This allows you to create new variable names on-the-fly. After un-escaping, it's executing:

  set( $downloadLink_1 = $downloadLink )
  set( $downloadLink_2 = $downloadLink )
  set( $downloadLink_3 = $downloadLink )
  ... etc...

By the way, while you can also use #evaluate to output (and thus in theory get back to a quick 3-line loop) Marketo won't track the links that way. *Sigh*. Thus my conclusion that you need to list separate conditions.