Customize Web Store Emails with FreeMarker
Caution: this content was last updated over 2 years ago
When it comes to customizing email templates for your webstore, I think there are three key areas you need to get to grips with:
- The reality of email rendering, especially across different email clients
- The nature of the templating engine we use for system email templates (FreeMarker)
- The objects and fields available to you in each email's context
All web store emails use a concept called system email templates, and they are used throughout the NetSuite application for other purposes, not just commerce.
Importantly for web stores, it covers four standard order scenarios (received, canceled, approved, fulfilled) as well as some additional ones regarding digital delivery and account notifications. Before beginning, I recommend having a perusal of our documentation.
Approach
You know how we have a list of supported browsers? The reason for this is that these are the browsers we expect your shoppers to use and the ones we, therefore, ensure that our code works on. Well, the variety of email clients is even wider. It would be really difficult (and limiting) to ensure just a few of them and we don't have a list of 'supported' clients.
Why? Standards for email clients don't really exist and it's generally been up to each email client provider to ensure emails rendered correctly, and it's fallen on the community to produce resources that describe the best way to ensure compatibility. If you've worked in email marketing before, or developed emails for them, then you'll know that each email client's rules can wreak havoc on your best-laid plans. In my experience, there has been a gradual convergence, but there can still be issues when you try to make your emails fancy.
Furthermore, another thing to keep in mind is that since around February 2014, mobile devices became the most popular way to open emails. At the time of writing, the iPhone mail client is the single most popular client for opening email. So, not only do you have to think about your emails will look on desktops, but you've got to think about tablets and mobiles too.
Keep It Simple
If you've had your email templates migrated from a previous email templating system, or you're using our defaults, then you'll be able to see the templates in your backend. You'll also be able to use your frontend to send yourself test versions of them (eg placing a test order). Our stock templates (they have names like Standard Web Site Order Received) have some interesting characteristics about them:
- Email templates are essentially just HTML pages
- Styling is inline
- We have specialist markup (FreeMarker)
- No JavaScript, jQuery, calls to external files, etc
There are a number of guides out there for writing cross-platform compatible emails. My favorite is one provided by the author of a popular responsive email framework, Antwort, which starts with "code like it's 1999". Indeed: use simple, bare tags; use tables; use inline styles; don't use JavaScript, shorthand styling, or anything too fancy.
Do you have to use inline styling? No, you don't have to. We and others encourage it because it's the best way to be sure that your emails are going to render the way you want them to. If you don't want to do this yourself, there are tools available online that can convert rules inside style tags into inline styles; I can't comment on how good they are, however.
The NetSuite UI
To begin, you need to either convert your existing emails to system email templates, use our fresh defaults, or create brand new ones.
If you have existing emails, they may already have been converted for you following your upgrade to 2018.2. You can check by going to Setup > Company > Email > System Email Template. You should see something like this:
The ones at the top are the ones automatically migrated, and the ones at the bottom are our new example templates. You can't edit the latter, but you can copy them and make changes to the copies.
If you don't have these, you can see our documentation on how to Create and Modify Email Template.
The Editor
After clicking edit next to one of our defaults (eg the Order Received email), you'll be taken a screen that allows you to edit it; the most prominent thing on the page will be the editor. As a rich text editor, you could edit your templates here using the editor buttons but as developers as I would suggest you use the code editor view because:
- Coding is going to be essential for rich emails
- As this is code, I strongly recommend you version it using a version control system (eg, see Get Started with Version Control (Git))
So, that's the context around system email templates and web store emails in NetSuite, let's look at the other two parts: the templating framework we use and accessing records.
FreeMarker Templating
For the coding aspect, we use something called FreeMarker. It is a Java templating engine and is perfect for something simple like emails. Essentially, we give it access to various objects to do with the customer and their order and then let you mix that with standard HTML markup. Like the Handlebars engine we use on the frontend of SuiteCommerce sites, we can do basic operations such as conditionals and loops.
If you're familiar with JavaScript and Handlebars, working with FreeMarker should just be a case of learning the syntax and getting going.
For example, let's take a look at a greeting that we could send in our Order Received email. We could do something like the following:
Dear <#if customer.isPerson == "T" && ( (customer.firstName)?has_content || (customer.lastName)?has_content )>${customer.firstName} ${customer.lastName}<#elseif customer.isPerson == "F" && (customer.companyName)?has_content>${customer.companyName}<#else>${preferences.naming_customer}</#if>,<br />
If I was the one who placed this order, I would expect it to say Dear Steve Goldberg, before breaking onto a new line. But how does the above code translate into that greeting? Let's take a look.
Note the opening <#if ...
notation: this is how we begin a conditional statement.
You can see that we're querying the customer
object and asking it if isPerson
equals T
(the SuiteScript version of true) and whether this customer has either a first or last name — if they do, then we know we can use it as a greeting. If isPerson
returns F
then we perform another check: (customer.companyName)?has_content
to see if there's a company name we can use instead. Now, this second check is also interesting.
You see, we're using the has_content
'built-in', which performs something kinda similar to a truthy/falsy check in JavaScript. For example, in JavaScript, we could do something like !!customer.companyName
to test whether a company name has a value — in FreeMarker, we use ?has_content
.
Also note that we've wrapped the object and property in parentheses; if the customer
object does not exist, it could cause an error before we even get to check whether the property has a value (the equivalent error in JavaScript would be something like, "Uncaught TypeError: Cannot read property 'companyName' of undefined"). FreeMarker have helpfully programmed in this parentheses trick as a handler for these cases. Thus, customer.companyName?has_content
will return false if customer
doesn't exist, companyName
doesn't exist, or (if they do) if companyName
doesn't have a value. Handy 👍
Finally, we print the values we want using notation such as ${customer.firstName}
— where the dollar symbol indicates an expression (variable) and the curly braces are used to wrap its reference.
For simple email templating, you perhaps do not need to fall too deep down this particular rabbit hole. However, there is a lot that you can do with FreeMarker.
Supported Records and Fields
So, what data is available?
To avoid repetition in this post, I'm just going to direct you to our documentation, which includes a table of all the available records and fields, and in which emails they are applicable to.
However, in the context of this post — which focuses on the Order Received email — we're going to be working with the website, salesorder and customer records. Note that these links point to 2020.1 version of these records; if you're using a newer version of NetSuite than this, you should use the latest version of the documentation for up-to-date details on these records.
Synthetic Fields
One crucial thing worth reiterating, however, is the use of synthetic fields. These are fields not exposed to SuiteScript but available in scriptable templates. They allow you to include additional information that may be useful in emails which is otherwise not available. In our example email, we used ${preferences.naming_customer}
— there is no preferences
object, instead we, NetSuite, have created a synthetic one that allows you to use the Name for Customer fallback preference in our email.
You'll also note that we don't need to build the table for all the items in the order summary table. Again, this is another synthetic field named overviewTable
. Now, if you wish to keep things simple, you can use this and it will return a fully functional table with a number of default columns. However, if you're interested in exercising more control, you can create your own iterator. This is what we do in our new default templates, and I recommend doing it as it will give you greater control over how your emails look.
Iterating Sublists
OK, so we've looked at some basic FreeMarker stuff, as well as some of the records and fields that NetSuite offers — let's go a little deeper and look at some complex examples.
One of FreeMarker's features is the ability to iterate over items (much like you can in Handlebars). As our documentation states, this looks like this:
<!-- Syntax -->
<#list record.sublist as sublistItem>
${sublistItem.field}
</#list>
<!-- Example -->
<#list customer.contactRoles as cr>
${cr.contact}, ${cr.email}
</#list>
All field IDs for the item sublist are available in our documentation, so they should get your mental gears turning.
Getting Started with the Defaults
As I mentioned previously, there are effectively two ways to get started with system email templates for web stores:
- Customize our default templates
- Migrate and customize your original templates
I, personally, would recommend working from our default templates because they come with a lot of best practices and example styling built into them already.
You will need to go into your site's setup record and head over to Emails > Order Emails. In the dropdowns under each sub-section, select the appropriate templates from Select A System Email Template. After saving the record, you can test how they look by performing the actions that trigger them on your site/instance. For example, when I test the order received email I get something like this:
Pretty good, apart from the fact that there's no logo URL: but that's fine, the default is a just #, so I can put in the URL myself.
If you compare what you see with what you see in the template, there are some interesting things worth pointing out.
Iterating Items
We just talked about sublists and how you generate lines for each item, and you can see an example in this email. I think iterating sublist items will probably be the most difficult thing you'll need to do and our example code contains all of the essential parts you'll need to know before tackling system email template design.
I could copy and paste the main block, but there's a lot of markup, so I've created a barebones version (eg no styling) that looks like this:
<#list salesorder.item as itemline>
<tbody>
<tr>
<!-- item image -->
<td>
<#if (itemimages[itemline.item.internalId])?has_content>
<img src="${itemimages[itemline.item.internalId]}">
</#if>
</td>
<!-- item details -->
<td>
<!-- item name with link -->
<div><a href="${itemurls[itemline.item.internalId]}">${itemline.item}</a></div>
<!-- if it's a matrix item -->
<#if (itemline.options)?has_content>
<#assign br = "<br />">
<#list (itemline.options)?split(br) as option>
<#assign label=option?substring(0,option?index_of(":")) value=(option?substring(option?index_of(":")+1))?trim>
<div>${label}: ${value}</div>
</#list>
</#if>
<div>Quantity: ${itemline.quantity}</div>
<div>Unit Price: ${itemline.rate}</div>
</td>
<td>${itemline.amount}</td>
</tr>
</tbody>
</#list>
A lot of new stuff here.
We start off with <#list salesorder.item as itemline>
which allows us to use itemline
in the block as the reference for the current iteratee. As this appears in a table element, each line will generate a table body and table row (admittedly, this may be redundant but it's not incorrect HTML), and then we can start creating the cells for the row. We're going to have two, with the second being split up into sections using divs.
The first cell contains the image, which we reference using the itemimages
object and then passing it an expression that evaluates to the internal ID of the item in context. We use a conditional to check whether it has content, and then call it like this:
<img src="${itemimages[itemline.item.internalId]}">
The square brackets notation lets us reference a specific value to look for, and it will evaluate what we put into it before looking up that key (much like JavaScript does for arrays and index references). After that, we work on the item details. We print the full product name, using a similar technique that we used for the image, except referencing the itemurls
object.
Both the itemimages
and itemurls
objects are special kinds of synthetic field that require an internal ID to function. We use them, as you can see, to return images and URLs using the current item line's internal ID.
Item Name
You may also note that the full item name is returned by default; that is, ${itemline.item}
returns something like SPORTSWEAR : TEES & TANKS : Tranquility Tank : Tranquility Tank-M-YE. Unfortunately, there is currently no clean way to pull the store display name of an item from the system (eg just Tranquility Tank). Thus, we generally recommend just printing the full name, but if you really want the shortened name, you'll have to do some FreeMarker scripting to get what you want.
When approaching how I would do this, I started my personal experiments by trying to do this in JavaScript led me to this:
var name = ' SPORTSWEAR : TEES & TANKS : Tranquility Tank : Tranquility Tank-M-YE ';
name.split(':').slice(-2)[0].trim();
// > "Tranquility Tank"
split()
takes a string then creates an array of values based on the separator we have specified, which is :
.
slice()
then asks to split this array up, returning only the second-to-last value. This assumes that the item name we want is always the second-to-last value, which I think it should be with matrix products; if you don't use matrix options, or mix them with normal products, you'll need to add in some logic to deal with it (or adjust the code).
The returned value will be an array with one value, one which also happens to have two spaces either side of it. So, we specify the first value in the array ([0]
) and then use trim()
to remove the white space, returning our desired value.
Naturally, I'm a little unhappy about this as we're manipulating the value we want three times, but, if this is what you want, then that's what you gotta do.
So, how do we do this in FreeMarker?
Well, but I found success in doing the following:
<!-- At the top of the loop -->
<#assign itemName = itemline.item?split(":")>
<!-- And then in the place you want it rendered -->
${itemName[itemName?size-2]?trim}
Note that we use the #assign
directive. This is how you can assign variables in FreeMarker; it is var
by another name.
OK, so we know how the string is going to look (names separated by colons) so we create a variable that converts it to an array, using :
as a separator. We obviously must do this in the loop that generates the order items and I suggest doing it immediately below the start of the loop, ie where we invoke #list
.
Then, where we want the name to render, we call the variable but we tell it to return only a specific value. The specific value is determined by counting the length (using the ?size
built-in, which is the same as using length
> on an array) and then deducting 2 from it (as we want the second-to-last value). Thus, we're saying itemName[2]
, which gives us the string for the item name. Then we just use ?trim
to remove the white spaces at the ends. Nice!
Keep in mind that I'm not sure this is a foolproof way to do it; there may well be cases where the full item name returned does not contain colons. This code will fail, therefore, if you have something like an item in the top level of your inventory with no matrix options. It will also break should you decide to put colons into your product's names. There are numerous scenarios which make string splitting a bit risky, so I advise you to exercise caution if you plan to use it (and be sure to advise your team too).
Item Options
Anyway, after the product name, we begin iterating the item options, again by first checking that they exist and then using #list
. We're using #assign
again to make the next part easier to work with. You see, you could just print out the item's options in one blob by using ${itemline.options}
but the next bit of code performs some work to split them up, giving you greater freedom over styling.
We turn the options into a list by splitting them up using the line break element that we know it contains. Each option is then split up again, by assigning each part (the label and the value) as a variable by splitting up by substring, kinda like I suggested for the item name. Again, this shouldn't be new for you if you've dabbled in the dark arts of JavaScript. We're basically saying find the colon in the string, and then split it into two from there: the first part is the label and the second part is the value. With that information, we can split them up with HTML elements and apply styling to them.
Finishing Up
From there, it's just a case of including some simple expressions: quantity, rate, total, etc.
You can see that we also do things like check whether a gift certificate is included as part of the order, or whether one was redeemed, as well as things like promo discounts, shipping and billing details, etc.
Styling and Implementing a Responsive Framework
As for styling, there's nothing to add beyond what I said earlier: do stuff inline, if possible, and, if required, use a tool to do it for you. We build them out as tables, as we think that's the most reliable way of doing it. You can use percentages to set the width of the table columns so that they scale nicely on different widths.
If you're thinking about implementing a responsive framework, then my initial impressionI think it could work; I can't see a reason why they wouldn't. You introduce risk, of course, so I would make sure that your emails look fine when rendered on numerous email clients, as well as with HTML disabled. Many email frameworks use style tags and classes, which may be problematic, and keep in mind that a central part of responsive design — media queries — is not universally supported across all clients. So, it's worth getting the core of your emails looking good (eg having a default fixed width table) and then add in styling that can progressively enhance them.
Code samples are licensed under the Universal Permissive License (UPL), and may rely on Third-Party Licenses