Skip to content

Layout ​

Übergeben Sie ein JSON-Dokument mit genau einem slot. Vorschau und Export legen die E-Mail des Autors in diese Hülle. Speichern tut das nicht: getContent() ist nur die verfasste Vorlage.

ts
init({ layout?: TemplateContent; sectionWrapper?: boolean })
renderToMjml(content, { layout?: TemplateContent })

Bauen Sie die Hülle mit createSlotBlock() und, für eine Karte, createWrapperBlock(). createBlock('slot') und createBlock('wrapper') werfen. sectionWrapper ist Editor-Chrome und unabhängig von layout. renderToMjml nimmt sectionWrapper nicht entgegen.

Beispiel ​

Grauer Seitenhintergrund, eine Zeile „Im Browser ansehen“, die Autorensektionen in einer weißen Karte, Impressum unter der Karte. Das ist die umschließende Hülle: Vorschau und toMjml() / toHtml() setzen sie zusammen; die Bearbeitungsleinwand und getContent() nicht.

ts
import {
  init,
  createSlotBlock,
  createWrapperBlock,
  createParagraphBlock,
  createDefaultTemplateContent,
} from "@templatical/editor";

const layout = createDefaultTemplateContent();
layout.settings.backgroundColor = "#f3f4f6";
layout.blocks = [
  createParagraphBlock({
    content:
      '<p style="text-align:center"><a href="https://example.com/view">Im Browser ansehen</a></p>',
  }),
  createWrapperBlock({
    styles: {
      backgroundColor: "#ffffff",
      padding: { top: 24, right: 24, bottom: 24, left: 24 },
    },
    borderRadius: 12,
    children: [createSlotBlock()],
  }),
  createParagraphBlock({
    content:
      '<p style="text-align:center"><a href="https://example.com/imprint">Impressum</a></p>',
  }),
];

const editor = await init({
  container: "#editor",
  layout,
  sectionWrapper: false,
});
mj-body                         ← grauer Untergrund (layout.settings.backgroundColor)
  mj-section                    ← Im Browser ansehen
  mj-wrapper                    ← weiße Karte
    [Autorensektionen…]
  mj-section                    ← Impressum

sectionWrapper: false blendet Wrapper hinzufügen an Autorensektionen aus. Sitzt der Slot in einem wrapper, würde das Steuerelement mj-wrapper in mj-wrapper erzeugen, was MJML verbietet. Lassen Sie sectionWrapper weg (oder übergeben Sie true), wenn Sie das Panel wollen; der Editor deaktiviert das Einschalten bei diesem Karten-Layout trotzdem.

Der Vertrag ​

layout ist ein TemplateContent mit genau einem slot.

FlächeMit layout
getContent() / setContent() / load / save / onChange / Verlaufnur verfasste Vorlage
Bearbeitungsleinwandnur verfasste Vorlage
Wrapper-Panel der Sektions-SymbolleisteIm Editor
renderToMjml(content) (ohne layout-Argument)nur verfasste Vorlage
Vorschauleinwandzusammengesetzt
editor.toMjml() / toHtml()zusammengesetzt
RenderPayload.contentzusammengesetzt

init führt validateLayout auf der Hülle aus (nach Merge-Tag-Normalisierung). applyLayout läuft in der Vorschau und bei toMjml / toHtml / renderToMjml(content, { layout }). Es klont: IDs der Inhaltsblöcke bleiben; Layout-Block-IDs sind auf dem Klon neu.

Gespeichertes JSON

getContent(), load, save und die Bearbeitungsleinwand enthalten nie Layout-Blöcke. Versenden Sie mit toMjml() / toHtml(), oder rufen Sie applyLayout auf dem Server auf. Das Ausblenden von Wrapper hinzufügen entfernt section.wrapper nicht aus dem gespeicherten Inhalt.

slot ist die Stelle, an der content.blocks landen. Genau eines im Layout-Baum. Zulässig als Kind von layout.blocks (oberste Ebene) oder von wrapper.children. Unzulässig in einer Sektionsspalte, in einem verschachtelten Wrapper und im Editor-Inhalt. Die Palette lässt ihn aus. Ein slot, der renderToMjml ohne layout erreicht, wirft.

wrapper ist die Layout-Karte: styles.backgroundColor / styles.padding / borderRadius / backgroundImage / fullWidth werden zu mj-wrapper. fullWidth: true reicht über das Fenster des Empfängers, während die Kinder bei settings.width bleiben; ein Eckenradius gilt nicht, solange es an ist. Weglassen oder false hält das Band auf der E-Mail-Breite. In Outlook unter Windows überdeckt die Hintergrundfarbe eines Bands das Hintergrundbild einer darin liegenden Sektion — setzen Sie das Bild auf das Band, oder lassen Sie das Band ohne Farbe. Der Linter des Editors sieht das Layout nicht und kann deshalb nicht davor warnen. Nur im Layout. Im Editor-Inhalt abgelehnt (setContent / load / addBlock / createBlock('wrapper')). Autorinnen nutzen weiterhin section.wrapper als Kurzform pro Sektion. Derselbe Schalter Hintergrund über die volle Breite liegt im Hintergrund-Panel des Wrapper-Blocks.

