Post Featured Image

Learn How to Customize Web Store Emails with System Email Templates

This blog post describes a feature available in the 2018.2 release of the commerce application (ie core NetSuite ERP). It is appropriate for SuiteCommerce and all versions of SuiteCommerce Advanced.

One of the exciting features in the 2018.2 platform release for commerce that you may have missed is the introduction of system email templates to web stores. Prior to this release, it was kinda clunky to update your email templates and it was hard to get them to look and work how you want. And this was before you take into account how difficult it can be to get your emails to function cross-client, cross-browser, and cross-device.

What I want to do is talk a bit about what's available with this new release, how you can make some changes to suit your business (and your shoppers!) as well as some general advice around email compatibility. For developers, I think there are three key areas you need to get to grips with:

  1. The reality of email rendering, especially across different email clients
  2. The nature of the templating engine we use for system email templates (FreeMarker)
  3. The objects and fields available to you in each email's context

However, before we do, let's just talk about the update itself first: it was introduced in the core code update to NetSuite, so as long as your account is running 2018.2 or newer, you'll be able to take advantage of this feature. System email templates are not new to NetSuite, but they are new to web stores.

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 to see where it applies to your web store. You'll also need to follow the docs to set them up for your site.

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, 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, take a look at our docs to get started:

I'm going to recommend that you start fresh with copies of our new defaults as they'll have our best practices built into them, where the converted ones probably won't.

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 outside of NetSuite

Using a version control system, such as Git, is something we've covered before; if you're not already using it, I cannot recommend it enough.

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 2018.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:

  1. Customize our default templates
  2. 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 synethetic 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.

Further Reading