Zum Inhalt springen

Pagebuilder-Anbindung (Dev)

Die Vorschau im tibi-admin ist nicht deine Website. Sie rendert vereinfachte Vorschau-Blöcke in einem Shadow-DOM. Die echte Website baut der BlockRenderer im Starter per SSR (siehe Website mit SSR).

Vorschau vs. Website: zwei Wege aus blocks[]
Eintrag (blocks[]) → Registry (Vorschau) → Shadow-DOM im Editor
Eintrag (blocks[]) → BlockRenderer.svelte → echtes HTML im Projekt

Die Registry ist eine JS-Datei mit einem Block-Namen pro Schlüssel. Jeder Block liefert render, dazu update und destroy im Handle sowie ein css-Array. Pfad steht in meta.pagebuilder.blockRegistry.file (Feld oder Collection).

Referenz-Registry (corporate-blocks/registry.js)
function defineBlock(renderContent) {
return {
render(container, row, context) {
const el = document.createElement("div")
renderContent(el, row, context)
container.appendChild(el)
return {
update(nextRow, nextContext) {
renderContent(el, nextRow, nextContext)
},
destroy() {
el.remove()
},
}
},
css: ["/assets/mock/corporate-blocks/blocks.css"],
}
}
export default registry
export { registry as blockRegistry }
Teil Vertrag (BlockMount.ts, load-registry.ts)
Export default oder blockRegistry als BlockRegistry-Map
render(container, row, context) Baut das DOM, gibt { update, destroy } zurück
update(row, context) Neue Daten bei gleichem Typ — kein DOM-Neuaufbau
destroy() Räumt auf (Listener, Element entfernen)
css Array mit Stylesheet-URLs, wird vor dem Mount vorgeladen
Fehlt der Typ render() gibt false zurück → Blueprint-Karte statt Vorschau

Laden ist lazy und pro URL gecacht (loadBlockRegistry, load-registry.ts:68-94: Map-Cache pro aufgelöstem Specifier; Import via dynamischem import(), vor esbuild versteckt). Schlägt der Import fehl oder fehlt der Export (default/blockRegistry), zeigt der Editor den Blueprint-Modus — kein Fehler, nur keine Vorschau.

Registry-Pfad-Auflösung in drei Schritten (resolveBlockRegistrySpecifier, load-registry.ts:18-35):

  1. Quelle: Feld-meta.pagebuilder.blockRegistry.file, Rückfall auf Collection-meta.pagebuilder… — jeweils String oder { eval }-Ausdruck (ausgewertet per resolveMetaEval mit Namespace und API-Basis).
  2. Ergebnis durch resolveProjectAssetSpecifier (Projekt-Basis) — siehe Tabelle unten.
  3. CSS-Präzedenz (resolveBlockCssTransform, load-registry.ts:50-61): Block-Definition → Feld-meta.pagebuilder.cssTransform → Collection → Default true (undefined heißt „nicht gesetzt”, nicht false).
Specifier Auflösung (project-asset-specifier.ts:17-50)
https://…, http://…, //…, data:, blob: Unverändert (extern/inline)
_/… Projekt-relativ zur Namespace-Basis (empfohlen)
/_/… Alias für projekt-relativ (rückwärtskompatibel)
/… Host-absolut, bleibt wie geschrieben
relativ (assets/x.js) Gegen die Projekt-Basis aufgelöst
{ eval: "…" } JS-Ausdruck vor dem Specifier ausgewertet (resolveMetaEval, load-registry.ts:26-31), Ergebnis läuft danach durch dieselbe Tabelle

Dein Block-CSS läuft durch transformPagebuilderCssLenientAsync (pagebuilder-css-transform.ts). Grund: Das CSS landet isoliert im Shadow-DOM, nicht im Seiten-<head>.

Umschreibung Warum
@media mit Breiten-Bedingung → @container Breakpoints reagieren auf die Vorschau-Breite, nicht aufs Editor-Fenster
html / body / :root → :host Diese Selektoren treffen im Shadow Root nichts
Relative url() → absolut (baseUrl) Inline-Styles lösen relativ sonst gegen das Admin-Dokument auf
@import wird eingebettet (fetchCss) Konstruierte Sheets werten @import im Browser nicht aus

Regeln dazu: Nur komplette Queries werden konvertiert (Teil-Umschreibung verboten). device-width und not bleiben @media. Unparsebares CSS lädt unverändert plus console.warn mit Zeile und Spalte.

Abschalten je Block, Feld oder Collection:

content.yml — Opt-out (load-registry.ts: Block → Feld → Collection → true)
meta:
pagebuilder:
cssTransform: false

Die Vorschau hydriert Fremdschlüssel selbst (hydratePagebuilderRow in lookup-hydration.ts). Das Muster kennst du aus Lesen und suchen: IDs plus _lookup-Objekt.

  1. IDs aus foreignKey/foreignMedia-Feldern sammeln (z. B. blocks.heroImage.image:medialib).
  2. Einträge per getEntriesByIds laden (gecacht pro namespace:collection:Feld:id).
  3. Ergebnis unter row._lookup.<feld> einbetten — der Registry-Block liest daraus.
  4. Datei-URLs per normalizeLookupUrl auflösen (relativ → Projekt-Basis).

Bildgrenze: MedialibImage ist die einzige Bild-Grenze im Starter (file.src oder Data-URI). Details: Dateien und Bilder, Medien.

