Overview
Please note before starting that MessageGears uses Apache FreeMarker (version 2.3.23) for both Content Templates and SQL templates. For more detailed information on FreeMarker and its uses, the official handbook from Apache can be found by clicking on the following link. This article discusses using FreeMarker at an Intermediate level. An understanding of the basic concepts of FreeMarker is recommended before using the processes detailed in this article. You can find additional information on building Campaigns with MessageGears, in the FreeMarker Basic and Advanced sections.
Table of Contents
Intermediate Topics (Technical Marketers)
Data Types, Built-ins, and Formatting
Scalars (String, Number, Date, Boolean)
Containers (Sequence/Array, Hash)
Understanding the FreeMarker Data Model
XML Recipient and Supplemental Data
JSON Recipient and Supplemental Data
Creating and Referencing Supplemental Data
Joining Audience and Supplemental Data
Intermediate Topics (Technical Marketers)
FreeMarker is an open-source templating language. This means that it uses placeholder text, also called variables, to create dynamic content. This templating language, along with Audience Data and Supplemental (formerly Context) Data used in Accelerator, means that personalized content can easily be created within MessageGears Templates.
For a more basic understanding of FreeMarker use with MessageGears, please read through the Basic use article on FreeMarker. Intermediate Topics include understanding the different types of data types within FreeMarker, using Arrays, parsing JSON and XML, and using Dynamic Content.
Data Types, Built-ins, and Formatting
Variables of different data types need to be compared and referenced in different ways. Each data type also has FreeMarker Built-In functions. These Built-In functions help process the data in a number of common ways. In addition to the FreeMarker Built-In functions, MessageGears also includes useful custom macros for special operations within email contacts.
Scalars (String, Number, Date, Boolean)
String Values
String data types are the most versatile of the data types. A String value can be any series of characters or numbers, including special characters and spaces. String values do not have any numerical value to them, unlike Number values. It is possible that when referencing a String value within a Conditional Statement, greater than or less than values may not work as expected.
String values have many built-in functions to choose from. Below are examples and descriptions of some commonly used functions for String values:
Cap_First and Capitalize:
The cap_first function will capitalize the first letter of the first word of the targeted data.
The capitalize function will capitalize the first letter of every word of the targeted data.
<#assign name = "john doe" />
${name?cap_first} → John doe
${name?capitalize} → John Doe
Trim:
If data is entered in the HTML Template with included white space, the Trim function will remove whitespace from the beginning and the end of the string. Trim will not remove spaces between words, but only starting and trailing white space.
<#assign email = " john.doe@gmail.com ">
${email?trim} → john.doe@gmail.com
Using Arguments, Trim can also be given a string of characters to remove instead of removing whitespace.
<#assign due_date = "April 1, 2021">
${due_date?trim(", 2021")} → April 1
URL:
The URL Built-In function inputs standard escape characters within a string to create a cohesive URL.
<#assign category = "living & comfort">
www.cityandglory.com/${category?url}/ → www.cityandglory.com/living%20%26%20comfort/
Number Values
Number values are values that exist on a number line. These can be a whole number or a decimal value and can be positive or negative values. Built-In functions for number values can be found at the following link.
Below are some examples and descriptions of common Built-In functions for number values:
Round, Floor, and Ceiling:
<#assign
a = 1.2
b = 1.5
c = 1.8
/>
${a?round} ${b?round} ${c?round} → 1 2 2
${a?floor} ${b?floor} ${c?floor} → 1 1 1
${a?ceiling} ${b?ceiling} ${c?ceiling} → 2 2 2
The Round Built-In function takes a number value and rounds it to the nearest whole number. A value like 1.5 will round up.
The Floor function will always round a decimal value down to a whole number. It does not matter the digit after the decimal value. Even a value of 1.9 will round down to a value of 1.
The Ceiling function will always round a decimal value up to a whole number. It does not matter the digit after the decimal value. Even a value of 1.1 will round up to a value of 2.
The ?string Function:
The ?string function modifies a Number value into different formats. Some of the formatting options can change based on the locale of the Template. For example, the currency method of the function will prepend a dollar sign ($) within the en_US locale. Within another locale, for example, the en_UK locale, a different currency symbol may be used.
<#assign x = 490500>
${x?string.number} → 490,500
${x?string.currency} → $490,500
The ?string function supports the following methods:
x?string → converts a number value to a string. Converting a number to a string allows all the other Built-In functions for strings to be used on the value.
${x?string.number} → Adds commas or periods to the number to make it more human-readable
${x?string.currency} → Adds currency symbols and commas
${x?string.percent} → converts the number to a percentage value and appends a percentage sign at the end of the strings
${x?string.computer} → removes any formatting and displays the number as a computer readable number.
Containers (Sequence/Array, Hash)
Container data types, or variables that contain multiple instances of data, exist within FreeMarker. Arrays contain a list of values. Hash containers contain key and value pairs. When storing values in these containers, Built-In methods are able to find, present, add, and remove values.
FreeMarker has multiple Built-Ins to interact with Arrays.
Join:
The Join function turns an array into a string. Two arguments are required when using the Join. The first argument is a string that will separate each value of the array. The second argument is what the function should display if an item of the array is null.
<#assign store_pref = ["Atlanta", "Boston", "New York"]>
${store_pref?join(", ", "-")} → Atlanta, Boston, New York
Another example:
<#assign names = ["John Doe", "Jane Doe", "", "Mike Smith"]>
${names?join(“ - “, “null”)} → John Doe - Jane Doe - null - Mike Smith
Seq_contains:
<#assign fav_catagories = ["Kitchen", "Dining", "Outdoor"]>
<#if fav_categories?seq_contains("Kitchen")>
New deals in Kitchen appliances!
</#if>
Hash containers are similar to Arrays in terms of containing more than one field of data. There is a key field and a value field that are paired together.
There are two Built-In functions for Hash values. The ?keys will display all keys within a Hash. The ?values will display all values within a Hash. Read more on using these functions in the FreeMarker documentation.
Understanding the FreeMarker Data Model
When using a SQL-based Audience (and/or ContextData) for Marketing Campaigns, Accelerator will always extract data and transform that data into an XML document. This includes the Drag-and-Drop Editor for Audiences, which is still connecting and running SQL.
When using an API-based Audience (and/or ContextData), Accelerator expects to be returned appropriately formatted XML or CSV/TSV data (which also gets converted to XML). JSON data response is currently not officially supported through Accelerator Bulk API sources; however, the MessageGears support team can help resolve this if necessary.
For additional detailed information on working with these types of data-models, you can access the Apache FreeMarker Manual section here.
XML Recipient and Supplemental Data
When extracting Recipient data, Accelerator will apply the root <RecipientData> node for the entire audience (in batches of 1 million recipients). Each message recipient gets an individual <Recipient> tag.
For SQL-based audiences, each attribute will be its own tag under the <Recipient>, keeping column casing consistent in the XML:
<Recipient>
<first_name>George</first_name>
<ADDRESS_LINE_1>177 North Ave NW</ADDRESS_LINE_1>
<favoriteItem>Jackets</favoriteItem>
</Recipient>
For SQL-based ContextData, Accelerator will apply the root <ContextData> node for the entire dataset. Each record within ContextData will receive an <Entry> tag, with all the columns in that record as tags under the <Entry>:
<ContextData>
<Entry>
<city>ATL</city>
<price>$55.05</price>
<temperature>72</temperature>
</Entry>
<Entry>
<city>ORL</city>
<price>$42.20</price>
<temperature>88</temperature>
</Entry>
</ContextData>
For API datasources, Accelerator will accept any valid XML or CSV/TSV data. For CSV/TSV data, the resulting XML will be very similar to what has been shown thus far. For custom XML, as long as it’s valid XML (with correct RecipientData and Recipient tags), you can use nested data:
<Recipient>
<EmailAddress>joe.user@gmail.com</EmailAddress>
<first_name>Joe<first_name>
<favorite_restaurants>
<store>
<id>1</id>
<name>Chick-fil-a</name>
<category>QSR</category>
</store>
<store>
<id>2</id>
<name>Culver’s</name>
<category>QSR</category>
</store>
</favorite_restaurants>
</Recipient>
If your database supports semi-structured data (i.e. JSON), Accelerator can handle that after the conversion to XML.
For additional detailed information on Imperative XML Processing, you can access the Apache FreeMarker Manual section here, and for more detailed information on Built-ins for nodes (for XML), the Apache FreeMarker Manual section can be found here.
JSON Recipient and Supplemental Data
When operating in Accelerator, XML is almost exclusively used. If planning to use JSON Recipient and Supplemental Data via the MessageGears Cloud API, please reach out to our support team via support@messagegears.com for more information.
Notable Differences in Templating With XML vs. JSON
Unfortunately, XML has no notion of data types. Therefore, anything that’s XML starts as a string when templating. If you want to perform mathematical operations or number formatting on Recipient or ContextData, you’ll need to convert your value to a number first. For JSON, FreeMarker will take the data type specified.
On the other hand, XML is much better when it comes to referencing and looking up ContextData. FreeMarker effectively converts XML into an extended hash, which makes complex lookups simpler and faster (see Creating and Referencing Supplemental Data). As JSON data typically employs arrays for lists, you have to create loops for lookups.
Creating and Using Snippets
One of the first things to understand what a FreeMarker template is in the context of a MessageGears template, specifically, an email, push, or sms template. It's important to remember that the term “template” is used frequently, and its meaning depends on the context that it is used in. The following are the types of templates most commonly used when discussing templates within Accelerator / MessageGears:
FreeMarker Template: Any text/code file that includes the FreeMarker (FTL) language. Common extension is .ftl. FreeMarker templates can optionally reference (include and/or import) other FreeMarker templates. In MessageGears, the personalization rendering engine for all message content is driven by FreeMarker. (Note: It can also be used in templating SQL for Audience and Supplemental Data)
{Channel} Template: Replace {channel} with one of the following: Email, Push, or SMS.
For Email, the email headers are individual FreeMarker templates: Subject Line, From Address, From Name, Reply-To Address, and Reply-To Name. The header templates are separate from the main HTML template.
For the native Push UI, each individual option field (e.g. title, body, link, etc.) are individual FreeMarker templates.
For the custom Push JSON Payload, it is one FreeMarker template by itself.
For SMS, the short code is optionally a FreeMarker template, and the body is also a template.
Cross-Channel Template: A container that has one or more unique channel templates. Optionally, cross-channel templates can have “Local Snippets”, a library of FreeMarker templates. The cross-channel template is the template that gets attached to a campaign.
Shared Content (Global): A library of FreeMarker templates that is shared across all cross-channel templates. All cross-channel templates and their individual Local Snippet libraries can reference shared content templates. Conversely, a shared content template can reference a local snippet, as long as it exists in the scope at the time of reference by the cross-channel template (i.e. within the cross-channel template local snippet library).
For additional information on Creating Structures for you Template, you can access the Apache FreeMarker Manual section here.
Creating Snippets
First, you want to decide if your snippet will be global (shared across all templates), or local (Local Snippet Library) - it all depends on your use-case. Are you creating something like a header or footer that will be shared across all templates? You definitely want to create global shared content snippets. If you’re looking to build out code blocks that are specific to one program, or will have constant iterations that shouldn’t be shared, then you should create local snippets.
Global Snippets: Content > Global snippets
Local Snippets: Content > Create/Choose Template > + Snippet
Referencing Snippets
There are two ways to reference a snippet: <#include> and <#import>. For now, we are just going to focus on leveraging <#include>. Referencing local and global snippets has one major difference, the word ‘global’ and ‘local’.
Let’s start by building our first local snippet and call it “my_first_snippet”:
<#-- my_first_snippet - Local Snippet Library -->
<p>This is a local snippet</p>
Now, you can reference my_first_snippet in another template, like the main HTML template:
<#-- Main HTML Template -->
<h1>This is a header</h1>
<#include '/local/my_first_snippet/'>
Snippets can also reference other snippets, global or local:
<#-- second_snippet - Global Shared Content Snippet →
<p>First I’m going to print my Global shared content, then include my local content:</p>
<#include '/local/my_first_snippet/'>
Then, in your main HTML template, you would just need your global snippet:
<#-- Main HTML Template -->
<#include '/global/second_snippet/'>
For additional information on working with Referencing Snippets, you can access the Apache FreeMarker Manual section here.
Creating and Using Loops
An important detail to understand when using <#list> in the context of XML data is that it all begins0 as a string. A list requires a sequence to loop through, that can be found using the ?split built-in frequently. It is typically recommend to use a comma as the split delimiter; however, users can also build concatenated strings from your database query with other delimiters, such as %% or ||.
For additional information on Creating and Using Loops, you can access the Apache FreeMarker Manual section here.
Sorting
Creating and Referencing Supplemental Data
Similar to Audiences (Recipient Data), Accelerator can be used to build Supplemental Data XML from a database query. When this feature is used, the following conventions are applied:
- The "root element" of the supplemental data is created with the name "ContextData"
- For each row coming back from the query, an "Entry" element is created.
- Each column returned by the query is returned as an element wrapping the column's value.
For example, the following query:
select id, title, category, desc
from movie
Returns the following XML:
<ContextData>
<Entry>
<id>1</id>
<title>Hook</title>
<category>movie</category>
<desc>A 1991 American fantasy swashbuckler adventure film directed by Steven Spielberg.</desc>
</Entry>
<Entry>
<id>2</id>
<title>The Mighty Ducks</title>
<category>movie</category>
<desc>A self-centered Minnesota lawyer is sentenced to community service coaching a ragtag youth hockey team.</desc>
</Entry>
</ContextData>
This functions well in the template in many situations. However, it can seem difficult to work with if looking for a specific row of data. For example, for a simple way to find and print the supplemental data for a specific offer number above, you’ll need to loop through all the movie query’s results. An example is listed below:
<html>
<#list ContextData.Entry as movie>
<#if movie.id?number == 1>
${movie.title}
</#if>
</#list>
</html>
A more concise, but also complex, way to do this is to use the following reference:
<#assign movie = ContextData['Entry[id="1"]']>
<p>${movie.title}</p>
<p>${movie.otherData}</p>
In each of these examples, “movie” is effectively returning a single-item hash container that can reference any of the fields within the ContextData (i.e. movie) row (or that particular <Entry> in the XML).
Joining Audience and Supplemental Data
Now, let’s say in the Recipient / Audience data there is a field for “favorite_movie_id” that will need to be referenced (instead of just a static “id=1”), then joining that together would look similar to the following:
<#assign movie = ContextData['Entry[id="${Recipient.favorite_movie_id}"]']>
This lookup doesn’t need to just be one ID / column, it could also be a multi-column match / lookup. Here is an example with two, but this lookup can be run with as many columns as necessary:
<#assign movie = ContextData['Entry[id="${Recipient.favorite_movie_id}"][category="${Recipient.favorite_category}"]']>
Multi-Row Result
While a one-to-one match between recipient data and supplemental is a fairly common use-case, users might need to look up something that matches multiple rows of Supplemental Data. With this use-case, a user's lookup result is not just a single hash, but instead a sequence+hash. An example can be found below:
<#assign movies = ContextData['Entry[category="${Recipient.favorite_category}"]']>
In this case, movies would be a sequence+hash with a size of 2. You can then loop through the results to display everything returned in the sequence:
<#list entry as x>
<p>${x.title}</p>
</#list>
Rendered Result:
<p>Hook</p>
<p>The Mighty Ducks</p>
When designing a template, it is best to understand if there is a possibility for a multi-row result. Users can always default to using <#list> on the result as it will support single or multi-row results.
Comments
Please sign in to leave a comment.