Email Layouts

A layout is a subject line and a block of HTML that Leed fills in for each recipient. It is the what the email says half of a send; the who gets it half is a contact group. A layout does not know how it will be used, which is the point: the same one can back a blast today and a form’s response email tomorrow.

Where layouts live

Open the Design tab and pick Emails in the left library panel — the section is labeled Transactional email templates. The panel lists your layouts with a search box and a sort control offering Recently created and Name (A–Z). New email template opens a dialog that asks for one thing: a name. Create it and the canvas opens at /email-templates/<id>, with the layout’s name in the header and the subtitle Email template.

The same list appears, read-only, at Settings → Emails, where each row carries an Open template link back into this canvas. That screen is an inventory; nothing is editable there.

Who can edit one

Listing layouts and picking one from a dropdown needs only contact:read — Read Only and above. Creating, opening and updating a layout needs developer:write, whose minimum role is Content Publisher and which is normally reached through the Developer override rather than through a role. That override is a checkbox on Settings → Team; billing and developer access covers what else it unlocks and why it is granted sparingly.

ActionAPI routePrivilegeEntitlement
List layoutsGET /api/templates/emailcontact:readnone
Create a layoutPOST /api/templates/emaildeveloper:writeCustom themes and layouts
Open a layoutGET /api/templates/email/:emailTemplateIddeveloper:writeCustom themes and layouts
Update a layoutPUT /api/templates/email/:emailTemplateIddeveloper:writeCustom themes and layouts
Send a testPOST /api/email/testcontact:writenone
Delete a layout— no route exists ———

Two things follow from that table and are worth stating plainly. There is no delete. A layout can be renamed and emptied but never removed, so a workspace accumulates layouts and naming them well matters more than it should. And the entitlement is enforced in the CMS, not in the API — the upgrade prompt is what a below-Growth author actually hits, because the access check refuses the route first anyway.

The email layout canvas with the subject field, the Test button and the HTML body in the code editor

Where the entitlement lands on your plan is on feature availability by plan, and what a gate looks like from inside the product is on when a feature is gated.

The layout canvas on a below-Growth workspace, with the upgrade prompt filling the editor area

Editing a layout

The canvas has two fields and one button.

  • Subject — the subject line. It is rendered through the same variable engine as the body, so Hello {{firstName}} works here too.
  • Body — the HTML, edited in a code editor with word wrap on and no minimap.
  • Test — sends the layout to yourself. It force-saves first: pressing Test cancels the pending autosave and writes the current subject and body immediately, so a test is never taken against a stale copy.

Edits autosave one second after you stop typing. There is no Save button and no dirty indicator; if you navigate away inside that one-second window the last keystrokes are lost, which is the one reason to pause before closing the tab.

Because the body is raw HTML you have complete control of the markup — and complete responsibility for it. Email clients are a decade behind browsers: use a table for structure rather than flexbox or grid, put every style inline rather than in a <style> block, give images explicit widths, and assume that anything clever degrades. A layout that renders correctly in the code editor’s preview-less pane and in your own webmail has still only been tested in one client.

Filling in the recipient

These are the values Leed passes to every render.

VariableTypeBraces to useAlways present?What it contains
pixelURL{{{ }}}YesThe per-recipient tracking pixel. Put it in an <img> src
unsubscribeURL{{{ }}}YesThe per-recipient unsubscribe URL. Mandatory — see below
firstNameText{{ }}Yes, may be emptyThe recipient’s first name, or an empty string
lastNameText{{ }}Yes, may be emptyThe recipient’s last name, or an empty string
emailText{{ }}YesThe recipient’s email address
nameText{{ }}YesfirstName lastName, falling back to firstName, then lastName, then email — in that order
homepageURL{{{ }}}YesA tracked link to your site’s home page
assetObjectsee belowNoPresent when the send carries a document — a form’s gated download, or a document you picked in the test dialog
pageObjectsee belowNoOn a blast, present only when the composer’s Page field is set. In a test, always present
recommendationsArray of objectssee belowYes on a real sendThree published pages from your site

