Skip to content

feat(surface): Kompositions-Renderer — aus einem Layout werden Inseln - #172

Merged
BechsteinDigital merged 1 commit into
mainfrom
feat/surface-composition-renderer
Aug 6, 2026
Merged

feat(surface): Kompositions-Renderer — aus einem Layout werden Inseln#172
BechsteinDigital merged 1 commit into
mainfrom
feat/surface-composition-renderer

Conversation

@BechsteinDigital

Copy link
Copy Markdown
Owner

1. Why is this change necessary?

Baustein 6: der Vertrag, aus dem der Composer sein Layout liefert, und die Schicht, die daraus
Markup macht. Damit steht alles unter dem Editor — der Block-Vertrag aus #161, die Kontext-Kanäle
aus #165/#167, und jetzt der Weg vom gespeicherten Layout zur gerenderten Seite.

2. What does this change do, exactly?

Ein Layout ist ein unveränderliches JSON-Dokument je Version, keine normalisierten Tabellen
je Sektion und Block. Rückrollen ist Zeilen-Kopieren, ein Diff ist trivial, und der Renderer liest
ein Dokument statt drei Joins. Shopware normalisiert und versioniert auf jeder Ebene
(cmsPageVersionId, cmsSectionVersionId, cmsBlockVersionId); diese Maschinerie kauft eine
Abfrage — „welche Layouts benutzen Block X" —, die ein schmaler abgeleiteter Index genauso
beantwortet.

Was nicht herauskommt, ist der Sicherheitsteil. data-callora-props steht im ausgelieferten
HTML; wer die Seite abruft, liest es — bei einer Public-Surface ohne Anmeldung. Gefiltert wird
deshalb hier, bevor Markup existiert, aus demselben Grund, aus dem SurfaceSlotResolver Views
serverseitig filtert statt per CSS:

  • Als vertraulich deklarierte Controls erscheinen nie im Attribut.
  • Eine context-Bindung reist als Bindung, nie als aufgelöster Wert. Aufgelöst stünde der
    aktuelle Wert im Quelltext — für jeden Besucher, gleich wer ihn sehen darf. Der Browser löst sie
    gegen den Kanal auf, wo die Projektion aus feat(surface): geteilter Kontext über Surface-Grenzen, projiziert je Empfänger #167 schon entschieden hat.
  • Jedes Attribut wird kodiert, auch was aus einem konfigurierten Layout kommt: Ein Operator ist
    vertrauenswürdig, eine gespeicherte Zeichenkette nicht.

Zwei Methoden, nicht eine mit Schalter. GetPublishedAsync ruft der öffentliche Pfad,
GetDraftAsync nur der Editor. Es gibt kein ?preview=true und keinen Header — bei einer
Public-Surface säße so eine Lücke hinter gar keiner Authentifizierung, und ein Schalter machte
die Garantie davon abhängig, dass jemand daran denkt, false zu übergeben.

Die Reihenfolge ist eine Entscheidung: ein komponiertes Layout gewinnt vor dem SSR-Entry eines
Plugins, das vor der eingebauten Schale. Jemand hat das Layout für diese Fläche veröffentlicht;
ein Template, das es still überschriebe, machte den Editor unzuverlässig.

Die Runtime ändert sich nicht: Das Insel-Format ist das, was mount.ts schon versteht.

3. Describe each step to reproduce the issue or behaviour.

Ohne Composer-Plugin ändert sich nichts — ISurfaceLayoutSource ist dann nicht registriert, und
die Surface rendert aus .njk wie bisher.

Mit einer Quelle, die ein Dokument liefert:

sections[] → <div class="cal-section" data-cal-layout="two-2-1" data-cal-spacing="lg">
  blocks[]  → <div class="callora-island" data-callora-island="communication.call-list"
                   data-callora-props='{"title":"Aktive Anrufe"}'></div>

10 Renderer-Tests, davon vier zur Filterung: vertrauliches Control, Kontext-Bindung bleibt
Bindung, kein Attribut wenn nichts übrig bleibt, gespeicherte Zeichenkette bricht nicht aus.

4. Please link to the relevant issues (if any).

Setzt §8 Baustein 6 sowie die Regeln 4 und 11 aus §10 um.

Was die Tests gefunden haben

Der erste Preview-Test prüfte nichts: Er suchte nach "preview" und preview:, was ein
[FromQuery] bool preview nicht trifft. Die gehärtete Fassung sucht das Wort — und hätte dann
ihre eigene Begründung im Kommentar gefunden, weshalb Kommentarzeilen jetzt ausgenommen sind.
Beide Gegenproben beißen.

