dd_emailforge: Build HTML Emails in the Terminal

dd_emailforge: Build HTML

Emails in the Terminal

By Jared Lyvers, ldnddev — August 28, 2026

Why email teams keep fighting their editor

Email marketing still lives in a split. The people who own the campaign sit in a drag-and-drop ESP. The people who care whether the HTML survives Outlook sit in a code editor. The file that actually sends is often a third thing: an export that nobody wants to open again. Brand colors drift. Footers get rewritten by hand. A “quick promo” ships without an unsubscribe that the ESP can even parse.

That split is expensive. Marketers wait on a developer for a padding tweak. Developers wait on a screenshot because the builder will not round-trip cleanly. Designers lose the system the moment someone pastes a one-off module into Mailchimp. We have seen the same campaign rebuilt three times because the source of truth was “whatever is currently in the ESP.”

dd_emailforge is our answer: a terminal email builder that treats the template as a git-backed document, not a canvas trapped in a vendor. You edit a typed template.json in a TUI. The app emits strict MJML. Official MJML 5 compiles the HTML. Preview is desktop and mobile. Export is the file you paste into an ESP — without the preview chrome.

If you run campaigns, design the layout, or compile the HTML, this is for you. Clone it, init a starter, and try a real send on last week’s promo before you rewrite another template in a browser widget.

What dd_emailforge actually is

It is a Ratatui TUI and a small CLI. There is no server in production and no database. One folder is one email. JSON is the source of truth. MJML and HTML are emit targets. There is no MJML round-trip import, on purpose. You do not fork a compiled file and hope the next person can open it.

The TUI is master/detail. Structure on the left is a tree: HEAD, BRAND, BODY, then sections, columns, and components. Details on the right is a full-email blueprint. Click a box to select that node. Press Enter to FormEdit. Press / to insert only the kinds that are legal under the current parent. Navbar links stay under navbar. Accordion elements stay under accordion. A section takes columns, not a stray button sitting in the void.

That last rule is the quiet win for marketing teams. Email HTML is not a free-form page. MJML exists because nested tables, Outlook, and mobile stacking are a real layout language. The insert picker will not let you build an invalid tree. You still choose the copy, the image, and the CTA. You do not spend Friday afternoon finding out why a column refused to stack.

This sits in the same family as our other terminal tools — the same “stay in the shell, keep a file you can version” instinct we wrote about in building better workflows. If you already live in a TUI for git and contrast checks, an email builder that behaves the same way is not a novelty. It is the missing piece.

JSON as source of truth, MJML as the compiler

MJML is still the open-source markup language for responsive email. You write semantic blocks — sections, columns, text, buttons — and the engine compiles them into the table HTML, MSO comments, and Outlook-safe patterns that inboxes actually honor. Drag-and-drop builders hide that. Developer markup makes you type it. dd_emailforge sits in the middle: a form and a tree that write JSON, then official Node mjml (pinned at ^5.4.0) compiles it. We do not use a Rust port. We do not shell out to npx. Discovery is the template’s node_modules/.bin/mjml, then PATH.

Brand tokens live on the template and emit as mj-attributes. Font family, colors, content width — set them once, and components inherit. That is how a designer keeps a system without locking every field in an ESP. Subject, preheader, language, and base_url live on the document too. Relative images in images/ need an https:// base URL on export. Dummyimage placeholders in the starters already qualify.

The pipeline is short on purpose:

template.json  →  template.mjml  →  template.html

Preview compiles into a gitignored .preview/ folder and opens a loopback wrapper at 600px and 320px. Exported HTML has none of that chrome. Autosave rewrites JSON two seconds after a change. Manual save also writes template.json.backup. Undo is capped at 20. The template is a file. Review it in a pull request like any other deliverable.

Info: This is not a campaign platform
dd_emailforge ships one email at a time. No list, no send, no shared brand.json. Pair it with the ESP you already use. The job is a template you can version, preview, and export.

If your stack already ties CMS, CRM, and automation together — the kind of unified campaign work we covered in CMS and marketing automation — the missing piece is often the HTML itself. Keep the send in Klaviyo or Mailchimp. Keep the source in git.

Starters that match real campaigns

Blank canvases waste the first hour. init writes a folder you can open immediately:

dd_emailforge init ./my-email --from welcome
cd my-email && npm install
dd_emailforge tui .

Four starters cover the emails marketing teams actually send. welcome is post-signup (and the default). newsletter is a digest. promo is a sale. transactional is a receipt or reset. Welcome, newsletter, and promo ship an address, an Unsubscribe label, and a Mailchimp-style *|UNSUB|* href. Transactional ships the address and leaves the unsub fields empty so validation stays clean — you should not put a marketing unsubscribe on a password reset.

Welcome also ships Google Font Raleway. The others stay on Arial / Helvetica. Starter images are dummyimage HTTPS placeholders. Swap them in FormEdit. On image URL fields, Ctrl+P opens a picker rooted at images/ so you cannot wander above the template folder. That is a small constraint with a large payoff: the asset that ships is the asset in the repo.