Der Listen-image-Eintrag (meta.preview.image: _pagebuilderThumbnail) kommt aus gerenderten Screenshots. Kette:

  1. Regel lesen: meta.pagebuilder.screenshot in content.yml (field: blocks, fileField: _pagebuilderThumbnail).
  2. Registry laden und Block im PagebuilderScreenshotHost rendern (versteckt, pro Regel).
  3. Schriften und Bilder abwarten (Timeout je 4 s, Render-Timeout 12 s).
  4. Per modern-screenshot (domToCanvas) aufnehmen.
  5. Pfad bauen: buildVersionedPagebuilderScreenshotPath → <feld>.v-<hash>.<ext> (webp/jpg).
  6. Ins file-Feld schreiben (applyFileField).
content.yml — Screenshot-Regel (Starter)
meta:
pagebuilder:
screenshot:
- field: blocks
fileField: _pagebuilderThumbnail

Seed für Mock-Daten: scripts/seed-mock-pagebuilder-screenshots.js (Playwright, Thumbnails für die Mock-Registry).

  1. Typ in api/collections/content.yml anlegen: choices-Eintrag unter blocks.type plus Felder mit dependsOn-Eval auf $parent.type.
  2. Svelte-Komponente in frontend/src/blocks/ anlegen (z. B. QuoteBlock.svelte mit $props(), SSR-sicher mit Window-Guards).
  3. case in BlockRenderer.svelte ergänzen (block.type === "quote" → <QuoteBlock {block} />).
  4. Vorschau-Block in der Registry-Datei ergänzen (gleicher Schlüssel, defineBlock-Vertrag, eigenes CSS).
  5. Seite speichern — Vorschau und Screenshot laufen automatisch über die neue Registry.
content.yml — Feld nur für den eigenen Typ zeigen
- name: quote
type: string
meta:
dependsOn:
eval: $parent.type == 'quote'
BlockRenderer.svelte — Case ergänzen
{:else if block.type === "quote"}
<QuoteBlock {block} />

Design-Regeln aus dem Starter (api/config.yml, meta.agent.designGuide): Svelte-5-Runes ($props, nie block destrukturieren), Tailwind v4 kanonisch, i18n per $_, Bilder nur über MedialibImage.

Key Wirkung Quelle
viewForm / viewBoth / viewPreview Modus-Umschalter im Overlay: Nur Formular, Formular und Vorschau, Nur Vorschau PagebuilderOverlay.svelte:293,310,326, de.json:598-600
closeTitle Schließen-Knopf mit ESC-Hinweis („Schließen (ESC)“) PagebuilderOverlay.svelte:348, PagebuilderOverviewOverlay.svelte:725, de.json:596
viewportDesktop / viewportTablet / viewportPhone Breiten-Presets 1280 / 768 / 375 in der Viewport-Steuerung PagebuilderViewportControl.svelte:23-25, de.json:605-607
Drill-Down-Modi overlay / inline overlay (Standard): Fullscreen-Overlay mit Layer-Stack; inline: direkte Blockliste im Formular bei meta.drillDown: false PagebuilderOverviewOverlay.svelte:4-8,34,61,83, PagebuilderWidget.svelte:205,238
blueprintMoreFields „+{count} weitere” — eingeklappte Restfelder auf der Blueprint-Karte BlueprintCard.svelte:125, de.json:611
blueprintChildren Kind-Zähler („1 Kind” / „n Kinder”) BlueprintCard.svelte:101, de.json:610
blueprintNoData Leertext „Keine Daten” bei fehlenden Block-Daten BlueprintCard.svelte:132, de.json:612
ESC / Blur / Drilldown Escape schließt nur den obersten Layer (isTopLayer), vorher activeElement.blur(); Drilldown-Navigation übernimmt Blur selbst PagebuilderOverlay.svelte:190-198,244, PagebuilderOverviewOverlay.svelte:378-382

Der Block-Editor (Fullscreen-Split mit Formular und Live-Vorschau), die Viewport-Steuerung (Desktop/Tablet/Mobil-Presets plus Slider) sowie Übersichts-Overlay und Blueprint-Fallback (Schema-Karte bei fehlendem Registry-Typ) sind Admin-Oberfläche — Bedienung siehe Eintrags-Editor. Aus Entwickler-Sicht zählen die Ursachen dahinter:

Symptom Ursache Lösung
Blueprint-Karte statt Vorschau Registry-Pfad falsch oder Typ fehlt in der Map blockRegistry.file prüfen, Schlüssel = type-Wert
CSS „blutet” in den Editor cssTransform: false oder Parse-Fehler (Warnung in der Konsole) Warnung mit Zeile/Spalte lesen, CSS fixen
Bild leer in der Vorschau Lookup-ID ohne Ziel oder Ziel ohne active:true Medialib-Eintrag prüfen, anonyme Abfrage gegenlesen
Altes Thumbnail Screenshot-Regel fehlt oder fileField falsch meta.pagebuilder.screenshot in content.yml prüfen
Felder erscheinen nicht dependsOn-Eval auf $parent.type fehlt Eval wie oben setzen, Typ-Schreibweise exakt

Verifiziert am 2b091d8 2026-10-17 (tibi-admin-nova: de.json admin.pagebuilder.*), am e3f92eb 2026-10-17 (tibi-svelte-starter: content.yml, BlockRenderer, config.yml designGuide); Registry-Ladeordnung und Asset-Specifier verifiziert am 2b091d8 2026-10-17 (load-registry.ts:14-94, project-asset-specifier.ts:17-50)