Deutsch
Installation
Aktive Entwicklung
Templatical wird aktiv weiterentwickelt und veröffentlicht häufig neue Versionen. Die öffentliche API stabilisiert sich — wir folgen SemVer, nutzen Changesets für jedes Release und dokumentieren Breaking Changes im Changelog. Pinnen Sie in Produktion eine Version und beobachten Sie die GitHub-Releases, um aktuell zu bleiben.
Feature-Wunsch oder rauer Kante begegnet? Diskussion eröffnen — Feedback formt die Roadmap.
Voraussetzungen
- Moderner Browser -- der Support hängt vom Mount-Modus ab:
- Standardmodus (
shadowDom: true, Shadow DOM) — Chrome 80+, Edge 80+, Firefox 101+, Safari 16.4+. Firefox- und Safari-Mindestversionen sind durch dieadoptedStyleSheets-API bestimmt, auf die der Shadow-Pfad angewiesen ist. - Opt-out-Modus (
shadowDom: false, Light DOM) — Chrome 80+, Edge 80+, Firefox 80+, Safari 14+. Verwenden Sie diesen Modus, wenn Sie ältere Firefox- oder Safari-Versionen unterstützen müssen oder Ihre Integration Light-DOM-Zugriff auf Editor-Interna benötigt. Siehe den Shadow-DOM-Leitfaden für die Kompromisse.
- Standardmodus (
- Container-Element -- muss eine definierte Höhe haben (der Editor füllt seinen Container aus). Im Standardmodus muss es ein Elementtyp sein, der einen Shadow Root hosten kann (z. B.
<div>,<section>,<article>). Siehe Container-Element-Anforderungen. - Kein
transformund kein Stacking-Kontext auf einem Vorfahren des Containers --transform,filter,perspective,will-change,opacityunter1,isolation,containund positionierte Elemente mitz-indexverändern jeweils, wo die Overlays des Editors gezeichnet oder positioniert werden. Das sind reine CSS-Regeln, keine Templatical-spezifischen Einschränkungen; sie betreffen jede Bibliothek, die Overlays mitposition: fixedpositioniert. Was jede Eigenschaft konkret bricht und wie Sie es umgehen, steht unter Den Editor einbetten. - Keine erforderlichen Peer-Dependencies -- Vue, TipTap und alle internen Bibliotheken sind im Editor gebündelt. Sie müssen weder Vue noch eine andere Framework-Runtime installieren, unabhängig davon, welches Framework Ihre App verwendet. (
@templatical/renderer,@templatical/qualityundpusher-jssind optionale Peers — installieren Sie sie nur, wenn Sie das entsprechende Feature nutzen; siehe Optionale Peers weiter unten.)
Netzwerk-Anfragen
Der Editor sendet keine Anfragen an Templatical. Es gibt keinen Lizenzschlüssel, keine Client-ID, keinen Aktivierungsaufruf, keine Berechtigungsprüfung und keine Telemetrie. Nichts am Editor wird aus der Ferne freigeschaltet oder deaktiviert — eine installierte Kopie funktioniert unbegrenzt weiter.
Genau eine Anfrage an Dritte findet dennoch statt, und Sie sollten sie vor dem Deployment kennen:
| Anfrage | Ausgelöst durch | Wann |
|---|---|---|
https://fonts.bunny.net/css?family=geist:400,500,600 | Ein CSS-@import am Anfang des Editor-Stylesheets | Bei jedem Parsen des Stylesheets, in beiden DOM-Modi |
Geist ist die Standard-UI-Schrift des Editors. Wird die Anfrage zum Laden blockiert oder schlägt sie fehl, funktioniert der Editor normal weiter — der Text fällt lediglich auf die nächste Schriftart im Stack zurück. In zwei Fällen könnte das auffallen:
- Strikte Content Security Policy — eine Richtlinie wie
style-src 'self'blockiert den@import. Ergänzen Siehttps://fonts.bunny.netinstyle-srcundfont-src, oder nehmen Sie die Fallback-Schrift in Kauf. - Air-Gapped- oder Offline-Deployments — die Anfrage schlägt fehl und die Fallback-Schrift wird verwendet.
Damit der Editor nicht auf Geist angewiesen ist, überschreiben Sie das Schrift-Token:
css
.tpl,
#ihr-editor-container {
--tpl-user-font-family: system-ui, sans-serif;
}Der @import bleibt dabei im Stylesheet erhalten, die Anfrage wird also weiterhin versucht. Um sie vollständig zu entfernen, hosten Sie Geist selbst und entfernen den @import in einem Build-Schritt aus Ihrer Kopie von dist/style.css. Die vollständige Schrift-Token-Oberfläche finden Sie unter Theming.
Der Container des Editors
Der Container, den Sie an init() übergeben, unterliegt einigen CSS-Einschränkungen. Ein Vorfahre mit transform, overflow: hidden oder einem eigenen Stacking-Kontext kann die Dialoge des Editors verschieben oder beschneiden.
Was jede Eigenschaft bricht und wie Sie es umgehen, steht unter Den Editor einbetten.
npm
bash
npm install @templatical/editorbash
pnpm add @templatical/editorbash
yarn add @templatical/editorbash
bun add @templatical/editor@templatical/editor ist der visuelle Editor. Um Templates in MJML zu konvertieren, installieren Sie zusätzlich @templatical/renderer:
bash
npm install @templatical/rendererbash
pnpm add @templatical/rendererbash
yarn add @templatical/rendererbash
bun add @templatical/rendererDer Renderer ist optional. Installieren Sie ihn dort, wo Sie MJML-Ausgabe benötigen:
- Im Browser, neben dem Editor – wenn Sie
editor.toMjml()aufrufen, um aus der Sitzung des Nutzers zu exportieren. - In Node.js / auf dem Server – wenn Sie nur gespeichertes Template-JSON haben und es serverseitig in MJML umwandeln möchten. Dafür benötigen Sie den Editor nicht; installieren Sie nur den Renderer.
Wenn Sie editor.toMjml() aufrufen, ohne dass der Renderer installiert ist, wird ein klarer Fehler ausgelöst, der das fehlende Paket benennt.
Paketübersicht
| Paket | Beschreibung | Erforderlich |
|---|---|---|
@templatical/editor | Visueller Drag-and-Drop-Editor und init()-Einstiegspunkt | Ja |
@templatical/types | Gemeinsame TypeScript-Typen, Block-Factory-Funktionen, Type Guards | Automatisch installiert |
@templatical/core | Framework-agnostische Editor-Logik (State, History) | Automatisch installiert |
@templatical/renderer | Rendert Templates zu MJML | Optional – installieren, wo Sie editor.toMjml() (Browser) oder renderToMjml() (Node.js, Server) aufrufen |
@templatical/quality | Template-Linter (Barrierefreiheit, Struktur, Links) für das Issues-Panel des Editors und Headless- / CI-Checks | Optional – installieren, um den Issues-Sidebar-Tab und die Inline-Block-Badges zu aktivieren |
@templatical/media-library | Eigenständiges Medien-SDK (init(), useMediaLibrary, MediaLibraryModal in einer Vue-App). Die Durchsuchen-Oberfläche des Editors ist in @templatical/editor gebündelt. | Optional – nur für eigenständige Nutzung, nicht für init({ media }) oder Clouds Speicher |
@templatical/template-tools | CLI und Bibliothek zum Validieren, Rendern, Bearbeiten, Importieren und Live-Vorschauen von Templates außerhalb des Editors | Optional – per npx ausführen, nichts zu installieren; für CI-Checks, Skripte oder das Erzeugen von Templates ohne gemounteten Editor |
@templatical/import-beefree | Konvertiert BeeFree-JSON-Templates in das Templatical-Format | Optional |
@templatical/import-unlayer | Konvertiert Unlayer-JSON-Design-Templates in das Templatical-Format | Optional |
@templatical/import-html | Konvertiert bestehende HTML-E-Mail-Templates (Tabellen-basiert) in das Templatical-Format | Optional |
@templatical/import-mjml | Konvertiert MJML-E-Mail-Templates in das Templatical-Format | Optional |
@templatical/import-topol | Konvertiert Topol-JSON-Templates in das Templatical-Format | Optional |
@templatical/import-stripo | Konvertiert Stripo-Plugin-HTML und kompilierte Datei→HTML-Exporte in das Templatical-Format | Optional |
@templatical/import-chamaileon | Konvertiert Chamaileon-getDocument()-JSON in das Templatical-Format | Optional |
@templatical/import-easy-email-pro | Konvertiert Easy-Email-Pro-Persist-JSON in das Templatical-Format | Optional |
@templatical/types und @templatical/core sind direkte Abhängigkeiten von @templatical/editor und werden automatisch installiert.
Optionale Peers
Der Editor lädt drei optionale Peers zur Laufzeit per dynamischem import(), abhängig davon, welche Features Sie nutzen:
| Peer | Wann geladen | Installieren, wenn Sie |
|---|---|---|
@templatical/renderer | Erster Aufruf von editor.toMjml() | MJML-Export aus dem Browser benötigen |
@templatical/quality | Beim Mounten des Editors (Issues-Panel) | Barrierefreiheit, Struktur und Link-Lint in der Issues-Sidebar nutzen möchten |
pusher-js | Cloud-Realtime-Verbindung | initCloud() verwenden |
Wenn Sie sie nicht installieren, mountet der Editor trotzdem. Der Issues-Tab von Quality und Pusher bleiben aus. editor.toMjml() wirft einen klaren Fehler, der das fehlende Paket nennt. Durchsuchen braucht kein Extra-Paket — das Modal ist ein Lazy-Chunk des Editors.
Hinweis zur Bundler-Ausgabe
Der Editor funktioniert mit allen modernen Bundlern out of the box — unabhängig davon, welche optionalen Peers Sie installieren, ist keinerlei Konfiguration auf Consumer-Seite erforderlich. Vite, esbuild, Rollup und Rolldown behandeln die optionalen dynamischen Imports stillschweigend. Webpack 5 ist etwas gesprächiger: Es analysiert jeden import()-Aufruf statisch und gibt für jeden nicht installierten optionalen Peer eine harmlose Module not found-Warnung aus. Der Build ist trotzdem erfolgreich und der Editor läuft korrekt — diese Warnungen sind rein kosmetisch.
Wenn Sie eine saubere Webpack-Ausgabe bevorzugen, können Sie die Warnungen optional über ignoreWarnings ausblenden:
js
// webpack.config.js — optional, nur falls die Warnungen stören
module.exports = {
ignoreWarnings: [
{
module: /@templatical[\\/]editor/,
message:
/Can't resolve '(pusher-js|@templatical\/(quality|renderer))'/,
},
],
};Framework-Integration
Templatical wird in jedes beliebige DOM-Element eingebunden. Intern erstellt es seine eigene isolierte Anwendung und funktioniert daher mit jedem Framework – oder ganz ohne Framework.
ts
import { init } from "@templatical/editor";
import "@templatical/editor/style.css";
const editor = await init({
container: "#editor",
onChange(content) {
console.log("Content changed", content);
},
});
// Später, beim Entfernen des Editors:
editor.unmount();tsx
import { useRef, useEffect } from "react";
import { init } from "@templatical/editor";
import "@templatical/editor/style.css";
import type { TemplaticalEditor } from "@templatical/editor";
export function EmailEditor() {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<TemplaticalEditor | null>(null);
useEffect(() => {
if (!containerRef.current) return;
let cancelled = false;
let instance: TemplaticalEditor | null = null;
(async () => {
const ed = await init({
container: containerRef.current,
onChange(content) {
console.log("Content changed", content);
},
});
if (cancelled) {
ed.unmount();
return;
}
instance = ed;
editorRef.current = ed;
})();
return () => {
cancelled = true;
instance?.unmount();
editorRef.current = null;
};
}, []);
return <div ref={containerRef} style={{ height: "100vh" }} />;
}vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { init } from "@templatical/editor";
import "@templatical/editor/style.css";
import type { TemplaticalEditor } from "@templatical/editor";
const container = ref<HTMLElement>();
let editor: TemplaticalEditor | null = null;
onMounted(async () => {
if (!container.value) return;
editor = await init({
container: container.value,
onChange(content) {
console.log("Content changed", content);
},
});
});
onUnmounted(() => {
editor?.unmount();
});
</script>
<template>
<div ref="container" style="height: 100vh" />
</template>svelte
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { init } from '@templatical/editor';
import '@templatical/editor/style.css';
import type { TemplaticalEditor } from '@templatical/editor';
let containerEl: HTMLElement;
let editor: TemplaticalEditor | null = null;
onMount(async () => {
editor = await init({
container: containerEl,
onChange(content) {
console.log('Content changed', content);
},
});
});
onDestroy(() => {
editor?.unmount();
});
</script>
<div bind:this={containerEl} style="height: 100vh;" />ts
import {
Component,
ElementRef,
OnDestroy,
OnInit,
ViewChild,
} from "@angular/core";
import { init } from "@templatical/editor";
import "@templatical/editor/style.css";
import type { TemplaticalEditor } from "@templatical/editor";
@Component({
selector: "app-email-editor",
standalone: true,
template: `<div #editorContainer style="height: 100vh"></div>`,
})
export class EmailEditorComponent implements OnInit, OnDestroy {
@ViewChild("editorContainer", { static: true })
containerRef!: ElementRef<HTMLElement>;
private editor: TemplaticalEditor | null = null;
async ngOnInit(): Promise<void> {
this.editor = await init({
container: this.containerRef.nativeElement,
onChange(content) {
console.log("Content changed", content);
},
});
}
ngOnDestroy(): void {
this.editor?.unmount();
}
}Wenn der Effect erneut läuft, bevor init() auflöst — React StrictMode macht das in der Entwicklung — unmounten Sie die Instanz, die gerade fertig wurde, nicht nur die Ref.
Wichtig
Rufen Sie immer unmount() auf, wenn Sie den Editor von der Seite entfernen. Dadurch werden Event-Listener, Timer und DOM-Elemente aufgeräumt. Dies ist besonders wichtig in Single-Page-Anwendungen, bei denen Komponenten während der Navigation ein- und ausgebunden werden.
TypeScript-Unterstützung
Alle Pakete werden mit vollständigen TypeScript-Typdefinitionen ausgeliefert. Konfigurationsoptionen, Callback-Payloads, Blocktypen und Instanzmethoden sind vollständig typisiert:
ts
import { init, unmount } from "@templatical/editor";
import type {
TemplaticalEditor,
TemplaticalEditorConfig,
} from "@templatical/editor";
import type {
TemplateContent,
Block,
ThemeOverrides,
FontsConfig,
} from "@templatical/types";Release-Tarballs
Jedes GitHub-Release enthält dieselben Tarballs, die auch an npm gehen — einen pro Paket. Installieren Sie daraus, wenn ein Build die Registry nicht erreichen kann oder wenn Ihre Abhängigkeiten aus URLs stammen müssen, die Sie selbst prüfen.
json
{
"dependencies": {
"@templatical/renderer": "https://github.com/templatical/sdk/releases/download/v<version>/templatical-renderer-<version>.tgz"
}
}<version> ist die Paketversion, das Tag dieselbe mit einem vorangestellten v. Jede veröffentlichte Version hat eines auf der Releases-Seite. Es ist dieselbe Datei, die npm ausgeliefert hätte — das Paket verhält sich also identisch.
Drei Dinge sollten Sie wissen:
Pinnen Sie auch die Templatical-Pakete, die Sie indirekt nutzen. Ein Tarball verweist auf seine Geschwisterpakete nur über eine Versionsnummer, Ihr Paketmanager sucht diese Version also weiterhin in der Registry. @templatical/core, @templatical/quality, @templatical/renderer, @templatical/media-library und die Importer hängen alle von @templatical/types ab. Verweisen Sie jedes davon, das Sie einbinden, ebenfalls auf einen Tarball:
yaml
# pnpm-workspace.yaml
overrides:
'@templatical/types': https://github.com/templatical/sdk/releases/download/v<version>/templatical-types-<version>.tgznpm und Yarn erledigen dasselbe über overrides bzw. resolutions in der package.json. @templatical/editor benötigt nichts davon — es bündelt alles, was es verwendet.
Fremdabhängigkeiten stammen weiterhin aus einer Registry. @templatical/types, @templatical/renderer, @templatical/import-beefree, @templatical/import-unlayer, @templatical/import-topol, @templatical/import-chamaileon und @templatical/import-easy-email-pro kommen zur Laufzeit ohne weitere Pakete aus. Die übrigen ziehen Pakete nach, die nicht von uns stammen: @templatical/core benötigt @vue/reactivity, @templatical/quality benötigt htmlparser2, @templatical/import-html, @templatical/import-mjml und @templatical/import-stripo benötigen cheerio und domhandler, und @templatical/media-library benötigt @lucide/vue, @vueuse/core und vue-advanced-cropper. Um diese ohne Registry zu installieren, brauchen Sie auch dafür einen Mirror.
Die Quellcode-Archive auf derselben Seite sind kein Ersatz. „Source code (zip)“ und „Source code (tar.gz)“ sind Momentaufnahmen des Repositories, ebenso wie eine github:templatical/sdk-Abhängigkeit. Keines von beiden enthält ein gebautes dist/, und beide verweisen auf Geschwisterpakete als workspace:*, was außerhalb dieses Repositories ins Leere läuft.
CDN
Wenn Sie keinen Paketmanager verwenden möchten, können Sie den Editor direkt über Script-Tags laden:
html
<link
rel="stylesheet"
href="https://unpkg.com/@templatical/editor/dist/cdn/editor.css"
/>
<script type="module">
import { init } from "https://unpkg.com/@templatical/editor/dist/cdn/editor.js";
const editor = await init({
container: "#editor",
});
</script>
<div id="editor" style="height: 100vh;"></div>Der CDN-Build ist vollständig eigenständig – alle Abhängigkeiten sind gebündelt. Schwere Bibliotheken (TipTap, Vue, Pusher usw.) werden per Code-Splitting in separate Chunks aufgeteilt und bei Bedarf nachgeladen.
Eine Version pinnen
Das Beispiel oben ist absichtlich unversioniert: unpkg löst eine URL ohne Versionsangabe bei jeder Anfrage auf die zuletzt veröffentlichte Version auf — praktisch, um den Editor auszuprobieren, bedeutet aber, dass sich der genaue Code, den Ihre Seite lädt, ändern kann, ohne dass sich auf Ihrer Seite etwas geändert hat. Pinnen Sie in Produktion eine exakte Version, und laden Sie diese von jsDelivr statt von unpkg:
html
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@templatical/editor@<version>/dist/cdn/editor.css"
/>
<script type="module">
import { init } from "https://cdn.jsdelivr.net/npm/@templatical/editor@<version>/dist/cdn/editor.js";
const editor = await init({
container: "#editor",
});
</script>Der Host ändert sich zusammen mit der URL-Form, nicht nur mit der Versionsnummer. Der CDN-Build wird per Code-Splitting in Chunk-Dateien mit Hash-Namen aufgeteilt, die der Einstiegspunkt bei Bedarf nachlädt — Einstiegspunkt und jeder nachgeladene Chunk müssen sich also auf dieselbe veröffentlichte Version auflösen. Der Redirect von unpkg bei einer unversionierten URL stellt das sicher, aber das Anfordern der Chunks einer bereits gepinnten Version über unpkg hat diese gelegentlich mit dem falschen Content-Type und einem fehlschlagenden CORS-Preflight ausgeliefert. jsDelivr liefert die Chunks einer gepinnten Version zuverlässig aus — pinnen Sie also dort, und belassen Sie die unversionierte Form bei unpkg.