Wie wir strukturierte benutzerdefinierte Templates gebaut haben: Nutzer den Stil ändern lassen, ohne beliebiges CSS zu öffnen
Wir hören immer wieder denselben Wunsch von Nutzern: „Das GitHub-Template sieht gut aus, aber kann ich die Überschriftenfarbe auf unsere Markenfarbe ändern? Und den Zeilenabstand etwas vergrößern?”
Klingt nach einem kleinen Feature, aber dahinter steht eine unausweichliche Abwägung: beliebiges CSS öffnen oder nicht?
Dieser Beitrag zeichnet den Weg nach, den wir letztlich eingeschlagen haben — strukturierte Overrides auf bestehenden System-Templates — und warum das besser zu einem Tool wie md-to.com passt, das nach dem Prinzip „ein Inhalt, viele Exportformate” arbeitet.
Zwei verbreitete Extreme, die nicht so recht funktionieren
Die meisten Markdown-Tools handhaben „Custom Styling” auf eine von zwei Arten:
Variante 1: nur feste Templates. Einfach und stabil, aber zu wenig. Nutzer wählen das nächstbeste Template und scheitern dann am letzten Schritt — „nur die Überschriftenfarbe will ich ändern”.
Variante 2: beliebiges CSS. Maximale Freiheit, der Vorschaubereich lässt sich endlos anpassen. Der Haken: gut im Browser heißt nicht gut nach dem Export.
Wir bauen ein Tool, das Markdown nach PDF / Word / HTML / Bild konvertiert, und mehrere dieser Exportpfade verarbeiten gar kein CSS:
- Word (
.docx) verwendet OOXML-Stile. Da gibt es keinen Begriff von „beliebigem CSS”. - PDF-Druck hat eigene Regeln für Pagination und Ränder. Layouts, die rein über Browser-CSS abgestimmt sind, fallen beim Druck oft auseinander.
- Bildexport läuft über Canvas-Screenshots, und einige CSS-Features (Teile von
filter,backdrop-filter) werden nur eingeschränkt unterstützt.
Wenn wir Nutzer beliebiges CSS schreiben lassen, sieht die Vorschau gut aus, aber die Exporte brechen still — diese „was du siehst, ist nicht was du bekommst”-Lücke ist eine sehr schlechte User Experience.
Also haben wir einen dritten Weg gewählt.
Unser Ansatz: Basis-Template + strukturierte Overrides
Die Kernidee in einem Satz: Nutzer wählen zuerst ein System-Template und ändern dann eine kleine Menge strukturierter Felder darüber.
Wir speichern nicht das vollständige Template, sondern „Basis-Template-ID + Overrides”:
{
version: 1,
baseTemplateId: 'classic',
overrides: {
colors: {
heading: '#0F766E'
},
fonts: {
body: 'Noto Sans SC'
},
fontSizes: {
body: 13
},
spacing: {
lineHeight: 1.7
}
}
}
Warum nicht ein vollständiges Template-Snapshot speichern? Drei Gründe:
- Nutzer profitieren, wenn die Basis sich verbessert. Wenn wir später z. B. das Padding der Code-Blöcke in Classic optimieren, übernehmen die Custom-Templates den neuen Wert automatisch — weil das Override dieses Feld nicht berührt.
- Kleines Payload, einfache Validierung auf beiden Seiten. Das Backend muss nur eine Handvoll Whitelist-Felder validieren, kein komplettes Template-Objekt.
- Die Semantik „basiert auf welchem Template” bleibt explizit erhalten. Die UI kann dem Nutzer sagen: „Du hast 3 Änderungen über Classic vorgenommen.”
Hier die aktuelle Editor-UI, die vier hochfrequente Feldgruppen abdeckt: Farben, Schriften, Schriftgrößen und Abstände:

Der Runtime: vom einzelnen Klick bis zum fertigen Export
Das gesamte System besteht aus drei Pipelines.
Pipeline 1: Live-Vorschau
Der Nutzer ändert eine Farbe im Editor und die Vorschau muss sofort aktualisieren:
- Der Editor schreibt die neuen Override-Werte in
localStorage.customDocumentTemplate. - Der Editor löst ein
template-change-Event aus. MarkdownPreviewhört auf das Event und ruftgetActiveTemplate()auf.getActiveTemplate()liestdocumentTemplate(die aktuelle Basis) undcustomDocumentTemplate(die Overrides). Stimmen beide überein, ruft escreateCustomTemplate(baseTemplate, overrides)auf, um sie zu einem vollständigen Template zu mergen.generatePreviewCSS(template)erzeugt die Vorschau-Styles neu.
Die Merge-Logik ist geradlinig — ein flacher Merge pro Feldgruppe:
return {
...baseTemplate,
id: `custom:${baseTemplate.id}`,
colors: {
...baseTemplate.colors,
...(overrides.colors ?? {}),
},
fonts: {
...baseTemplate.fonts,
...(overrides.fonts ?? {}),
},
fontSizes: {
...baseTemplate.fontSizes,
...(overrides.fontSizes ?? {}),
},
spacing: {
...baseTemplate.spacing,
...(overrides.spacing ?? {}),
},
};
Pipeline 2: Als privates Template speichern
Wenn der Nutzer „Template speichern” klickt, packt der Tool-Adapter den aktuellen Zustand:
{
version: 1,
toolSlug,
documentTemplateId,
customDocumentTemplate
}
Dieses Payload landet in saved_templates.settings_json. Beachte, dass documentTemplateId und customDocumentTemplate getrennt gespeichert werden — ein privates Template bedeutet „Y Modifikationen über Template X”, nicht ein von der Herkunft losgelöster Stil-Snapshot.
Pipeline 3: Privates Template anwenden
Wenn der Nutzer eines aus PrivateTemplateSelector auswählt oder über den ?tpl=<id>-Parameter lädt:
- Der Adapter ruft
applySettings(settingsJson)auf. - Hat das Payload ein
customDocumentTemplate, wird es zurück in den localStorage geschrieben; sonst wird es gelöscht. documentTemplateIdwird angewendet.template-changewird ausgelöst, und Vorschau- / Exportpfade sehen alle das gemergte vollständige Template.
Ein leicht zu übersehendes, aber kritisches Detail: Overrides löschen, wenn ein System-Template gewählt wird
Wir haben innerhalb von setActiveTemplate(templateId) eine einzige Zeile hinzugefügt:
localStorage.removeItem('customDocumentTemplate');
Warum? Ohne dies entsteht ein verwirrendes Szenario:
- Nutzer passt Classic mit grüner Überschrift an, das wird im localStorage gespeichert.
- Nutzer wechselt zu Dracula.
- Nutzer wechselt zurück zu Classic — und die Überschrift ist wieder grün, aber er erinnert sich nicht, das geändert zu haben.
Wir definieren „ein System-Template wählen” als expliziten Reset: du wählst ein sauberes Template und bekommst das saubere Template. Um es wieder anzupassen, klicke erneut auf „Anpassen”. Diese Semantik ist viel klarer als „die letzten Änderungen des Nutzers möglichst beibehalten”.
TemplatePickerModal führt dieselbe Aufräumaktion aus, wenn auf „Anwenden” geklickt wird.
Backend-Validierung: die Whitelist ist das Sicherheitsnetz
Das Backend validateTemplateInput() validiert settingsJson.customDocumentTemplate strikt:
customDocumentTemplateist optional; wenn vorhanden, muss es ein Objekt sein.versionmuss1sein (Spielraum für spätere Migrationen).baseTemplateIdmuss ein nicht leerer String sein.overridesmuss ein Objekt sein.- Felder außerhalb der Whitelist werden abgelehnt.
Die Whitelist deckt alles ab, was die UI heute exponiert, mit etwas mehr Spielraum für künftige Felder:
| Gruppe | Erlaubte Felder |
|---|---|
colors | heading, text, link, codeBackground, codeBorder, codeText, quoteText, quoteBorder, quoteBackground, tableHeaderBg, tableHeaderText, tableBorder, tableRowOdd, tableRowEven |
fonts | heading, body, code |
fontSizes | h1, h2, h3, h4, h5, h6, body, code |
spacing | headingBefore, headingAfter, paragraphBefore, paragraphAfter, lineHeight |
Die Whitelist ist das Sicherheitsnetz dieses ganzen Designs: weil wir keine beliebigen Schlüssel akzeptieren, gelangen alte Schlüssel, die Nutzer einmal gespeichert haben, später nicht ins Runtime-Template und verschmutzen die Ausgabe nicht — selbst wenn wir später Renderlogik zu einem Feld hinzufügen oder es umbenennen.
Der Gewinn: Exportpfade müssen „Custom Templates” gar nicht kennen
Nach dem Merge liegt ein vollständiges DocumentTemplate vor, daher muss keine der Downstream-Exportfunktionen geändert werden:
- Vorschau:
generatePreviewCSS(template) - PDF / Druck:
generatePdfStyles()/generatePrintStyles() - HTML:
generateExportHTML() - Rich-Text-Kopie:
generateRichTextHTML() - Word:
templateToDocxStyles(template)
Genau das ist die Belohnung dafür, dass wir beliebiges CSS von Anfang an abgelehnt haben: solange jeder Output weiterhin DocumentTemplate als einheitliche Zwischendarstellung konsumiert, ist formatübergreifende Konsistenz im Wesentlichen kostenlos. Die vom Nutzer gewählte Überschriftenfarbe erscheint identisch in der Browser-Vorschau, im PDF, in Word und in Bildexporten — nicht „ungefähr gleich”, sondern wortwörtlich derselbe Hex-Wert, weil alles aus demselben gemergten Template gelesen wird.
Aktuelle Einschränkungen
Die erste Version ist bewusst eng gehalten:
- Nur hochfrequente Felder sind exponiert; nicht jede Farbe, Schriftgröße oder jeder Abstand ist editierbar.
- Es gibt keinen dedizierten Einstiegspunkt zum „Benennen deines Custom-Templates” — die Benennung läuft weiterhin über den „Template speichern”-Dialog.
- Kein JSON-Import / -Export für Templates.
- Kein beliebiges CSS.
- Die Editor-UI ist ein leichtgewichtiger erster Wurf — gruppierte Panels, voreingestellte Farbpaletten und mobiles Layout haben noch Luft nach oben.
Wohin das geht
Ein paar klare nächste Schritte für diese Struktur:
- Mehr editierbare Felder. Tabellenrahmenfarbe, abwechselnde Zeilenhintergründe, Zitat-Hintergrund, Code-Textfarbe — die Whitelist hat bereits Plätze dafür; es fehlt nur die UI.
- Ein einheitliches Feld-Schema. Frontend-Formular und Backend-Validierung sollen sich eine Definition teilen, damit sie nicht auseinanderdriften.
- Ein „Bearbeiten”-Eintrag in der Workspace-Template-Liste. Aktuell kann man nur eine neue Kopie speichern; ein bestehendes privates Template lässt sich nicht weiterbearbeiten.
- Ein expliziter Flow „System-Template als privates Template duplizieren”. Damit „ich will eines basierend auf Classic privat haben” eine direktere Aktion wird.
- JSON-Import / -Export für Custom-Templates. Nützlich, um Stile im Team zu teilen.
Fazit
Im Rückblick liegt der Schlüssel dieses Designs nicht in dem, was wir gebaut haben — sondern in dem, was wir nicht gebaut haben:
- Wir haben kein beliebiges CSS erlaubt — das hat formatübergreifende Brüche verhindert.
- Wir haben keine vollständigen Template-Snapshots gespeichert — das hat den Vorteil von Basis-Template-Upgrades erhalten.
- Wir haben Overrides beim Wechsel des System-Templates nicht still beibehalten — das hat die Verwirrung „ich habe nichts geändert, aber der Stil ist anders” vermieden.
- Das Backend akzeptiert keine Felder außerhalb der Whitelist — das lässt Raum für die Weiterentwicklung des Designs.
Beschränkungen liefern tatsächlich eine vorhersagbarere Erfahrung. In einem Tool, das mehrere Exportformate abdeckt, ist diese Vorhersagbarkeit mehr wert als Freiheit.
Wenn du auch ein „ein Inhalt, mehrere Exporte”-Produkt baust, schau auf md-to.com vorbei und probier dieses Custom-Template-System aus — sieh, ob es das „genau richtige” Maß an Freiheit ist, nach dem du gesucht hast.
Verwandte Tools
Markdown in Word (.docx) umwandeln – Online & Kostenlos
Markdown in Word-Dokumente umwandeln – kostenlos online. MD zu DOCX mit Echtzeit-Vorschau, Formatierung bleibt erhalten. Ohne Anmeldung, lokal im Browser.
Word in Markdown umwandeln – Kostenloser DOCX-zu-MD-Konverter
Word-Dokumente in Markdown umwandeln – kostenlos online. DOCX zu MD mit Erhaltung von Überschriften, Tabellen, Listen und Formatierung. Ohne Anmeldung.
Markdown in PDF umwandeln – Kostenloser Online-Konverter
Markdown in PDF umwandeln – kostenlos online mit 20+ professionellen Vorlagen. Perfekt für Berichte, Dokumentation und Druck. Ohne Anmeldung, lokal im Browser.
Markdown in LaTeX umwandeln – Kostenloser Online-Konverter
Markdown in LaTeX umwandeln – kostenlos online. MD-Dokumente in TeX-Format für akademische Arbeiten und technische Dokumente transformieren. Ohne Anmeldung.