init also writes package.json (MJML 5 pinned, npm not run for you), .gitignore for preview and node_modules, and an images folder. If mjml is already on your PATH, skip npm. Node 20+ is required to preview or export HTML. The TUI will still open chrome without a template; you will get a toast telling you to run init. That is intentional. An empty session is not a fake document.

Ship email templates your team can version

If campaign HTML still lives in an ESP canvas, we can help you put brand-safe, export-ready templates in git and wire them to the stack you already send from.

Edit, insert, preview without leaving the TUI

Most email tools make you choose: visual or code. FormEdit is a form on the selected node. Tab between fields. Cycle enums with left and right. Ctrl+S saves. Esc cancels. You are not hunting XML attributes. You are filling the same fields a designer would see in a properties panel. For example: logo src, alt, href, and width but without a browser tab.

Insert is a fuzzy picker filtered to legal children. Type to filter, Enter to insert. Nested MJML stays nested. A leaf dropped on a section splices into the last column, or wraps a column if BODY is empty. Other tree edits match how people actually rearrange a layout: delete, duplicate, undo, reorder, add or remove a column with equal-percent widths, hop between columns. Below 48 columns of terminal width, Details hides and you get Structure only. Tab toggles pane focus when both panes exist.

Preview is the check marketers actually need. Press p (or run dd_emailforge preview ./my-email --port 8766). Relative image src is rewritten to loopback URLs so local assets render. You get a 600px frame and a 320px frame. That is desktop and a phone-width stack, not a terminal approximation pretending to be CSS. F3 validates. Errors block preview and export. Warnings — unused font, a marketing footer heuristic — toast or print and let you continue. That split matters. A missing unsubscribe on a promo should be loud. A font you imported and never used should not trap the send.

If your team already cares about contrast and readable type in the products those emails promote, this is the same discipline in a different surface. dd_wcag 0.5.0 is how we check color in the terminal. dd_emailforge is how we keep the campaign HTML from becoming a one-off.

Validate and export what your ESP can send

Export is Shift+E in the TUI, or dd_emailforge export ./my-email. It writes template.mjml and template.html next to the JSON, or to --out. The HTML is exactly mjml stdout. No wrapper, no dual iframes, no preview metadata. That is the file you paste into “code your own” in Mailchimp, Klaviyo, HubSpot, or whatever you already pay for.

Validation is structural plus images plus a marketing footer heuristic plus version plus padding. Unknown JSON version is refused at load. Padding follows MJML shorthand: one to four px or % values. Bare numbers save as px. Invalid units fail FormEdit and F3. Relative images on export need base_url. Compile uses official MJML in strict validation. Includes are off. One email, one folder, no surprise partials from another campaign.

That last point is a product decision, not a missing feature. Shared partials are how teams accidentally ship last quarter’s legal footer. If you need includes, MJML can do that in a compiler-only workflow. dd_emailforge is for shipping one email you can read in a PR.

CLI covers the rest of a team workflow without opening the TUI: validate, preview, export, and show to dump JSON. A designer can FormEdit. A developer can script export in CI. A marketing lead can init --from promo, swap the hero copy, preview at 320px, and hand engineering a folder instead of a Figma frame labeled “final_v7.”

Get it from GitHub and try it

The project is MIT licensed: github.com/jaredlyvers/dd_emailforge. The illustrated tutorial in the repo is the same walkthrough we used to write this — setup, install, TUI, keys, and how the screenshots are recaptured from the live renderer.

From a checkout:

./install.sh
dd_emailforge init ./my-email --from welcome
cd my-email && npm install
dd_emailforge tui .

The installer builds a release binary into ~/.local/bin and writes a default theme only if one is missing. Re-run is safe. Uninstall is ./install.sh uninstall. Quit the TUI with Ctrl+Q only. Bare q never quits. F1 is the full key and mouse list.

Give it one real email, not a demo. Init --from promo or newsletter. Put your logo in images/. Set brand colors. Preview at 320px. Export and paste the HTML into the ESP you already use. If F3 blocks, read the first error. That is the ticket you would have gotten after the send.

We built dd_emailforge because campaign HTML should be a document, not a screenshot of a builder. Marketing owns the send. Design owns the system. Developers own the compile. The TUI is where those three jobs can share a file. Pull it, try it, and tell us which starter is still missing from your week.

Until next time, Jared Lyvers

Frequently Asked Questions

Do I need to know MJML to use dd_emailforge?

No. You edit a typed JSON document in a form and a structure tree. The app emits strict MJML and official MJML 5 compiles the HTML. You still benefit if you understand sections and columns, because that is how email layout works — but you are not hand-writing nested tables.

Can I paste the export into Mailchimp, Klaviyo, or HubSpot?

Yes. Export writes template.html with none of the preview chrome. Paste it into the ESP’s “code your own” path. Merge tags such as *|UNSUB|* are already in the marketing starters. dd_emailforge does not send mail.

How do I install dd_emailforge?

Clone github.com/jaredlyvers/dd_emailforge, run ./install.sh, then dd_emailforge init ./my-email --from welcome. Install MJML 5 with npm install in that folder unless mjml is already on your PATH. Open the TUI with dd_emailforge tui ..