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>
| Name | Type | Description |
|---|---|---|
type | string | Editor identifier. Not needed by the renderer. |
config | string | Valid 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.
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>`
import jsonconfig = 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.
{%#if logoSrc%}<image src="{%logoSrc%}" width="120px" />{%/if%}
{%#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%}
{%#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.
social-links
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>
| Name | Type | Description |
|---|---|---|
title | string | Optional heading above the icons |
titleColor | string | Title color (default #666666) |
linksrequired | array | Array of { platform, href, iconSrc } |
iconSize | string | Icon size with unit. The 24px default is editor-only; the render-time template has no fallback, so always set it |
alignment | string | left, center, right (default center) |
padding | string | Section 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.
header
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>
| Name | Type | Description |
|---|---|---|
centered | boolean | true 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 |
logoSrc | string | Logo image URL |
logoHref | string | Logo click URL (default #) |
logoWidth | number | Logo width in pixels (default 120) |
title | string | Brand text |
links | array | Array of { label, href } |
titleColor | string | Default #1a1a1a |
linkColor | string | Default #666666 |
backgroundColor | string | Default #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>
| Name | Type | Description |
|---|---|---|
title | string | Heading text |
titleColor | string | Default #5227FF |
description | string | Sub-heading text |
descriptionColor | string | Default #666666 |
appStoreUrl | string | App Store URL |
googlePlayUrl | string | Google Play URL |
badgeWidth | string | Default 150px |
backgroundColor | string | Default #ffffff |
unsubscribe-footer
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>
| Name | Type | Description |
|---|---|---|
text | string | Body message |
linkText | string | Default "Unsubscribe" |
backgroundColor | string | Default #f8f9fa |
textColor | string | Default #999999 |
linkColor | string | Default #999999 |
padding | string | Default 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>