---
title: Gespeicherte Blöcke
description: Ermöglichen Sie Ihren Nutzern, wiederverwendbare Blockgruppen zu speichern und in andere Vorlagen einzufügen — mit Ihrem eigenen Speicher.
---

# 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.**

::: tip Nicht dasselbe wie benutzerdefinierte Blöcke
[Benutzerdefinierte Blöcke](/de/guide/custom-blocks) 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:

```js
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`**:

```ts
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.

Eine minimale REST-Implementierung:

```ts
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

```ts
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;
}

interface SavedBlockInput {
  name: string;
  content: Block[];
  category?: string;
}

type SavedBlockPatch = Partial<{
  name: string;
  content: Block[];
  category: string; // "" löscht die Kategorie; fehlender Key lässt sie
}>;
```

- **Die `id` kommt vom Provider.** Der Editor erzeugt nie eine eigene, sondern verwendet, was `create()` zurückgibt.
- **Umbenennen mit `update(id, { name })`, umkategorisieren mit `update(id, { category })`.** Für beides gibt es keine separate Methode — `update` nimmt 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, bevor `list()` 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, die `useSavedBlocks` direkt ansteuern (siehe [Headless-Nutzung](#headless-nutzung)). Welche Einträge jemand überhaupt sehen darf, wird in `list()` entschieden.
- **Zeitstempel dienen nur der Anzeige.** Jeder Eintrag zeigt eine relative Angabe wie „vor 5 Min." (aus `updatedAt`, ersatzweise `createdAt`), 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`.

```ts
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.

```json
[
  { "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:

```ts
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.

::: warning 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.

  ![Die Aktionsleiste eines ausgewählten Blocks mit der Lesezeichen-Aktion, die eine Auswahl-Sitzung startet](/images/saved-blocks-pick-start.png)

  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.

  ![Eine Auswahl-Sitzung auf der Arbeitsfläche: zwei ausgewählte Blöcke sind umrandet, darunter eine schwebende Leiste, die die Anzahl sowie „Abbrechen" und „Block speichern" anbietet](/images/saved-blocks-pick-selector.png)

  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.

  ![Der Dialog „Als Block speichern" — ein Namens- und ein Kategoriefeld über einer umsortierbaren Vorschau der beiden ausgewählten Blöcke, jede Zeile mit einem Anfasser](/images/saved-blocks-save.png)

- **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.

  ![Das Modal „Gespeicherte Blöcke durchsuchen" — links Suche und Kategoriefilter über der Liste der Einträge, rechts die Live-Vorschau und die Auswahl der Einfügeposition](/images/saved-blocks-browse.png)

- **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.

## Events

```ts
savedBlocks: {
  // ...list, create, update, delete
  onCreated: (block) => {},
  onUpdated: (block) => {},
  onDeleted: (block) => {},
}
```

Jedes löst aus, sobald der Editor die Änderung in seiner eigenen Liste übernommen hat: `onCreated` stellt nach einem Speichern den neuen Eintrag voran, `onUpdated` ersetzt ihn nach einer Änderung, `onDeleted` filtert ihn nach dem Entfernen heraus.

::: tip `onDeleted` trägt den Block, keine ID
`delete` löst zu nichts auf, daher übergibt der Editor den Eintrag, den er unmittelbar vor dem Entfernen erfasst hat.
:::

::: tip Ein Löschen außerhalb der geladenen Liste löst kein Event aus
Der erfasste Eintrag muss bereits in der Liste des Editors vorhanden sein. Das Löschen einer ID, die der Editor nie geladen hat — etwa durch einen Headless-Aufrufer, der mit einer ID von anderswo arbeitet —, löscht weiterhin erfolgreich; es gibt dann aber nichts, das an `onDeleted` übergeben werden könnte, weshalb es nicht auslöst.
:::

Eine Handler-Funktion, die einen Fehler wirft, wird abgefangen und an `onError` gemeldet — sie lässt das auslösende Erstellen, Aktualisieren oder Löschen nie fehlschlagen.

## Headless-Nutzung

Die reaktive Zustandsebene wird aus `@templatical/core` exportiert, falls Sie eine eigene Oberfläche über einem Provider bauen möchten:

```ts
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](/de/cloud/saved-blocks).
