Search

dd_emailforge for developers

This page collects everything for developers: running dd_emailforge from the command line, the template.json format, starters, the theme file, and keeping templates in git. For the app itself, see A tour of the app.

How it works

dd_emailforge is a single Rust binary with a Ratatui terminal interface. Each template is a folder with a typed template.json:

template.json  →  template.mjml  →  template.html
   (you edit)      (strict MJML)     (mjml ^5.4.0, Node)
  • dd_emailforge emits strict MJML from the JSON.
  • The official Node MJML 5 compiler (mjml ^5.4.0) compiles it to HTML. dd_emailforge never uses npx or mrml, so preview and export need mjml from the folder's node_modules (after npm install) or on your PATH.
  • Preview uses mjml -w writing into .preview/.

Out of scope in v0.9.2: campaign containers, a shared brand.json, mj-include, MJML/HTML import, and sending or test email.

CLI commands
CommandWhat it does
dd_emailforge [template.json|dir]Open the app on a template. With no path, reopens the last template (stored in ~/.config/ldnddev/dd_emailforge/last_path).
dd_emailforge init <dir> [--from welcome|newsletter|promo|transactional]Create a new template folder from a starter (default: welcome). Writes template.json, package.json, images/, .gitignore, then prints cd <dir> && npm install. Doesn't run npm.
dd_emailforge validate <path>Check a template. Prints nothing and exits 0 when valid; exits 1 on errors or a missing path; exits 2 on an unsupported version. Warnings go to stderr.
dd_emailforge export [--out <dir>] <path>Validate, then write template.mjml and template.html (next to template.json, or into --out, which is created if missing). Exits 1 on errors.
dd_emailforge preview [--port 8766] <path>Serve a browser preview (600px and 320px frames) on loopback. Default port 8766.
dd_emailforge show <path>Pretty-print the template JSON to stdout. Accepts a folder or a template.json.
dd_emailforge --help / --versionUsage (every subcommand also takes --help) / prints dd_emailforge 0.9.2.

<path> is a template folder or its template.json.

A terminal running dd_emailforge validate and export commands on a template folder

A typical scripted flow:

dd_emailforge init ./ldnddev-monthly --from newsletter
cd ldnddev-monthly && npm install
dd_emailforge validate .
dd_emailforge export . --out dist
The template folder
FilePurposeCommit to git?
template.jsonThe template. Source of truth.Yes
images/ (with .gitkeep)Local images, used with Ctrl+P and base_url.Yes
package.jsonPins mjml.Yes
package-lock.jsonCreated by npm install.Yes (recommended)
.gitignoreIgnores .preview/, node_modules/, template.json.backup.Yes
template.mjml, template.htmlExport output.Your choice (see below)
template.json

Top-level shape:

{
  "version": 1,
  "name": "ldnddev Monthly Newsletter",
  "subject": "…",
  "preheader": "…",
  "lang": "en",
  "base_url": "",
  "brand": {
    "font_family": "Arial, Helvetica, sans-serif",
    "text_color": "#1a1a1a",
    "background_color": "#f4f4f4",
    "content_width": 600,
    "button_background": "#FFAF46",
    "button_color": "#0F1114"
  },
  "head": { "title": "…", "breakpoint": "480px", "fonts": [], "json_ld": "", "css": "", "css_inline": false },
  "body": { "background_color": "", "nodes": [ ] }
}
  • version must be 1. Any other value is refused with exit code 2.
  • name, subject, preheader, lang, base_url are top-level keys. In the app they're edited on the [HEAD] row, together with head. The exported MJML puts them in <mj-head> (title, preview text, attributes).
  • brand holds the six email-wide tokens above. They're emitted as mj-attributes.
  • head holds title, breakpoint, fonts, json_ld, css, css_inline.
  • body.nodes[] is the ordered list of top-level blocks.
  • Every node has a kebab-case "type" tag, such as "email-header" or "mj-section".
  • Child keys depend on the parent: children for mj-section (its columns) and mj-hero (its content blocks), components for mj-column, elements for mj-social, social for email-footer.
  • The mj-button label is stored as content.
  • Relative image paths need an https:// base_url at export time.

The complete, validated example is the finished newsletter on Build your first email. The full schema is in docs/SPEC.md and components/*.md in the repo.

Components

Email blocks (body level only):

TypeKey fields
email-headerlogo_src, logo_alt, logo_href, logo_width, background_color
email-heroimage_src, image_alt (required if image_src is set), heading, subheading, background_color. No button or background image.
email-articleimage_src, image_alt, title, copy, link_label, link_href, image_position (top, left, right)
email-ctaheading, copy, button_label, button_href (required), background_color
email-footercompany_name, address_lines[] (required), unsubscribe_label, unsubscribe_href (default *|UNSUB|*), social[], copyright. Emits a divider, social links, and 12px text.

MJML primitives: mj-head, mj-wrapper, mj-section, mj-column, mj-group, mj-hero, mj-text, mj-image, mj-button, mj-divider, mj-spacer, mj-social, mj-table, mj-navbar (+ mj-navbar-link), mj-accordion (+ mj-accordion-element), mj-carousel (+ mj-carousel-image).

mj-social elements[]: each item has name (one of facebook, instagram, linkedin, x, github, youtube, pinterest, google, tumblr, snapchat, vimeo, medium, soundcloud, dribbble, xing, web), href, and optional src (custom icon), alt, background_color, icon_size, padding. A new mj-social has no elements. x is emitted as MJML twitter, so it renders the old Twitter bird icon. Set src to a hosted PNG for an X logo.

Nesting rules

  • email-* blocks only at body level.
  • mj-wrapper holds sections and heroes.
  • mj-section holds columns or groups. A new section has no columns ("children": []). Inserting a leaf on a section puts it in the last column.
  • In the app, a leaf inserted while [BODY] or an email-* block is selected is wrapped in a new mj-section with one 100% mj-column.
  • mj-column and mj-hero hold leaf blocks: text, image, button, divider, spacer, social, table, navbar, accordion, carousel.
  • mj-table.content is row markup only (<tr>/<td>), with no outer <table>.

Validation rules worth knowing

  • On mj-button, a pixel width smaller than inner-padding left + right fails validation.
  • An unknown version is refused (exit 2).
Starters

init --from copies one of four starters: welcome (the default when --from is left out), newsletter, promo, transactional. Starter images are dummyimage.com placeholders; replace them with your own hosted images before sending. The template's name starts as the folder name.

For example, the newsletter starter's body is: email-header, email-hero, email-article ("Story one"), email-article ("Story two"), email-cta ("See all"), email-footer. Its head has subject and title "This week" and preheader "Two reads worth your time."

The ~/.config/ldnddev/dd_emailforge/templates/ folder created by the installer isn't used in v0.9.2.

Theme config (app colors only)

The theme controls the terminal app's colors only. Email styling always comes from [BRAND] in each template. Press F2 in the app to view it.

The Theme window in dd_emailforge showing the app's color settings

Lookup order. The first file found with version: 1 wins:

  1. ./dd_emailforge_theme.yml (current directory)
  2. $XDG_CONFIG_HOME/ldnddev/dd_emailforge_theme.yml
  3. ~/.config/ldnddev/dd_emailforge_theme.yml (written by the installer)
  4. Built-in defaults

The schema is documented in LDNDDEV_TUI_VISUAL_STANDARD.md in the repo. last_path is stored in the config dir too.

Version control

Keep each template folder in git. template.json is the source of truth, and its plain JSON diffs well in reviews.

cd ldnddev-monthly
git init
git add template.json package.json package-lock.json .gitignore images/
git commit -m "ldnddev Monthly Newsletter"
  • The generated .gitignore already excludes .preview/, node_modules/, and template.json.backup.
  • Exports: either commit template.html so non-developers can grab the sendable file from the repo, or ignore exports and rebuild them in CI with dd_emailforge export . --out dist. Pick one per repo and stick to it.
  • Edit templates in dd_emailforge (or template.json), never the exported HTML. There's no import, so hand edits to template.html are overwritten on the next export.
Known limits in v0.9.2
  • Hard dependency on Node mjml. Preview and export fail without it.
  • No send or test email, no MJML/HTML import, and no visual preview inside the terminal (browser only).
  • Undo is capped at 20 steps.
  • C/V rebalance every column in the section to equal widths, overwriting custom width values. Set custom widths in each column's FormEdit (Width) after you finish adding or removing columns.
  • The x social network renders the old Twitter bird icon (emitted as MJML twitter).
  • The preview watches template.mjml, not template.json. Edits made to the JSON outside the app don't refresh it until template.mjml is regenerated.
  • Ctrl+U doesn't clear a FormEdit field (use End + Backspace).
  • q doesn't quit (use Ctrl+Q). Narrow terminals drop footer keys; below 48 columns only Structure shows.
Further reading

The repository ldnddev/dd_emailforge has the full reference: README.md, docs/SPEC.md, components/*.md, and the project site at ldnddev.github.io/dd_emailforge.