Gespeicherte Blöcke
Gespeicherte Blöcke ermöglichen es Ihren Nutzern, eine Gruppe von Blöcken festzuhalten — einen Header, einen Footer, ein Produktraster, einen CTA — und sie in jede andere Vorlage einzufügen.
Der Editor übernimmt das Erlebnis: eine Speicheraktion an jedem Block, einen durchsuchbaren Browser mit Live-Vorschau, Einfügen an beliebiger Position, Umbenennen und Löschen. Der Speicher liegt bei Ihnen.
Nicht dasselbe wie benutzerdefinierte Blöcke
Benutzerdefinierte Blöcke sind von Entwicklern definierte Blocktypen mit eigener Vorlage und eigenen Feldern. Gespeicherte Blöcke sind Instanzen gewöhnlicher Blöcke, die Endnutzer speichern und wiederverwenden. Beide sind voneinander unabhängig.
Schnellstart
Der mitgelieferte browserlokale Provider braucht kein Backend:
import { init, createLocalStorageSavedBlocksProvider } from '@templatical/editor';
const editor = await init({
container: '#editor',
savedBlocks: createLocalStorageSavedBlocksProvider(),
});Einträge landen im localStorage unter templatical:saved-blocks. Geeignet für Demos, Prototypen und ein einzelnes Gerät — sie liegen in einem Browserprofil, synchronisieren nicht und verschwinden, wenn die Websitedaten gelöscht werden. Für alles darüber hinaus liefern Sie einen Provider.
Eigenen Speicher anbinden
savedBlocks akzeptiert jedes Objekt, das SavedBlocksProvider implementiert — vier Mitglieder. list ist eine Methode; jede Mutation ist entweder eine Funktion oder false:
interface SavedBlocksProvider {
list(params?: { search?: string; category?: string }): Promise<SavedBlock[]>;
create: false | ((input: SavedBlockInput) => Promise<SavedBlock>);
update: false | ((id: string, patch: SavedBlockPatch) => Promise<SavedBlock>);
delete: false | ((id: string) => Promise<void>);
}false bedeutet, dass die aktuelle Person diese Aktion nicht ausführen darf; der Editor blendet das Bedienelement aus.
Warum erforderlich und nicht optional
Eine optionale Methode würde „Ich habe mich gegen delete entschieden" nicht von „Ich habe delete noch nicht geschrieben" unterscheidbar machen. Ein false entsteht nicht durch Vergessen. Jeder speichernde Provider in diesem Abschnitt ist gleich geformt.
Eine minimale REST-Implementierung:
import { init } from '@templatical/editor';
import type { SavedBlocksProvider } from '@templatical/editor';
const json = async (res: Response) => {
if (!res.ok) throw new Error(`Anfrage für gespeicherte Blöcke fehlgeschlagen: ${res.status}`);
return res.json();
};
const savedBlocks: SavedBlocksProvider = {
// Geben Sie alles zurück, was die aktuelle Nutzerin sehen darf — grenzen Sie
// hier nach Nutzer, Team oder Konto ab. Der Editor ruft diese Methode ohne
// Argumente auf und filtert im Browser.
list: async () => {
const res = await fetch('/api/saved-blocks');
return json(res);
},
create: async (input) => {
const res = await fetch('/api/saved-blocks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
return json(res);
},
update: async (id, patch) => {
const res = await fetch(`/api/saved-blocks/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
});
return json(res);
},
delete: async (id) => {
const res = await fetch(`/api/saved-blocks/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(`Löschen fehlgeschlagen: ${res.status}`);
},
};
await init({ container: '#editor', savedBlocks });Die Datenstruktur
interface SavedBlock {
id: string; // vom Provider zugewiesen, von create() zurückgegeben
name: string;
content: Block[]; // Blöcke der obersten Ebene; eine Section enthält ihre eigenen Kinder
category?: string; // optional — freie Gruppierung, steuert den Filter im Browser
canUpdate?: boolean; // optional — fehlt = erlaubt; false verbietet
canDelete?: boolean;
createdAt?: string; // optional — nur Anzeige, ohne Einfluss auf die Reihenfolge
updatedAt?: string;
}- Die
idkommt vom Provider. Der Editor erzeugt nie eine eigene, sondern verwendet, wascreate()zurückgibt. - Umbenennen mit
update(id, { name }), umkategorisieren mitupdate(id, { category }). Für beides gibt es keine separate Methode —updatenimmt ein partielles Patch-Objekt. - Die Reihenfolge kommt vom Provider. Der Editor stellt die Reihenfolge von
list()dar und sortiert nie um; Filtern grenzt ein, ohne umzuordnen. Sortieren Sie serverseitig, bevorlist()zurückgibt. - Das Filtern geschieht im Editor. Suchfeld und Kategoriefilter arbeiten im Speicher auf dem, was
list()zurückgegeben hat.list()akzeptiert{ search, category }, doch der Editor sendet es nie — dieser Weg existiert für Aufrufer, dieuseSavedBlocksdirekt ansteuern (siehe Headless-Nutzung). Welche Einträge jemand überhaupt sehen darf, wird inlist()entschieden. - Zeitstempel dienen nur der Anzeige. Jeder Eintrag zeigt eine relative Angabe wie „vor 5 Min." (aus
updatedAt, ersatzweisecreatedAt), das absolute Datum beim Überfahren. Lassen Sie beide weg, entfällt die Angabe.
Berechtigungen steuern
Eine ganze Fähigkeit vorenthalten, indem Sie false statt einer Funktion übergeben. Der Editor blendet aus, was er nicht kann — keine Lesezeichen-Aktion an Blöcken, wenn create aus ist (und damit gar kein Speicherablauf), kein Umbenennen bei update: false, kein Löschen bei delete: false.
const savedBlocks: SavedBlocksProvider = {
list: async () => {
const res = await fetch('/api/saved-blocks');
return json(res);
},
// Diese Person darf hinzufügen, aber Bestehendes nie ändern oder entfernen.
create: (input) => post('/api/saved-blocks', input),
update: false,
delete: false,
};Einen einzelnen Eintrag ausnehmen, indem Sie canUpdate / canDelete mitliefern. Fehlt der Wert, ist die Aktion erlaubt — die Felder dienen ausschließlich dem Verbieten und werden also nur bei den Ausnahmen gesetzt. Der Provider liefert sie, wo die Antwort ohnehin bekannt ist; der Editor ermittelt nichts selbst und zieht die Angabe des Providers nicht in Zweifel.
[
{ "id": "1", "name": "Mein Header", "content": [] },
{ "id": "2", "name": "Team-Footer", "content": [], "canUpdate": false, "canDelete": false }
]Damit entsteht eine Bibliothek, in der die Person den eigenen Eintrag bearbeiten und den geteilten nur einfügen kann. Die beiden Hebel greifen nur in einer Richtung zusammen: canUpdate: true kann ein vom Provider als false übergebenes update nicht wieder aktivieren — die Fähigkeit hat Vorrang.
Eine schreibgeschützte Bibliothek
Setzen Sie alle drei auf false, erhalten Sie eine kuratierte Bibliothek, die Nutzer durchsehen, in der Vorschau ansehen und einfügen, aber nie verändern können:
const savedBlocks: SavedBlocksProvider = {
list: async () => {
const res = await fetch('/api/saved-blocks');
return json(res);
},
create: false,
update: false,
delete: false,
};Das Einfügen funktioniert weiterhin, denn es berührt ausschließlich die Arbeitsfläche — den Provider erreicht dabei nichts. list ist das einzige Mitglied, das sich nicht abschalten lässt; ohne es hätte die Funktion nichts zu zeigen.
Keine Sicherheitsgrenze
Ein ausgeblendetes Element verhindert, dass der Editor die Aktion anbietet — es hält niemanden auf, der es darauf anlegt. Berechtigungen müssen zusätzlich serverseitig erzwungen werden: Die Provider-Methoden laufen im Browser der Nutzenden — auch dafür, wer einen Block in einer im Team geteilten Bibliothek lesen, ändern oder löschen darf.
Fehlerbehandlung
Jede Methode kann ablehnen. Der Editor meldet den Fehler über den onError-Callback des Editors und lässt seine Liste im Speicher unverändert — ein fehlgeschlagenes Löschen lässt einen Block also nicht aus der Oberfläche verschwinden. Der Speicherdialog zeigt die Fehlermeldung zusätzlich direkt an.
Im Editor
Speichern — beim Auswählen eines Blocks der obersten Ebene erscheint in seiner Aktionsleiste eine Lesezeichen-Aktion. Ein Klick darauf startet eine Auswahl-Sitzung, in der dieser Block bereits ausgewählt ist.

Während einer Sitzung kommen weitere Blöcke per einfachem Klick hinzu oder fallen wieder heraus (ohne Zusatztasten), und eine Leiste über der Arbeitsfläche zeigt die Anzahl mit „Speichern" und „Abbrechen". Ein Klick innerhalb einer Section wählt die gesamte Section aus — Section-Kinder sind nicht einzeln speicherbar, da eine Section ihre Spalten samt Inhalt mitnimmt. Esc bricht ab, Enter bestätigt.

Beim Bestätigen öffnet sich ein Dialog, der den Namen und eine optionale Kategorie erfragt und dabei die bereits verwendeten vorschlägt. Die Vorschau listet die Blöcke in der Reihenfolge auf, in der Sie sie ausgewählt haben. Jede Zeile hat einen Anfasser, mit dem Sie sie ziehen können — oder Pfeil nach oben / Pfeil nach unten, während der Anfasser fokussiert ist —, sodass Sie vor dem Speichern umsortieren können. In der Reihenfolge, in der die Liste am Ende steht, werden die Blöcke gespeichert.

Durchsuchen — sobald die Funktion konfiguriert ist, sitzt ein Eintrag in der linken Leiste, der einen durchsuchbaren Browser mit Live-Vorschau öffnet. Er ist vom ersten Rendern an vorhanden, unabhängig davon, ob etwas gespeichert ist — die Leiste verschiebt sich also nie, und eine leere Bibliothek öffnet einen Zustand, der erklärt, wie man sie füllt. Geladen wird erst, wenn dieser Browser (oder der Speicherdialog) sich öffnet:
list()wird beim Laden des Editors nie aufgerufen, und beim ersten Öffnen erscheinen Platzhalterzeilen, bis die Antwort da ist.
Kategorisieren — eine Kategorie ist freier Text, flach und optional; es gibt weder Ordner noch Verschachtelung. Sobald irgendetwas kategorisiert ist, zeigt der Browser einen Kategoriefilter mit genau den verwendeten Kategorien — eine Kategorie existiert so lange, wie ein Eintrag sie trägt. Suche und Kategoriefilter grenzen die Liste gemeinsam ein.
Einfügen — wählen Sie eine Position (am Anfang, nach einem bestehenden Block oder am Ende) und fügen Sie ein. Eingefügte Blöcke erhalten immer neue IDs, sodass zweimaliges Einfügen desselben Eintrags nie kollidiert.
Umbenennen / Umkategorisieren / Löschen — direkt in jeder Zeile des Browsers; die Bearbeitungszeile umfasst Name und Kategorie, und ein leeres Kategoriefeld hebt die Kategorisierung auf. Das Löschen wird zuvor bestätigt.
Standardmäßig deaktiviert
Lassen Sie savedBlocks weg, fehlt die Funktion: keine Speicheraktion, kein Eintrag in der Leiste und kein zugehöriger Code wird geladen. Die Oberfläche ist in verzögert geladene Chunks aufgeteilt, die erst beim Öffnen eines Dialogs abgerufen werden.
Headless-Nutzung
Die reaktive Zustandsebene wird aus @templatical/core exportiert, falls Sie eine eigene Oberfläche über einem Provider bauen möchten:
import { useSavedBlocks } from '@templatical/core';
const {
savedBlocks, // Ref<SavedBlock[]>
isLoading, // Ref<boolean>
categories, // ComputedRef<string[]> — verwendete Kategorien, sortiert
canCreate, // ComputedRef<boolean> — hat der Provider create geliefert?
canUpdate,
canDelete,
canUpdateBlock, // (block) => boolean — Fähigkeit UND das Flag des Eintrags
canDeleteBlock,
load, // (params?: { search?, category? }) => Promise<void>
create, // (name, content, category?) => Promise<SavedBlock>
update, // (id, patch) => Promise<SavedBlock>
remove, // (id) => Promise<void>
} = useSavedBlocks({
provider,
onError: (error) => {
/* behandeln */
},
});Die Liste bleibt nach jedem erfolgreichen Aufruf synchron — beim Erstellen vorangestellt, beim Aktualisieren ersetzt, beim Löschen entfernt — und Fehler werden nach der Meldung an onError erneut geworfen.
Prüfen Sie canCreate / canUpdateBlock / canDeleteBlock, bevor Sie eine Aktion in Ihrer eigenen Oberfläche anbieten. Der Aufruf einer Mutation, die der Provider vorenthält — oder die ein Eintrag verbietet —, wird abgelehnt statt still erfüllt, sodass niemand eine Ablehnung für ein Speichern halten kann.
Sie nutzen Templatical Cloud? Cloud implementiert diesen Vertrag ohne jede Konfiguration — siehe Gespeicherte Blöcke auf Cloud.