HTML Email Pro Pack - convert any page or canvas into an HTML email

It’s always been difficult to send Coda content as a professional HTML email. This pack tackles all of the heavy lifting by converting the HTML Coda exports to email friendly HTML. There are some fun Easter eggs and power user features included as well.

One Simple Formula

CreateEmail([My Page])

Pack: https://coda.io/packs/html-email-pro-43947

Doc: https://coda.io/@ben/html-email-pro

Check out the companion doc for a ton of information on what the pack includes, how to use it, and exactly what’s supported.

  • Facepiles
  • Image Handling for Every Scenario
  • Exhaustive Table Style and Conditional Formatting Support
  • Easy Style Presets, like darkmode
  • Full Power-User Control

ProMode and ColorMap for Power Users

For power users who want full control, all styling configuration attributes are surfaced and configurable with ProMode and JSON. The doc even includes an easy to use JSON builder for this. And if you want absolute control over colors, a ColorMap is also included so you can customize red to your preferred and exact red. This doc also includes an easy JSON builder for this feature.

Give it a try and I’d love feedback!

9 Likes

This is an excellent and powerful pack.
I will take it for a spin and let you know how it goes.

Well done.
:lobster:Max

1 Like

Thank you so much, @Max_OBrien!

I’ve added the Mailgun(Email on Acid powered) results to the doc now. I’ll find some time to tag them better soon, but you can use table search until then. There are 100 device/system/client previews to click through. Only an issue or two on older systems and holding off a bit to see if they even matter. I’ll tag those later as well so they stand out and people don’t have to search for them.

2 Likes

How things render side-by-side


See side-by-side images for all features at What's Supported · HTML Email Pro .

Images in a grid setup

Inline images

Tables

Table conditional formatting

Notes


The pack truncates tables with rowLimit so you never have to worry about sending an email with 50,000 rows on accident. And rowLimit is an optional formula parameter so you customize it to your needs.

There is even an appended last row with a message “showing the first {count} rows” so readers know the table was truncated.

2 Likes

New Release: v1.1.1

Fixed a few bugs where special characters were corrupting output in a few areas. Also cleaned up some tech debt and improved stability and maintainability.

Highlights

  • Special characters in your content (like $, &, <, and quotes) now render exactly as typed.
  • Button labels display character-for-character, including symbols like & and <, with no risk of broken or missing text.
  • Table footer and facepile overflow messages now render as plain text, so apostrophes, ampersands, and other common symbols always display correctly.
  • Custom list marker symbols handle special characters reliably.
  • General internal cleanup for long-term stability and maintainability.
2 Likes

Haven’t tested it out yet but looks like stellar work!!!

Seems like leaving Coda did you well @Ben_Lee :grin: Can’t imagine you making this pack while being officially on the team.

Technical question. Do I understand it correctly that when you pass a page/canvas to a Coda formula (an action formula probably), it implicitly calls ToHtml() on it and passes it as HTML already? And your pack then transforms that HTML? Or do you get to the page’s formatting metadata somehow differently? (i.e. obtain the Slate object and then load tables separately)

P.S. Nvm, read the accompanying doc. Seems like you’re already receiving the Coda-rendered HTML of a page in your pack.

One of the ways you could restore the e.g. columns and callout styles is if you also asked pro users to pass a [Page]._Merge().ToText() of that page, and then figure out proper styling from that data. Just an idea :upside_down_face:

2 Likes

Hi @Paul_Danyliuk!

This uses the same functionality as the ToHtml() pack formula from Eric Koleda’s pack.

I picked this project because my goal was learning more about code building strategies and HTML email is an impossible problem to solve. That means plenty of decisions would have to be made and I’d have to really dig in and understand every aspect. The clearest example of this is Outlook requires MSO conditionals to display a lot of the nicer styles, but Gmail strips all MSO code before sending it. Finding best fit solutions over 30 email clients that all conflict with each other is why no engineers ever wanted to take this on with me, even as a hackathon project, and rightfully so.

But I’m pretty proud of what I was able to accomplish with it. I was also a big part of the performance efforts while I was at Coda, so there are a lot of efficiencies built into this. Like truncating tables before processing all the HTML so the pack doesn’t spin on 50,000 records that aren’t intended to really be emailed. Or creating the JSON override system for configurable variables that’s used both for ProMode as well as for the quick style presets. One system powers multiple features.

