sevk Markup

A tag-based email language that compiles to inline-styled, table-based HTML every email client agrees on. You write sections, rows, columns, headings, and buttons; sevk produces the crusty XHTML that Outlook and Gmail both accept.

What sevk Markup Is

Writing email HTML by hand is painful: nested tables, inline styles only, MSO conditionals, no grid, no flex, nothing predictable. sevk Markup is a small vocabulary of tags (<section>, <container>, <row>, <column>, <heading>, <paragraph>, <button>, <image>, <link>, <list>, <divider>, <codeblock>) that compile into exactly that crusty HTML.

You can use it three ways: pass markup to sevk.emails.send() and the SDK renders it to HTML locally before the request goes out, call the SDK renderer directly to produce HTML yourself, or design visually in the sevk editor and export.

Why not React Email / Foundation / MJML

React Email, Foundation for Emails, and MJML already exist and they are good. If you have an engineering team that wants full code-level control over every email (JSX components, a build step, npm dependencies), pick one of those. We use them ourselves.

That said, none of them is universal across stacks. React Email requires React itself: if your backend is Go, Rust, Python, PHP, Ruby, Java, or .NET you cannot use it without standing up a Node service just to render the template. MJML ships official support only for Node and a CLI; the bindings for other languages are community efforts with varying quality and lifecycle. So even when those tools fit one part of your stack, they often do not fit the rest of it.

sevk Markup is built for a different audience and a different workflow. The audience is marketers, designers, founders, AI agents, and polyglot backends. The workflow is: describe the email in plain language, let an AI assistant produce the markup, paste it into the dashboard or the API, ship it. No build step. No JSX runtime. No template repository to clone. The same template renders identically whether the call comes from our Node, Python, PHP, Ruby, Go, Rust, Java, or .NET SDK, because every SDK ships the same renderer.

That goal shapes the design:

Easy to share. A template is a single string. You can drop it into Slack, a doc, a ticket, an email (anywhere) and the recipient can open it directly in sevk. There is nothing to compile and nothing to install.

Easy for AI to write. The tag set is small and regular. An LLM can produce a valid sevk Markup template on the first try, edit one block without touching the rest, and reason about what each tag does without needing to render an MJML AST or step through table-based HTML.

Same output everywhere. Whether the source comes from a human, the visual editor, or an AI, it compiles to the same inline-styled, table-based HTML that Outlook and Gmail both accept.

The bet is that most marketing email should be authored in plain language, refined through AI conversation, and shared as a string. sevk Markup is the substrate for that workflow: a small, predictable surface for both the AI and the human.

Hello, World

<section padding="40px 20px" background-color="#f8f9fa">
<container max-width="600px">
<heading level="1" color="#1a1a1a">Welcome to sevk</heading>
<paragraph color="#666666" font-size="16px">
Thanks for signing up. Tap below to open the dashboard.
</paragraph>
<button
href="https://app.example.com"
background-color="#000000"
color="#ffffff"
padding="12px 24px"
border-radius="8px"
>
Open dashboard
</button>
</container>
</section>

Document Shape

A full template is wrapped in <mail> (or <email>, same thing). The <head> holds document-level settings, the <body> holds the content. If you skip the wrapper, sevk wraps your content for you.

<mail lang="en" dir="ltr">
<head>
<title>Welcome</title>
<preview>Tap below to get started.</preview>
<style>
.muted { color: #999999 }
</style>
<font name="Inter" url="https://fonts.googleapis.com/css2?family=Inter" />
</head>
<body>
<section padding="32px">
<heading level="1">Hello</heading>
</section>
</body>
</mail>

lang and dir flow to the <html> tag in the rendered output. preview becomes the preheader text shown in the inbox list.

Blocks

Blocks package a chunk of markup and a JSON config together, so you can drop a social-icon row or a compliant footer into a template without retyping the same eight nested tags. See the Blocks page for the full list and the template grammar.

<block type="social-links" config='{"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 text-align="{%alignment ?? center%}">
{%#each links as link%}
<link href="{%link.href%}">
<image src="{%link.iconSrc%}" width="{%iconSize%}" alt="{%link.platform%}" />
</link>
{%/each%}
</section>
</block>

Rendering

Every SDK exports a render function that turns markup into email-safe HTML locally. No API call, no network.

Node

import { render } from 'sevk'
const html = render(`
<section padding="32px">
<heading level="1">Hello</heading>
</section>
`)

Python

from sevk.markup import render
html = render("""
<section padding="32px">
<heading level="1">Hello</heading>
</section>
""")

PHP

<?php
use function Sevk\Markup\render;
$html = render(<<<MARKUP
<section padding="32px">
<heading level="1">Hello</heading>
</section>
MARKUP);

Ruby

require 'sevk'
html = Sevk::Markup.render(<<~MARKUP)
<section padding="32px">
<heading level="1">Hello</heading>
</section>
MARKUP

Go

import "github.com/sevk-io/sevk-go/markup"
html := markup.Render(`
<section padding="32px">
<heading level="1">Hello</heading>
</section>
`)

Rust

use sevk::markup;
let html = markup::render(r#"
<section padding="32px">
<heading level="1">Hello</heading>
</section>
"#);

Java

import io.sevk.markup.Renderer;
String html = Renderer.render(
"<section padding='32px'>" +
"<heading level='1'>Hello</heading>" +
"</section>"
);

.NET

using Sevk.Markup;
var html = Renderer.Render("""
<section padding="32px">
<heading level="1">Hello</heading>
</section>
""");

CLI

sevk markup render template.sevk -o output.html

Sending via the API

Pass markup instead of html and the SDK renders it to HTML locally before the request goes out; the API itself only ever sees the rendered html.

import { Sevk } from 'sevk'
const sevk = new Sevk('sevk_full_xxxxxxxx')
await sevk.emails.send({
subject: 'Welcome',
markup: `
<section padding="32px">
<heading level="1">Welcome to sevk</heading>
<paragraph>Thanks for signing up.</paragraph>
</section>
`
})