Headless-applyOperations und template-tools edit adressieren vorhandene Wrapper-Kinder mit parent: { wrapperId: string }; Sektionen ohne section.wrapper dürfen direkte Wrapper-Kinder sein. Sektionen mit section.wrapper müssen auf der obersten Ebene bleiben: addBlock, moveBlock und updateBlock lehnen ihre Platzierung in einem Wrapper ab. Das bearbeitet einen Layout-Baum außerhalb des visuellen Editors. Der visuelle Editor lehnt selbst verfasste Wrapper weiterhin ab. Prüfen Sie ein Layout nach Operationen mit validateLayout; es lehnt auch verschachtelte Sektions-Wrapper ab. setContent prüft selbst nur die Form.

Layout-Factories und -Helfer (createSlotBlock, createWrapperBlock, createParagraphBlock, createDefaultTemplateContent, applyLayout, validateLayout, isSlot, isWrapper, layoutWrapsSlot) werden aus @templatical/editor exportiert. Headless-Rendering kann applyLayout aus @templatical/types ohne den Editor importieren.

validateLayout wirft:

[Templatical] layout: must contain exactly one slot block
[Templatical] layout: slot must be a top-level or wrapper child, not nested in a section
[Templatical] layout: a wrapper cannot contain a wrapper
[Templatical] layout: a wrapper cannot contain a section with its own wrapper (section.wrapper)

setContent / load / addBlock lehnen slot und wrapper im Inhalt ab.

MJML verbietet mj-wrapper in mj-wrapper. Sitzt der Slot in einem Wrapper, durchläuft applyLayout die eingeschleusten content.blocks. Jeder Block, der mj-wrapper erzeugen würde — section.wrapper gesetzt oder type === 'wrapper' — wirft:

[Templatical] layout: a wrapper around the slot cannot contain blocks that emit mj-wrapper (section.wrapper)

Ein Slot in children: [[slot]] ist mj-section in mj-column, was MJML ebenfalls verbietet. Der Slot bleibt Kind des Body oder eines Wrappers.

Kopf und Fuß ​

Keine Karte: der Slot ist Geschwister von Kopf und Fuß. Autor-section.wrapper ist dann ein Geschwister-mj-wrapper unter mj-body — gültig. Lassen Sie sectionWrapper weg, wenn Autorinnen weiterhin Wrapper hinzufügen sollen.

ts
layout.blocks = [
  createParagraphBlock({
    content:
      '<p style="text-align:center"><a href="https://example.com/view">Im Browser ansehen</a></p>',
  }),
  createSlotBlock(),
  createParagraphBlock({
    content:
      '<p style="text-align:center"><a href="https://example.com/imprint">Impressum</a></p>',
  }),
];
mj-body                         ← grauer Untergrund
  mj-section                    ← Im Browser ansehen
  [Autorensektionen…]            ← darf section.wrapper enthalten
  mj-section                    ← Impressum

Im Editor ​

Das Wrapper-Panel ist der Schalter Wrapper hinzufügen in der Sektions-Symbolleiste plus Farbe / Padding / Radius, wenn er an ist. Die Deaktivierung bei Karten-Layout und sectionWrapper sind unabhängig: layout zu setzen blendet das Panel nicht aus. Eine Hülle aus Kopf / Slot / Fuß bietet Wrapper hinzufügen weiterhin an.

Wenn layout gesetzt ist und der Slot in einem wrapper sitzt, kann der Schalter nicht eingeschaltet werden. Darunter steht eine gedämpfte Zeile:

Dieser Editor rahmt die E-Mail bereits ein. Ein zusätzlicher Rahmen für diesen Abschnitt wird nicht unterstützt — Vorschau und Export schlagen fehl.

Hat eine geladene Vorlage bereits section.wrapper, bleibt der Schalter an und aktiviert, damit er ausgeschaltet werden kann. Der Hinweis bleibt.

ts
init({ sectionWrapper?: boolean })
WertPanel
weggelassen / truesichtbar, plus die Deaktivierung bei Karten-Layout oben
falseausgeblendet, auch dort, wo Wrapper hinzufügen zulässig ist (Kopf / Slot / Fuß, oder kein Layout)

sectionWrapper: false entfernt section.wrapper nicht aus dem Inhalt, lehnt updateBlock nicht ab und ändert getContent() nicht. Ausblenden ändert nie einen Wert.

Wenn sectionWrapper === false und die ausgewählte Sektion bereits wrapper hat, wird das Panel weiterhin gerendert und der Schalter bleibt aktiviert, damit er ausgeschaltet werden kann. Ist er aus, verschwindet das Panel. Der Hinweis zum Karten-Layout erscheint weiterhin, wenn das Panel sichtbar ist.