Und [FromServices] auf einem nicht registrierten Interface lässt die Bindung scheitern und
antwortet 400, wo „kein Composer installiert, mach weiter" gemeint ist. Das kostete 19 Tests, bis
der Dienst aus dem Container statt aus der Signatur kam.

Zahlen

17 Dateien. .NET 1919 + 29 Analyzer, Surface-Runtime 99 — vier Läufe zur Bestätigung.

Was noch fehlt bis zum Composer

Baustein 7: das Plugin selbst — eigenes Schema über IPluginDbContextFactory<T>, Canvas mit
Drag & Drop, Entwurf/Veröffentlichen, und die Umstellung auf ein Vue-Global statt zwei
(§7.6), damit ein Block-Bundle in beiden Runtimes läuft.

Baustein 6: der Vertrag, aus dem der Composer später sein Layout liefert, und die Schicht,
die daraus Markup macht.

Ein Layout ist EIN unveränderliches JSON-Dokument je Version, keine normalisierten Tabellen
je Sektion und Block. Rückrollen ist Zeilen-Kopieren, ein Diff ist trivial, und der Renderer
liest ein Dokument statt drei Joins. Shopware normalisiert UND versioniert auf jeder Ebene
(cmsPageVersionId, cmsSectionVersionId, cmsBlockVersionId); diese Maschinerie kauft eine
Abfrage — „welche Layouts benutzen Block X" —, die ein schmaler abgeleiteter Index genauso
beantwortet.

WAS NICHT HERAUSKOMMT, IST DER SICHERHEITSTEIL
data-callora-props steht im ausgelieferten HTML. Wer die Seite abruft, liest es — bei einer
Public-Surface ohne Anmeldung. Gefiltert wird deshalb hier, bevor Markup existiert, aus
demselben Grund, aus dem SurfaceSlotResolver Views serverseitig filtert statt per CSS:

- Als vertraulich deklarierte Controls erscheinen nie im Attribut.
- Eine context-Bindung reist als BINDUNG, nie als aufgelöster Wert. Aufgelöst stünde der
  aktuelle Wert im Quelltext — für jeden Besucher, gleich wer ihn sehen darf. Der Browser
  löst sie gegen den Kanal auf, wo die Projektion schon entschieden hat.
- Jedes Attribut wird kodiert, auch was aus einem konfigurierten Layout kommt: Ein Operator
  ist vertrauenswürdig, eine gespeicherte Zeichenkette nicht.

ZWEI METHODEN, NICHT EINE MIT SCHALTER
GetPublishedAsync ruft der öffentliche Pfad, GetDraftAsync nur der Editor. Es gibt kein
?preview=true und keinen Header — bei einer Public-Surface säße so eine Lücke hinter gar
keiner Authentifizierung. Ein Schalter machte die Garantie davon abhängig, dass jemand daran
denkt, false zu übergeben.

Zwei Tests nageln das als Quelltext-Regel fest, nicht über einen Aufruf: Ein Verhaltenstest
belegt, dass EIN Pfad den Entwurf nicht holt; die Regel muss für jeden gelten, den jemand
morgen hinzufügt. Beide Gegenproben beißen — die erste Fassung des Preview-Tests prüfte
nichts, weil sie nach einem zu engen Muster suchte und ihre eigene Begründung im Kommentar
gefunden hätte.

DIE REIHENFOLGE IST EINE ENTSCHEIDUNG
Ein komponiertes Layout gewinnt vor dem SSR-Entry eines Plugins, das vor der eingebauten
Schale. Jemand hat das Layout für diese Fläche veröffentlicht; ein Template, das es still
überschriebe, machte den Editor unzuverlässig.

Die Runtime ändert sich nicht: Das Insel-Format ist das, was mount.ts schon versteht, und
ein Block hat genau eine Darstellung — die Vue-Komponente. Ein zweiter, serverseitiger
Renderpfad je Block wurde erwogen und verworfen: zwei Implementierungen derselben
Darstellung driften, und im Canvas bräuchte jede Konfig-Änderung einen Server-Roundtrip.

Unterwegs gelernt: [FromServices] auf einem nicht registrierten INTERFACE lässt die Bindung
scheitern und antwortet 400, wo „kein Composer installiert, mach weiter" gemeint ist. Der
Dienst kommt jetzt aus dem Container statt aus der Signatur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant