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.
Keys, screens, and commands may differ in other versions.
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 usesnpxormrml, so preview and export needmjmlfrom the folder'snode_modules(afternpm install) or on your PATH. - Preview uses
mjml -wwriting 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.
| Command | What 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 / --version | Usage (every subcommand also takes --help) / prints dd_emailforge 0.9.2. |
<path> is a template folder or its template.json.
A typical scripted flow:
dd_emailforge init ./ldnddev-monthly --from newsletter
cd ldnddev-monthly && npm install
dd_emailforge validate .
dd_emailforge export . --out dist
| File | Purpose | Commit to git? |
|---|---|---|
template.json | The template. Source of truth. | Yes |
images/ (with .gitkeep) | Local images, used with Ctrl+P and base_url. | Yes |
package.json | Pins mjml. | Yes |
package-lock.json | Created by npm install. | Yes (recommended) |
.gitignore | Ignores .preview/, node_modules/, template.json.backup. | Yes |
template.mjml, template.html | Export output. | Your choice (see below) |
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": [ ] }
}
versionmust be1. Any other value is refused with exit code 2.name,subject,preheader,lang,base_urlare top-level keys. In the app they're edited on the [HEAD] row, together withhead. The exported MJML puts them in<mj-head>(title, preview text, attributes).brandholds the six email-wide tokens above. They're emitted asmj-attributes.headholdstitle,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:
childrenfor mj-section (its columns) and mj-hero (its content blocks),componentsfor mj-column,elementsfor mj-social,socialfor email-footer. - The mj-button label is stored as
content. - Relative image paths need an
https://base_urlat 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):
| Type | Key fields |
|---|---|
email-header | logo_src, logo_alt, logo_href, logo_width, background_color |
email-hero | image_src, image_alt (required if image_src is set), heading, subheading, background_color. No button or background image. |
email-article | image_src, image_alt, title, copy, link_label, link_href, image_position (top, left, right) |
email-cta | heading, copy, button_label, button_href (required), background_color |
email-footer | company_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-wrapperholds sections and heroes.mj-sectionholds 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-columnandmj-herohold leaf blocks: text, image, button, divider, spacer, social, table, navbar, accordion, carousel.mj-table.contentis row markup only (<tr>/<td>), with no outer<table>.
Validation rules worth knowing
- On
mj-button, a pixelwidthsmaller than inner-padding left + right fails validation. - An unknown
versionis refused (exit 2).
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.
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.
Lookup order. The first file found with version: 1 wins:
./dd_emailforge_theme.yml(current directory)$XDG_CONFIG_HOME/ldnddev/dd_emailforge_theme.yml~/.config/ldnddev/dd_emailforge_theme.yml(written by the installer)- 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.
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
.gitignorealready excludes.preview/,node_modules/, andtemplate.json.backup. - Exports: either commit
template.htmlso non-developers can grab the sendable file from the repo, or ignore exports and rebuild them in CI withdd_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 totemplate.htmlare overwritten on the next export.
- 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/Vrebalance every column in the section to equal widths, overwriting customwidthvalues. Set custom widths in each column's FormEdit (Width) after you finish adding or removing columns.- The
xsocial network renders the old Twitter bird icon (emitted as MJMLtwitter). - The preview watches
template.mjml, nottemplate.json. Edits made to the JSON outside the app don't refresh it untiltemplate.mjmlis regenerated. Ctrl+Udoesn't clear a FormEdit field (use End + Backspace).qdoesn't quit (useCtrl+Q). Narrow terminals drop footer keys; below 48 columns only Structure shows.
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.
Previous: Export and send · Next: FAQ and troubleshooting