The object variables carry these paths. recommendations is an array whose entries have exactly the same shape as page, so iterate it with a section: {{#recommendations}}…{{/recommendations}}.

PathTypeNotes
asset.hrefURLA tracked download link — use triple braces
asset.filenameTextThe stored file name, e.g. whitepaper.pdf
asset.nameTextThe asset’s display name in your library
page.titleText
page.summaryText
page.hrefURLA tracked link — use triple braces
page.featureImage.srcURLAbsolute, on your site domain. Absent when the page has no feature image
page.featureImage.altTextFalls back to unknown when the image carries no alt text
page.publishedAtDateAbsent when the page has no publish date
recommendations[].*—Same fields as page

Text versus URLs: double braces and triple braces

This is the rule with teeth, and it is the one that breaks layouts.

{{ }} HTML-escapes its value. Mustache’s escape set is larger than most people expect — it includes / and = as well as &, <, >, ", ' and a backtick. So a URL written in double braces comes out mangled:

<!-- Wrong: double braces escape the slashes -->
<a href="{{unsubscribe}}">Unsubscribe</a>

renders as

<a href="https:&#x2F;&#x2F;example.com&#x2F;e&#x2F;o&#x2F;b_7fa2&#x2F;9c31d4.unsubscribe">Unsubscribe</a>

A browser will still follow that link, because it decodes the entities when it parses the attribute — which is exactly why the mistake survives every visual check you make. What does not survive it is the unsubscribe check below, which compares raw strings.

The fix is one character. Use triple braces — or the equivalent {{&unsubscribe}} — for every URL:

<!-- Right -->
<a href="{{{unsubscribe}}}">Unsubscribe</a>
<img src="{{{pixel}}}" width="1" height="1" alt="" />

Triple-brace every one of unsubscribe, pixel, homepage, page.href, asset.href and recommendations[].href. Use plain double braces for names, titles, summaries and anything else that is text — escaping is doing its job there, and a contact whose company is written Smith & Co will thank you.

After rendering and after link rewriting, and immediately before handing the message to the mail service, Leed searches the finished HTML for the exact unsubscribe URL it generated for that recipient. If the string is not there, the send throws:

Unsubscribe link not detected in email. emailTemplateId: <id> companyId: <id>

The page a recipient lands on when they click it, and the two templates you can override to brand that page, are described in unsubscribes and opt-outs.

Testing a layout

Test sends the layout to you. The recipient is bound server-side to your signed-in account’s email address; the email field in the dialog is render data, so whatever you type there appears inside the message as the email variable and changes nothing about where the message goes. You cannot use the test dialog to mail a colleague.

The dialog has five fields — firstName, lastName, email, a page picker and an asset picker — and they populate the corresponding variables. A test send is classified transactional: it works on every plan including Free, and it does not touch the marketing allotment.

The Test Email Template dialog with all five fields filled in

After Mustache renders the body, Leed scans it for anchors pointing at your own site and converts each one into a per-recipient tracked URL. You do not have to do anything to opt in — writing an ordinary link is enough.

The scan is deliberately narrow, and everything below is left exactly as you wrote it:

  • Links to any host that is not your site domain (or www. plus your site domain).
  • Links whose path already contains /e/ — already-tracked URLs are never double-wrapped.
  • Relative hrefs. href="/pricing" is not rewritten; write href="https://yoursite.example/pricing" if you want the click counted.
  • Anchors written with single quotes. The scan matches href="…" with straight double quotes only.
  • Anything that is not an <a href> — a bare URL in text, a linked image’s surrounding markup, a button built from a <form>.

The URLs that variables give you (homepage, page.href, asset.href, recommendations[].href, pixel, unsubscribe) are already tracked links and are unaffected by this pass. What the resulting URL is made of, what each hit records, and what tracking cannot tell you are all in tracked links and open tracking.

What happens between Save and Send

flowchart TD
  A["Stored subject + HTML body"] --> B["Mustache render<br/>with this recipient's data"]
  B --> C["Rewrite customer-domain anchors<br/>into per-recipient /e/ URLs"]
  C --> D{"Does the rendered HTML contain<br/>the exact unsubscribe URL?"}
  D -- "no" --> E["Throw:<br/>Unsubscribe link not detected in email<br/>this recipient is never mailed"]
  D -- "yes" --> F["Send from no-reply@ your site domain<br/>return path bouncer+batch=contact"]
  F --> G["Stamp the sent time on<br/>the recipient's row"]

The failure edge is the whole reason this diagram exists. It happens once per recipient, after the batch has already been accepted, and it leaves no trace anywhere in the CMS.

One layout, many uses

A layout does not know how it will be used. That is a feature — it is what lets you pick the same one from a form’s settings and from the blast composer — but it means the layout has to survive contexts it was not written for.

Name layouts by purpose rather than by campaign: Whitepaper delivery, Monthly digest, Webinar follow-up. A name like July send is useless the second time you open the dropdown, and since layouts cannot be deleted, every bad name is permanent.

Keep each layout self-contained, and guard the variables that are not always there. page is absent on a blast where nobody set the Page field, and asset is absent on anything that is not delivering a file. An unguarded {{page.title}} renders as nothing, which is usually fine; an unguarded <a href="{{{page.href}}}">Read the post</a> renders as a link to nowhere, which is not. Wrap optional blocks in a section:

{{#page}}
  <tr><td>
    <a href="{{{page.href}}}" style="color:#c6005c;">{{page.title}}</a>
    <p style="margin:4px 0 0;">{{page.summary}}</p>
  </td></tr>
{{/page}}

A layout attached to a form also delivers the gated asset, which is the one case where asset is reliably present — see response emails and gated downloads.

A minimal layout you can paste in and edit

This renders on every recipient, satisfies the unsubscribe check, and degrades sensibly when page is absent. Replace the colors and the copy; keep the structure and the last two rows.

<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f5f5;padding:24px 0;">
  <tr>
    <td align="center">
      <table role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;font-family:Arial,Helvetica,sans-serif;color:#171717;">
        <tr>
          <td style="padding:24px 32px 8px;">
            <p style="margin:0;font-size:16px;">Hi {{firstName}},</p>
          </td>
        </tr>
        <tr>
          <td style="padding:0 32px 16px;font-size:15px;line-height:22px;">
            <p style="margin:0;">Write the message here.</p>
          </td>
        </tr>
        {{#page}}
        <tr>
          <td style="padding:0 32px 16px;">
            <a href="{{{page.href}}}" style="color:#c6005c;font-size:15px;">{{page.title}}</a>
            <p style="margin:4px 0 0;font-size:13px;color:#525252;">{{page.summary}}</p>
          </td>
        </tr>
        {{/page}}
        {{#asset}}
        <tr>
          <td style="padding:0 32px 16px;">
            <a href="{{{asset.href}}}" style="color:#c6005c;font-size:15px;">Download {{asset.name}}</a>
          </td>
        </tr>
        {{/asset}}
        <tr>
          <td style="padding:16px 32px 24px;font-size:12px;color:#737373;border-top:1px solid #e5e5e5;">
            <a href="{{{homepage}}}" style="color:#737373;">Visit our site</a>
            &nbsp;&middot;&nbsp;
            <a href="{{{unsubscribe}}}" style="color:#737373;">Unsubscribe</a>
          </td>
        </tr>
        <tr>
          <td style="line-height:1px;font-size:1px;">
            <img src="{{{pixel}}}" width="1" height="1" alt="" style="display:block;" />
          </td>
        </tr>
      </table>
    </td>
  </tr>
</table>
ESC