I like your idea on using _Merge() to get the extra JSON data about the page or canvas, but It gives me a gut feeling that there could be content collisions or issues under certain circumstances. There’s just something about both pieces being lossy in data and having to be parsed in a way that provides reliable matches, even if the exact same line of content is used repeatedly in a page. I want to hold reliability as a high priority on this project.

I was also careful to use graceful failure rules instead of breaking errors. If a color is passed in that doesn’t match a colormap value, it retains the passed in color instead of erroring out. There are chains of fallbacks too so it’s pretty robust.

The goal wasn’t to offer every feature, it was to choose the fewest number of optional parameters that offered the maximum flexibility and most desired features for end users. The one hard-line rule is that only one required parameter is allowed, keeping it easy for users of any technical skill level.

Definitely test it out and keep posting your thoughts if things come up!

4 Likes

Hi Ben,

This pack looks great. Well done!

My query is related but also unrelated. Hope you don’t mind.

One of my struggles with emails (except formatting) is localization. I use Google translate pack to translate an email split into segments which GT understands and doesn’t plain-text format (e.g. hyperlinks). This is pretty tough because one email I might need to split into 10-15 segments, each in Google Translate formula. I need to be able to select a target language which then processes the EN template to translate before sending.

Do you have any suggestions for an easier approach to translating emails into different languages?

2 Likes

Hi Stefan,

Thanks for checking out the pack!

For your question, I think I would use subtables for this. If I understand this correctly, you’ll have a single email and it needs to be translated based on the receiving person’s preferred language.

Top table - This would be your emails with one row per email.

Secondary table - This would be the connected subtable that would hold the translations from the Google Translate pack. So “Email 1” from the top table would be connected to 10 or 15 rows in this table with each row being a single translation of the email.

Third table - This is your subscribers or sending list with a column that assigns each person a preferred language. When you send from this table, you can set the filter formula for which email to use to be the one from their language.

This might sound like a bit more than you want with table connections, but I don’t think the solution is to try to do this “on the fly”. I would stick with a source of truth email with the content, then a subtable of all the translations for that email content. This avoids a lot of complexity issues that creep in when trying to get too fancy with a solution and it also keeps it dead simple where you see the text that’s being sent for each language.

If I misunderstood, let me know.

1 Like

Hi Ben,

Thanks for the quick and detailed reply. My case requires a bit more variables in the email and for that reason my main struggle is with keeping the formatting (urls, QR code, links to forms, etc) within the translated email. To achieve this I’ve created a complex button formula which includes a pretty long concatenate formula which combines a lot of those interactive elements and static text.

The localization happens by splitting the content by segments in such a way that the non-interactive content is translated with the pack and the interactive elements are kept intact.

While this setup achieves the requirement for a fairly templated email, I would prefer a better formatted emails with more flexible content creation which would then be translated into selected from a list language (not preselected).

Right now I have created template which gets translated into any language Google Translate supports. But user cannot update the text easily, add/insert elements freely and then send it to any selected language.

1 Like

I understand things are complex. I’d still suggest taking the time to make it as templated as possible. This will allow you to present fields for people to edit text more easily. By templated, I mean to standardize what blocks show up in all emails. Then you can pick and choose which blocks things happen to.

Here is a resource I mocked up to show that canvases can be pulled together with the list formula and I also included some fun List() formula hacks that I like to use when combining different blocks of content with different rules.

Your catch is that you’ll likely have rich text converted to plain text when you run it through Google Translate. In that case, you can sub in the Format() formula where text styling might be needed for headers. But if plain text is fine, then what I have can work as it is.

And this is of course a simplified resource, I know you are concatenating more. But I think you’ll see the pattern used and be able to pull the parts of your current formula into it.

You can keep the formula as it is and still use the HTML Email Pro pack with the CreateEmail() formula wrapping it, but a lot of the power of it is being able to edit in a canvas, or at least put things together in a canvas to see the final product.

2 Likes

Hi Ben,

I finally had a chance to look at your response and test out a bit more.

Before I get deeper into my cases, one question - how could I translate "Body Content #2’ and maintain the formatting in it? I tested with a simple Translate() formula and the result is the below:

So, I ran into the case when maybe the only option is to segment it down to a phrase and then combine it somehow…

1 Like

Good question.

This gets back to the “templated” or “standardized” strategy I was talking about. You’ll want to come up with a predictable pattern of “Header”, “Text”, “Image”, “Text”, or whatever your pattern ends up being. Then each of these blocks will get a column.

Then you can set in that List() formula in the resource which blocks run through the translator and also wrap the ones that need header styling in a Format() formula using a template of the header and style you need.

List(
  If([Header 1].IsBlank(),"",Format(H1.Template,thisRow.[Header 1])),
  If(Body.IsBlank(),"",thisRow.Body)
).Filter(CurrentValue.IsNotBlank()).Join(LineBreak())

The part with Format(H1.Template,thisRow.[Header 1]) is referencing a table of “templates” where the canvas column for the “H1” row is just {1} with the H1 styling applied. Format() retains the styling of the template.

I’ve added this to the same resource I posted before so you can try it.

Then your “Body” or paragraph text doesn’t need special styling because it would just be text anyway.

If you have any links or images, you’ll want to put those in their own columns as well because those won’t be translated and need to stay how they are.

Just split this up into blocks that need translating or not and that need header styling or not.

Simplify your emails so that there’s an image at the top, then a header, then the body, then put the links and other images or QR code at the bottom. That way you keep 4 sections and only the middle two need translating. This is where you lose some design freedom, but you open up the possibility for translating to different languages.

1 Like

New Release v1.1.2

This release changes the default colors used to the new Superhuman Docs product colors and includes a few other fixes.

Highlights

  • Updated the default color system to match the latest Superhuman Docs styling, including light and dark modes, buttons, links, tables, highlights, backgrounds, dividers, blockquotes, code blocks, and conditional formatting.
  • Color mapping is now more reliable when using custom colorMap overrides: partial overrides preserve built-in defaults, invalid color values are safely ignored, and unrecognized colors fall back to the originally passed color where appropriate.
  • Expanded regression testing across full pages, canvas columns, facepiles, and every table conditional-formatting style to help prevent rendering regressions.
  • General internal cleanup and documentation improvements for long-term stability and maintainability.
2 Likes

New Release v1.1.3

This release changes default header font-weight to normal from bold to better match the new Superhuman Docs revamp, adds custom unchecked and checked images for checklist bullet markers, and adds the ability to add custom images for bullet markers using ProMode.

Highlights

  • Checklists now use custom checked and unchecked images by default for cleaner, more consistent rendering across email clients.
  • Added support for custom image-based list markers, including Coda-hosted images, with automatic image formatting and safe fallbacks for unsupported markers.
  • Refined header styling and facepile spacing to better match the latest Superhuman Docs appearance.
  • Improved handling of special characters in email titles and preheaders to ensure safe, accurate rendering.
  • Updated the Coda Packs SDK and development dependencies, with expanded regression testing for both email and facepile rendering.
  • General internal improvements to testing, documentation, and long-term maintainability.
1 Like

New Release v1.1.4

This release adds dynamic checkbox coloring that automatically matches text colors, including custom ColorMap settings and conditional formatting. Checkbox images are now hosted through CDN for fast, optimized delivery, with built-in contrast support for light and dark email color schemes.

This release also improves conditional formatting in tables, preserves combined text styles like bold, italic, underline, and strikethrough, and fixes unwanted formatting on list markers for more consistent email rendering.

Highlights

  • Checkboxes now automatically match your content colors, including custom ColorMap settings and conditional formatting, using dynamic image coloring.
  • Improved checkbox visibility across light and dark email color schemes by preserving foreground and background contrast within the images.
  • Extended dynamic image coloring to support custom image-based unordered list markers if those are added later.
  • Improved conditional formatting in tables so text colors, highlights, code, and blockquotes remain readable and consistent with proper contrast.
  • Fixed combined text formatting so bold, italic, underline, and strikethrough styles are preserved when used together.
    • Superhuman Docs still does not pass conditional formatting through for italics. Functionality is there, it’s just a Superhuman Docs bug that isn’t fixed yet.
  • Improved list marker rendering to prevent unwanted underline and strikethrough formatting.
  • General internal improvements to documentation, regression testing, and long-term maintainability.
1 Like