Einstellungen ​

Layout-settings ist ein vollständiges TemplateSettings. Gelesen wird nur backgroundColor.

FeldGewinner
backgroundColorLayout → mj-body
width, fontFamily, textColor, linkColor, linkUnderline, locale, preheaderText, directionInhalt

Inhalts-settings.backgroundColor wird nicht verändert. Die Template-Einstellungen bearbeiten es weiterhin. Es ist nicht mj-body, wenn ein Layout angewendet wird. direction kommt aus der verfassten Vorlage. Layout setzt kein dir.

Sie können die Einstellungen des Inhalts auf das Layout kopieren und backgroundColor überschreiben.

Headless-Nutzung ​

Dieses vollständige TypeScript-Beispiel benötigt @templatical/renderer und @templatical/types. Es erstellt ein Template und ein Layout mit je einer deutschen Version, setzt beide zusammen und rendert sie. Es gibt true true 2 de aus: Das Standard-Rendering enthält das Legal notice des Layouts, das deutsche sein Impressum, die zusammengesetzte Standardversion hat zwei Blöcke (den Titel des Templates im Slot, dann die Fußzeile), und settings.locale der deutschen Zusammensetzung ist de.

ts
import { renderToMjml } from "@templatical/renderer";
import {
  applyLayout,
  applyOperations,
  createDefaultTemplateContent,
  createParagraphBlock,
  createSlotBlock,
  createTitleBlock,
  resolveLocale,
  sourceHash,
  type TemplateContent,
} from "@templatical/types";

// The template and its layout each carry their own German version.
const title = createTitleBlock({ content: "Welcome" });
const email: TemplateContent = createDefaultTemplateContent("Arial", {
  locale: "en",
});
email.blocks.push(title);
const content = applyOperations(email, [
  { op: "addLocale", locale: "de" },
  {
    op: "updateBlockLocale",
    blockId: title.id,
    locale: "de",
    text: {
      content: { value: "Willkommen", sourceHash: sourceHash(title.content) },
    },
  },
]);

const footer = createParagraphBlock({ content: "<p>Legal notice</p>" });
const shell: TemplateContent = createDefaultTemplateContent("Arial", {
  locale: "en",
});
shell.blocks.push(createSlotBlock(), footer);
const layout = applyOperations(shell, [
  { op: "addLocale", locale: "de" },
  {
    op: "updateBlockLocale",
    blockId: footer.id,
    locale: "de",
    text: {
      content: {
        value: "<p>Impressum</p>",
        sourceHash: sourceHash(footer.content),
      },
    },
  },
]);

const mjml = await renderToMjml(content, { layout });
const composed = applyLayout(layout, content);

// One language version of a localized template, as renderToMjml composes it:
const germanMjml = await renderToMjml(content, { layout, locale: "de" });
const german = applyLayout(
  resolveLocale(layout, "de", { missing: "default" }),
  resolveLocale(content, "de"),
);

console.log(
  mjml.includes("Legal notice"),
  germanMjml.includes("Impressum"),
  composed.blocks.length,
  german.settings.locale,
);

Vorschau, toMjml und toHtml setzen zuerst zusammen, dann optional resolvePreview:

base = applyLayout(resolveLocale(layout, locale, { missing: "default" }), resolveLocale(content, locale))
if (resolvePreview) base = await resolvePreview({ content: base, recipient, surface, trigger })

locale ist in der Vorschau die aktive Sprache, sonst die Option locale von toMjml / toHtml, standardmäßig die Standardsprache des Templates. Layout und Template werden getrennt projiziert, das Layout mit missing: "default": Seine Standardsprache muss nicht der des Templates entsprechen, und eine Sprache, die dem Layout fehlt, nutzt die Standardsprache des Layouts. Siehe Lokalisierte Templates.

Die Bearbeitungsleinwand nimmt diesen Pfad nie. Das Zusammensetzen des Layouts ist synchron.

Wenn layout gesetzt ist, ist PreviewResolveContext.content das zusammengesetzte Dokument. Geben Sie diese Form zurück. Nur den inneren content zurückzugeben, lässt die Hülle weg.

renderToMjml(content) ohne layout-Argument ruft applyLayout nicht auf.

Test-E-Mail: payload.content ist die verfasste Vorlage; MJML/HTML aus toMjml / toHtml enthält die Hülle.

Lint läuft über den Editor-Inhalt. Die Hülle wird nicht gelintet.

Ein Versandpfad, der gespeichertes JSON ohne layout neu rendert, lässt die Hülle weg. Übergeben Sie dasselbe layout an renderToMjml (oder rufen Sie applyLayout auf) auf dem Server.

Im Playground ​

Das Setup Layout legt eine Kartenhülle um die E-Mail. Sie erscheint in der Vorschau und im MJML-Export, nie im gespeicherten JSON.

Im Playground öffnen