Creating emails with the Maily API — A how to (part 2)

So last time we got to an awesome email setup with componentized, responsive HTML emails (without hating ourselves) using React. For some more fun, let’s add some additional stuff now!

  • Text emails (we need to send those along anyhow),
  • Localized texts (because your customers may prefer another language),
  • Open tracking in your emails (so you don’t have an ESP do it in exchange for €€€$$$$)

So in case you missed the previous part, check back to that post. Otherwise, you can use the dedicated Github repository to see where we stand.

Adding text versions of your email

You need to add a text version for your email anyhow to prevent them going to Junk Mail directly. So we can let Maily generate such a version as well.

Basically, we just take the HTML versions, and strip all styling (hence some components could be removed altogether). Which is a fairly quick process as one can see in this commit. We also added the invoice to the text/index.js so Maily knows how to render it. We can then supply the same json data to it, and the text only version will be rendered. Note how we simply replaced all of mjml tags with simple div tags.

There is a point in asking why we do this. I will get to this in a bit, but for us the most important parts were:

  • Keep all email generation logic in one place,
  • Reuse translations between both versions for consistency.

So let’s talk localization!

Localization in the emails

Fair enough, many people speak English nowadays. But that does not mean we should just assume everybody does, nor suddenly address them in that language. And that is where localization pops in.

For our internal use cases, we (luckily) did not have to handle too many different languages, so we went with a simple template based system.

So lets start this out by creating a translate.js and translations.js file under components. For convenience in this tutorial, the translations.js file will be really simple:

The translate file will be a bit more difficult though. So let’s formulate some requirements:

  1. We want to be able to insert text values in the localized string,
  2. We want to use easy identifiable parameters for this,
  3. We actually also want to be able to inject other components in a localized string,
  4. We want to error hard if a translation is missing.

Using these requirements, and checking with our designers, we ended up with the following localization format: Attached you will find your invoice for order {:orderId}. Doesn’t get much simpler than that.

The API for translations would therefore become translate(language, key, values) where language is the language identifier, key is the translation key, and values is an object containing a key-value binding between the localization placeholder and the value to be used (where the value is either a string/number or a component).

First we will get the language and key pair, giving us the localization format. If we did not get any, we error. So much for requirement #3.

The next step is to tokenize the string on all translations. For each of these, we need to add the correct value into the new string. However, since we want to be able to return a component as a value, that means we have to return a React component instead of a string! Doing this dynamically is a bit annoying, so depending on whether we will be wrapping a string or a component, we can use one of the following functions:

  1. If we are simply wrapping a string, we put a dummy span around it,
  2. If there is a React component, we clone it, and assign a key.

The key is important here, it allows React to much quicker determine what needs to change (and also shuts up the program output considerably ;).

So let’s get back to the tokenization. After we did this, we end up with a tokenized string. For each section we now check whether this is a placeholder in need to substitution or not. If it is, we check the replacement value. If it is an object it’s a React component, so we push a clone of it to the children. If it was a string, we wrap it, and push that to the children. If it didn’t need translation, it was a string anyway, and we push the wrapped version to the children. Which we can again wrap in a dummy to return (React 15 does not allow us to return arrays, so waiting for React 16 to come along).

This process might sound complicated, but once in code it is easy enough to follow:

Combining all the above we get to the result which can be seen on Github. The visual result looks like this:

Much easier to manage, and the format is simple enough for most parties to work with! Obviously you do not get the full advantages of something like Gettext, but it might suffice for you.

Adding open tracking

Often your ESP might already be able to do this for you, but often you either pay big $$$ or they completely mess up your email by rewriting it. Since doing it ourselves it really, really, easy, lets go for that.

First step: you will need a public service. Often the one requesting the emails from Maily is good enough. This service should generate something unique (I’d go with a UUID) for each email. This is then passed as a prop to maily. In turn, maily will add an 1x1px image to the envelope of the HTML email containing the code. For us, this goes to api.inventid.nl/emails/opened/<uuid>.png which returns a simple 1x1 transparent pixel. However by the fact the image was requested, we know from the request URI which email this corresponded to, and mark it as read. Note this is not a fool proof method (it may fail due to firewalls, text only clients, remote images being disabled, and more). However, it is better than nothing. Plus your ESP is unlikely to do it better than this.

inventid simply uses a trackerUrl which is passed in from the requesting service. If it does not exist, we omit it:

const tracker = this.props.trackerUrl ? <OpenTracker url={this.props.trackerUrl}/> : null;

which simply renders an OpenTracker equivalent to mj-image src={this.props.url} border="none" width="1px", which is injected below the footer on the HTML envelope. The required server code you need to write yourself ;)

Wrap up

In just two sessions, we created an awful lot of stuff:

  1. Componentized HTML emails
  2. Responsive HTML emails
  3. Componentized text emails
  4. Localized emails
  5. Test tooling
  6. Open tracking
  7. All above as a service!

A full repository for these blogs is available on my Github. Maily itself is available as open source from inventids Github!

--

--

Software Engineer. Lead software engineer @ Magnet.me, former CTO @ inventid.nl. General nerd. github.com/rogierslag

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store