Blocks

A block is markup plus a JSON config, bound together. The template engine expands the block into normal sevk Markup before rendering - so blocks compose cleanly with everything else.

Why Blocks Exist

Some email patterns repeat everywhere: a row of social icons, a compliance footer with the unsubscribe link, an app-download badge pair, a header with a logo and a few nav links. Rewriting those with raw <section> and <row> tags every time is tedious and error-prone.

A block is a chunk of template plus a JSON config. The sevk editor exposes blocks as drag-and-drop components with a settings panel that writes the config for you. In code you write the block tag directly.

Shape of a Block

A <block> tag takes a type (identifies which editor panel to show; optional for rendering) and a config (a JSON string). The inner content is the template that gets expanded.

<block type="social-links" config='{"title":"Follow us","iconSize":"32px","links":[
{"platform":"x-twitter","href":"https://x.com/sevk","iconSrc":"https://cdn.sevk.io/icons/x-twitter.png"}
]}'>
<section text-align="{%alignment ?? center%}">
{%#if title%}<paragraph>{%title%}</paragraph>{%/if%}
{%#each links as link%}
<link href="{%link.href%}">
<image src="{%link.iconSrc%}" width="{%iconSize%}" />
</link>
{%/each%}
</section>
</block>
NameTypeDescription
typestringEditor identifier. Not needed by the renderer.
configstringValid JSON. Wrap the attribute in single quotes so you can use double quotes inside. Defaults to {} when omitted; unparsable JSON also silently falls back to {}.

Build the config programmatically with your language's JSON encoder. Every SDK has one. Don't hand-escape JSON.

One real limitation: before parsing, the renderer rewrites every single quote inside the config value to a double quote. A config string containing an apostrophe (like Don't miss out) therefore breaks the JSON and the whole config silently falls back to {}. Keep apostrophes out of config values; put that copy in the template instead.

Node
const config = JSON.stringify({
title: 'Follow us',
iconSize: '32px',
links: [
{ platform: 'x-twitter', href: 'https://x.com/sevk', iconSrc: 'https://cdn.sevk.io/icons/x-twitter.png' }
]
})
const markup = `<block type="social-links" config='${config}'>...</block>`
Python
import json
config = json.dumps({
"title": "Follow us",
"iconSize": "32px",
"links": [{"platform": "x-twitter", "href": "https://x.com/sevk", "iconSrc": "https://cdn.sevk.io/icons/x-twitter.png"}],
})
markup = f"<block type='social-links' config='{config}'>...</block>"

Template Grammar

Block templates use {%...%} delimiters. This is deliberately different from the {{...}} send-time variable syntax, so the two never collide.

The grammar covers: variable injection with fallbacks, {%#if%} with {%else%}, equality (==, !=) and logical operators (&&, ||), and {%#each%} array iteration. Everything is driven by the config object.

Variables

<!-- Plain injection -->
<heading level="1" color="{%titleColor%}">{%title%}</heading>
<!-- With fallback, triggered when the value is missing, null, or empty -->
<section padding="{%padding ?? 20px%}" background-color="{%bg ?? #ffffff%}">
<paragraph color="{%color ?? #333333%}">
{%body ?? Default copy goes here.%}
</paragraph>
</section>

Conditionals

A value is falsy when it is null, undefined, the empty string, false, 0, or an empty array. Everything else is truthy.

Simple if
{%#if logoSrc%}
<image src="{%logoSrc%}" width="120px" />
{%/if%}
if / else
{%#if centered%}
<section text-align="center">
<heading level="1">{%title%}</heading>
</section>
{%else%}
<row>
<column><heading level="1">{%title%}</heading></column>
<column text-align="right">{%subtitle%}</column>
</row>
{%/if%}
Comparison and logical operators
{%#if layout == "centered"%}
<section text-align="center">...</section>
{%/if%}
{%#if status != "draft"%}
<paragraph>Already sent.</paragraph>
{%/if%}
{%#if logoSrc && title%}
<section>
<image src="{%logoSrc%}" />
<heading level="2">{%title%}</heading>
</section>
{%/if%}
{%#if appStoreUrl || googlePlayUrl%}
<paragraph>Download the app.</paragraph>
{%/if%}

Conditionals nest. The engine expands the innermost {%#if%} first and works outward.

Loops

Iterate a config array with {%#each key as alias%} and read item fields as {%alias.field%}. Parent-scope config variables remain visible inside the loop.

Server-rendered markup allows up to 1,000 items per loop and uses a 2,000,000 UTF-16 code-unit expansion budget per block. Exceeding either limit fails rendering instead of silently dropping rows or text. These are server rendering limits, not limits imposed by the SDKs' local renderers; request-body and final message-size limits still apply separately.

{%#each links as link%}
<link href="{%link.href%}">
<image src="{%link.iconSrc%}" width="{%iconSize%}" alt="{%link.platform%}" />
</link>
{%/each%}
<!-- Default alias is "this" -->
{%#each items%}
<paragraph>{%this.name%} - {%this.description%}</paragraph>
{%/each%}
<!-- Guard the iteration -->
{%#if links%}
<section text-align="center">
{%#each links as link%}
<link href="{%link.href%}" color="{%linkColor ?? #666%}">{%link.label%}</link>
{%/each%}
</section>
{%/if%}

Block Config Variables

{%...%} is processed by the block template engine at render time, from your config. It is local to the block. The markup layer itself does not substitute anything else.

<link href="{%href%}" color="{%linkColor ?? #999%}">
{%linkText ?? Unsubscribe%}
</link>

Built-in Blocks

sevk ships a small set of first-party blocks. They are ordinary blocks, nothing magic, and you can copy their templates as starting points for your own.

A centered (or aligned) row of icon links.

<block type="social-links" config='{
"title":"Follow us","titleColor":"#666666",
"links":[
{"platform":"x-twitter","href":"https://x.com/sevk","iconSrc":"https://cdn.sevk.io/icons/x-twitter.png"},
{"platform":"github","href":"https://github.com/sevk","iconSrc":"https://cdn.sevk.io/icons/github.png"}
],
"iconSize":"32px","alignment":"center"
}'>
<section padding="{%padding ?? 12px 0%}" text-align="{%alignment ?? center%}">
{%#if title%}
<paragraph text-align="{%alignment ?? center%}" font-size="14px" color="{%titleColor ?? #666666%}">
{%title%}
</paragraph>
{%/if%}
{%#each links as link%}
<link href="{%link.href%}">
<image src="{%link.iconSrc%}" width="{%iconSize%}" height="{%iconSize%}" alt="{%link.platform%}" />
</link>
{%/each%}
</section>
</block>
NameTypeDescription
titlestringOptional heading above the icons
titleColorstringTitle color (default #666666)
linksrequiredarrayArray of { platform, href, iconSrc }
iconSizestringIcon size with unit. The 24px default is editor-only; the render-time template has no fallback, so always set it
alignmentstringleft, center, right (default center)
paddingstringSection padding (default 12px 0)

sevk hosts ready-to-use icons at https://cdn.sevk.io/icons/<platform>.png for: linkedin, x-twitter, github, discord, instagram, facebook, youtube, tiktok.

Logo, title, and nav links. Two layouts: centered (stacked) or side-by-side (logo left, links right).

<block type="header" config='{
"centered":true,
"logoSrc":"https://example.com/logo.png","logoHref":"https://example.com","logoWidth":120,
"title":"sevk",
"links":[{"label":"Blog","href":"/blog"},{"label":"About","href":"/about"}],
"titleColor":"#1a1a1a","linkColor":"#666666","backgroundColor":"#ffffff"
}'>
{%#if centered%}
<section text-align="center" background-color="{%backgroundColor ?? #ffffff%}">
{%#if logoSrc%}
<link href="{%logoHref ?? #%}">
<image src="{%logoSrc%}" width="{%logoWidth ?? 120px%}" />
</link>
{%/if%}
{%#if title%}
<heading level="3" color="{%titleColor ?? #1a1a1a%}">{%title%}</heading>
{%/if%}
{%#if links%}
<section padding="12px 0 0 0" text-align="center">
{%#each links as link%}
<link href="{%link.href%}" color="{%linkColor ?? #666%}">{%link.label%}</link>
{%/each%}
</section>
{%/if%}
</section>
{%else%}
<section background-color="{%backgroundColor ?? #ffffff%}">
<row vertical-align="middle">
<column width="auto">
{%#if logoSrc%}<image src="{%logoSrc%}" width="{%logoWidth ?? 120px%}" />{%/if%}
{%#if title%}<paragraph font-weight="bold">{%title%}</paragraph>{%/if%}
</column>
<column width="auto" text-align="right">
{%#each links as link%}
<link href="{%link.href%}" color="{%linkColor ?? #666%}">{%link.label%}</link>
{%/each%}
</column>
</row>
</section>
{%/if%}
</block>
NameTypeDescription
centeredbooleantrue for stacked, false for side-by-side. The true default is editor-only; at render time a missing value falls to the side-by-side branch
logoSrcstringLogo image URL
logoHrefstringLogo click URL (default #)
logoWidthnumberLogo width in pixels (default 120)
titlestringBrand text
linksarrayArray of { label, href }
titleColorstringDefault #1a1a1a
linkColorstringDefault #666666
backgroundColorstringDefault #ffffff

app-download

App Store and/or Google Play badges with a heading.

<block type="app-download" config='{
"title":"Grab the app","titleColor":"#5227FF",
"description":"Works on iOS and Android.","descriptionColor":"#666666",
"googlePlayUrl":"https://play.google.com/store/apps/details?id=com.example",
"appStoreUrl":"https://apps.apple.com/app/example",
"badgeWidth":"150px","backgroundColor":"#ffffff"
}'>
<section padding="{%padding ?? 30px 20px%}" background-color="{%backgroundColor ?? #ffffff%}" text-align="center">
{%#if title%}<heading level="3" color="{%titleColor ?? #5227FF%}">{%title%}</heading>{%/if%}
{%#if description%}<paragraph color="{%descriptionColor ?? #666%}" font-size="14px">{%description%}</paragraph>{%/if%}
<paragraph text-align="center">
{%#if googlePlayUrl%}<link href="{%googlePlayUrl%}"><image src="https://cdn.sevk.io/banners/get-it-on-google-play.png" width="{%badgeWidth ?? 150px%}" margin="4px" alt="Get it on Google Play" /></link>{%/if%}
{%#if appStoreUrl%}<link href="{%appStoreUrl%}"><image src="https://cdn.sevk.io/banners/download-on-the-app-store.png" width="{%badgeWidth ?? 150px%}" margin="4px" alt="Download on the App Store" /></link>{%/if%}
</paragraph>
</section>
</block>
NameTypeDescription
titlestringHeading text
titleColorstringDefault #5227FF
descriptionstringSub-heading text
descriptionColorstringDefault #666666
appStoreUrlstringApp Store URL
googlePlayUrlstringGoogle Play URL
badgeWidthstringDefault 150px
backgroundColorstringDefault #ffffff

The compliance footer. The {{unsubscribeUrl}} placeholder is filled in per recipient only when the template is delivered through a broadcast. In other contexts it stays as literal text, so supply your own URL via config if you need it elsewhere.

<block type="unsubscribe-footer" config='{
"text":"You are receiving this because you subscribed.","linkText":"Unsubscribe",
"backgroundColor":"#f8f9fa","textColor":"#999999","linkColor":"#999999","padding":"20px"
}'>
<section padding="{%padding ?? 20px%}" background-color="{%backgroundColor ?? #f8f9fa%}">
<paragraph text-align="center" font-size="12px" color="{%textColor ?? #999999%}">
{%text ?? You received this because you subscribed.%}
</paragraph>
<paragraph text-align="center" font-size="12px" color="{%textColor ?? #999999%}">
<link href="{{unsubscribeUrl}}" color="{%linkColor ?? #999999%}">
{%linkText ?? Unsubscribe%}
</link>
</paragraph>
</section>
</block>
NameTypeDescription
textstringBody message
linkTextstringDefault "Unsubscribe"
backgroundColorstringDefault #f8f9fa
textColorstringDefault #999999
linkColorstringDefault #999999
paddingstringDefault 20px

Custom Blocks

You don't need a type to use a block; drop raw template as inner content with a config and it works. Use this to stamp out repeating patterns in your own codebase.

<block config='{"headline":"Summer sale","discount":"50%","ctaText":"Shop now","ctaUrl":"https://example.com/sale","bg":"#fff3e0"}'>
<section padding="40px 20px" background-color="{%bg ?? #ffffff%}" text-align="center">
<heading level="1" color="#e65100" font-size="36px">{%headline%}</heading>
<heading level="2" color="#bf360c" font-size="48px">{%discount%} off</heading>
<paragraph color="#666666" padding="10px 0 20px 0">Ends Sunday.</paragraph>
<button href="{%ctaUrl%}" background-color="#e65100" color="#ffffff" padding="15px 40px" border-radius="8px">
{%ctaText ?? Learn more%}
</button>
</section>
</block>