Search by

tietge / silverstripe-markup

moritz-sauer-13

Anmerkungen direkt auf der Website für SilverStripe: eingeloggte Kunden markieren einen Punkt oder Bereich, beschreiben, was anders sein soll, und hängen Bilder an — die Agentur arbeitet die Anmerkungen im CMS ab.

Package info

git.innomedia.de/Tietge/silverstripe-markup

Type:silverstripe-vendormodule

pkg:composer/tietge/silverstripe-markup

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

v0.1.2 2026-09-24 12:51 UTC

This package is auto-updated.

Last update: 2026-09-24 13:27:49 UTC


README

Anmerkungen direkt auf der Website. Ein eingeloggter Kunde setzt auf seiner Seite einen Punkt (am Rechner auch einen Bereich), schreibt dazu, was anders sein soll, und hängt bei Bedarf Bilder an. Das Modul verankert die Anmerkung an Seite und Elemental-Block, legt einen Screenshot der Kundenansicht mit eingezeichnetem Punkt geschützt ab und gibt der Agentur im CMS ein Kanban-Board, einen Reiter an Seite und Block, einen Zähler im Seitenbaum und den Sprung in die CMS-Vorschau, in der dieselbe Anmerkung offen ist. Vorbild für die Bedienung ist markup.io: Der Kunde navigiert die Seite ganz normal, die Punkte liegen als Overlay darüber.

Kein Gastzugang, kein Inline-Editing, keine externen Dienste: Wer anmerken darf, ist im CMS angemeldet, alles Weitere läuft im eigenen Projekt.

Installation

composer require tietge/silverstripe-markup
vendor/bin/sake db:build --flush

Nicht auf Packagist — im Projekt als VCS-Repository eintragen:

"repositories": [
    {"type": "vcs", "url": "git@git.innomedia.de:Tietge/silverstripe-markup.git"}
]

Abhängigkeiten: silverstripe/framework ^6, silverstripe/cms ^6, silverstripe/admin ^3, silverstripe/assets ^3. dnadesign/silverstripe-elemental ist optional: Ohne Elemental hängen Anmerkungen nur an der Seite (Selektor-Pfad), und es gibt keinen Reiter am Block.

Danach die beiden Tasks in den Cron eintragen (siehe Mails und Cron). Ohne Cron funktioniert alles außer der Sammelmail an die Agentur und dem Aufräumen alter Bilder.

Rechte und Gruppen

RechtFürDarf
MARKUP_COMMENT („Anmerkungen: erstellen“)KundeAnmerkungen sehen, setzen und beantworten; eigene bearbeiten (solange offen), löschen (offen und ohne Antwort) und selbst abhaken
MARKUP_MANAGE („Anmerkungen: verwalten“)AgenturAlles sehen, Status setzen, antworten, löschen; Board, Reiter und Baum-Zähler im CMS, Reiter „Anmerkungen“ unter Einstellungen

ADMIN schließt beide Rechte ein. CMS_ACCESS_LeftAndMain („alle CMS-Bereiche“) allein reicht für das Board nicht.

db:build legt einmalig die Gruppe „Website-Anmerkungen“ (Code markup-feedback) mit MARKUP_COMMENT an. Kunden kommen in diese Gruppe. Umbenennen oder anpassen ist danach erlaubt: Das Modul erkennt die Gruppe am Code und fasst sie nicht mehr an.

Ablauf für einen neuen Kunden: Konto im CMS anlegen (Sicherheit › Benutzer) → der Gruppe „Website-Anmerkungen“ zuordnen → den Einladungslink verschicken (siehe „Einladungslink“ unter „Für die Agentur“). Ohne Konto in dieser Gruppe meldet sich der Link zwar an, aber MARKUP_COMMENT fehlt, und das Overlay bleibt aus.

Alle Rechteprüfungen laufen über canView(), canEdit(), canDelete(), canSetStatus() und canReply() an Tietge\Markup\Model\MarkupComment. Standard ist ein Mandant: Jeder Kunde sieht alle Anmerkungen. Wer das eingrenzen will, hängt eine Extension mit canView() an.

Konfiguration

Empfänger der Sammelmail. Ausschließlich im CMS unter „Einstellungen“ im Reiter „Anmerkungen“ pflegbar (Feld „E-Mail-Adresse für die Sammelmail“, SiteConfig.MarkupAgencyEmail). Der Reiter ist nur für Member mit MARKUP_MANAGE sichtbar. Tietge\Markup\Config::agencyEmail() liefert den getrimmten Wert, Config::agencyEmailSource() woher er kommt (siteconfig/none). Ohne Eintrag verschickt der Digest-Task nichts.

Alle übrigen Werte mit ihren Standards:

Tietge\Markup\Config:
  enabled: true                  # aus: kein Overlay, /markup/api/… antwortet 404
  max_body_length: 4000          # Zeichen je Anmerkung bzw. Antwort
  max_attachments: 3             # Bildanhänge je Anmerkung
  max_upload_bytes: 8388608      # je Datei (Screenshot oder Anhang), 8 MB
  max_pixels: 40000000           # Breite × Höhe, geprüft vor dem Dekodieren
  attachment_max_edge: 2000      # längste Kante eines Anhangs nach dem Neukodieren
  screenshot_max_edge: 2000      # längste Kante eines Screenshots
  screenshot_timeout_ms: 8000    # Zeitlimit der Aufnahme im Browser
  rate_limit_per_hour: 60        # schreibende Aufrufe je Member und Stunde, 0 = kein Limit
  media_retention_days: 90       # Bilder so viele Tage nach „Erledigt“ löschen, 0 = nie
  resolved_visible_days: 30      # Erledigte im Board so lange zeigen, 0 = alle
  target_classes:                # zulässige Klassen für die Block-Zuordnung (mit Unterklassen)
    - DNADesign\Elemental\Models\BaseElement

Dazu Stellschrauben an einzelnen Klassen, ebenfalls mit Standards:

Tietge\Markup\Service\UrlNormalizer:
  strip_parameters: [CMSPreview, stage, ElementalPreview, markup, fbclid, gclid]
  strip_parameter_prefixes: [utm_]

Tietge\Markup\Service\MediaStore:
  folder: 'markup'               # Ordner im Asset-Store
  quality: 85                    # JPEG/WebP beim Neukodieren

Tietge\Markup\Admin\ThumbnailRenderer:
  width: 640                     # Kartenbild im Board
  height: 400
  min_crop_width: 800            # Mindestbreite des Ausschnitts im Screenshot
  attachment_edge: 320           # längste Kante der Anhang-Kacheln im Panel
  quality: 82

Nur in einer Umgebung abschalten, etwa live:

---
Only:
  environment: live
---
Tietge\Markup\Config:
  enabled: false

Für den Kunden

Einstieg. Wer angemeldet ist und MARKUP_COMMENT oder MARKUP_MANAGE hat, sieht unten rechts den schwarzen Knopf „Anmerkung hinzufügen“ mit dem Zähler „n offen“. Vorhandene Punkte liegen halbtransparent über der Seite. Anonyme Besucher bekommen weder Knopf noch Skript. Das Overlay steht nie im (gecachten) HTML, sondern holt alles per GET /markup/api/session.

Punkt oder Bereich. Nach dem Klick auf den Knopf rahmt ein blauer Rand die Seite. Oben steht eine Leiste mit einer Zeile Anleitung, „n Punkte anzeigen“ bzw. „ausblenden“ und „Fertig“. Beim Bewegen der Maus leuchtet das Element unter dem Zeiger auf und wird benannt („Hero · Überschrift“). Ein Klick setzt einen Punkt, am Rechner zieht Klicken und Ziehen einen Bereich auf. Danach öffnet sich ein Kasten mit einer Frage — „Was soll hier anders sein?“ —, einem Textfeld, „Bild anhängen“ und dem Hinweis „Screenshot wird mitgeschickt.“

Handy. Unter 768 px Fensterbreite oder bei Touch-Bedienung: kürzere Leiste, Bottom-Sheet statt Kasten, Touch-Ziele ab 44 px, getrennte Knöpfe „Foto“ (Kamera) und „Bild“ (Galerie). Nur Punkte, keine Bereiche.

Screenshot. Die Aufnahme startet im Moment des Tippens bzw. Klickens, im Hintergrund und bevor Kasten oder Bottom-Sheet aufgehen: Scrollposition, Viewport und Lage des Punkts werden dabei eingefroren. So zeigt das Bild genau das, was beim Setzen zu sehen war, auch wenn danach Bottom-Sheet und Bildschirmtastatur den Viewport verkleinern und die Seite zum Textfeld scrollt. Am Handy bekommt das Textfeld den Fokus erst, wenn die Seite geklont ist (typisch unter einer Sekunde). Das Bild bleibt bis zum Abschicken im Speicher; nach dem Anlegen zeichnet der Browser den Punkt mit seiner Nummer hinein und reicht das Bild nach. „Abbrechen“ verwirft es. Der Kunde wartet nie darauf. Scheitert die Aufnahme, erscheint nur ein kleiner Hinweis.

Einladungslink. Öffnet ein berechtigter Kunde eine Seite mit ?markup=welcome, entfernt das Overlay den Parameter aus der Adresse, zeigt einmal „Du bist angemeldet. Tippe auf ‚Anmerkung hinzufügen‘, um loszulegen.“ und lässt den Knopf kurz pulsieren (bei prefers-reduced-motion nur ein Rahmen). Der Setzen-Modus startet nicht von selbst. Ohne Berechtigung passiert nichts.

Status und Verlauf. Ein Klick auf einen Punkt öffnet den Verlauf: Text, Anhänge, Antworten, Status (blau Offen, orange In Arbeit, violett Rückfrage, grün Erledigt) und Pfeile durch alle Punkte der Seite. Eigene offene Anmerkungen lassen sich bearbeiten, unbeantwortete löschen und jede eigene mit „Für mich erledigt“ abhaken. Escape schließt, der Fokus bleibt im Kasten.

Gefunden, nicht im Bild, verwaist. Ein Punkt gilt als gefunden, wenn sein Element im DOM steht und eine Box hat, auch wenn es gerade nicht zu sehen ist. Sichtbar ist er, wenn die Box innerhalb aller beschneidenden Vorfahren (overflowvisible) liegt und weder display noch visibility sie verstecken; Punkte weiter unten auf der Seite zählen als sichtbar. Punkte in einem anderen Slide, hinter overflow: hidden, in einem eingeklappten <details> oder in einem Element, das es nur in der anderen Ansicht gibt, stehen in der Leiste am rechten Rand unter „Nicht im Bild“ mit dem Knopf „Anzeigen“, bei Punkten aus der anderen Ansicht mit dem Hinweis „Gesetzt am Handy, 390 px breit“. „Anzeigen“ schaltet einen Swiper-Slider auf das richtige Slide (slideTo), klappt <details> auf und scrollt das Element in die Mitte; danach erscheint der Punkt. Die Sichtbarkeit wird beim Scrollen, bei Größenänderungen und bei Attribut- oder Klassenänderungen an den Vorfahren (Slider wechselt, Menü klappt) neu bewertet. Eigene Slider-Bibliotheken meldet ein Projekt so an:

window.tietgeMarkup?.registerRevealer((el) => {
  const slide = el.closest('.my-slide');
  if (!slide) return false;
  mySlider.goTo([...slide.parentElement.children].indexOf(slide));
  return true; // darf auch ein Promise liefern (Animation abwarten)
});

window.tietgeMarkup steht, sobald overlay.js läuft; die fertige App meldet sich zusätzlich mit dem Event tietge-markup:ready am window.

Wenn sich die Seite ändert. Hat sich der Text an der Stelle geändert, trägt der Punkt ein Warnzeichen („Inhalt hat sich geändert“). Passt der gespeicherte Selektor-Pfad nicht mehr oder zeigt er auf anderen Text (etwa am Handy gesetzt, am Desktop anders verschachtelt), sucht das Overlay den gespeicherten Text im Block und setzt den Punkt dorthin. Ist die genaue Stelle weg oder hat sie keine Box mehr, der Block aber schon, sitzt der Punkt ungefähr im Block und ist gestrichelt umrandet („Position ungefähr“). Hat auch der Block keine Box, steht er unter „Nicht im Bild“. Erst wenn nichts davon mehr im DOM steht, landet die Anmerkung unter „Nicht mehr gefunden“. Sie geht nie stumm verloren.

Für die Agentur

Einladungslink. Ein fertiger Link, den die Agentur dem Kunden schickt: Er meldet sich damit an und landet direkt auf der Website mit dem Anmerkungs-Tool -- nicht im CMS. Technisch ein SilverStripe-Login mit BackURL auf die Zielseite (?markup=welcome); ein Overlay-Skript erkennt den Parameter nach der Anmeldung und zeigt kurz einen Hinweis samt Hervorhebung des Einstiegsknopfs. Tietge\Markup\Service\CommentLinks::inviteLink(?SiteTree $page = null) baut den Link, mit $page auf diese Seite, ohne auf die Startseite.

Zu finden:

  • Einstellungen › Reiter „Anmerkungen“ (nur mit MARKUP_MANAGE): der Link auf die Startseite, mit „Kopieren“-Knopf.
  • Reiter „Anmerkungen (n)“ an jeder Seite: derselbe Link, aber auf diese Seite. Fehlt bei Seiten ohne öffentliche Adresse (z. B. eine ErrorPage).
  • Board (admin/markup): Knopf „Einladungslink kopieren“ in der Kopfzeile, kopiert den Link auf die Startseite.

Voraussetzung: ein Benutzerkonto in der Gruppe „Website-Anmerkungen“ (siehe „Rechte und Gruppen“). Ohne das Konto oder ohne die Gruppe meldet sich der Kunde zwar an, sieht aber weder Knopf noch Overlay.

Reiter „Anmerkungen (n)“ an jeder Seite und, mit Elemental, an jedem Block. Nur mit MARKUP_MANAGE sichtbar, n zählt die nicht erledigten. Die Liste ist nur lesend: Nr., Status, Autor, Auszug, Block (nur an der Seite), Datum, Antworten, dazu „In Vorschau zeigen“ und „Auf der Website“.

Zähler im Seitenbaum. Seiten mit offenen Anmerkungen tragen ein Badge mit der Anzahl (Titel „n offene Anmerkungen“). Es kommt über den offiziellen Hook updateStatusFlags, wie das „Geändert“-Flag. Mit Elemental trägt derselbe Badge auch den betroffenen Block in der Blockliste beim Bearbeiten der Seite.

Board unter dem Menüpunkt „Anmerkungen“ (admin/markup):

  • Vier Spalten (Offen, In Arbeit, Rückfrage, Erledigt) mit Farbpunkt und Zähler; jede Spalte scrollt für sich, leere Spalten zeigen nur einen Satz. Unter 1100 px Breite liegen die Spalten in einer waagerechten Leiste.
  • Karten: Screenshot-Ausschnitt (16:10) um den Punkt mit einem Ring in Statusfarbe und der Nummer darüber, Anhang-Zähler („2 Bilder“), erste Zeile als Titel, „Seite › Block“, Initialen des Autors, Alter und Antworten. Die Position des Rings liefert das Board-JSON (thumbnailMarker, Pixel des Ausschnitts); im Screenshot selbst ist der Marker ohnehin eingezeichnet, aber klein.
  • Status wechseln per Drag&Drop oder über das Karten-Menü (Drei-Punkte-Knopf: Status, Details, „Im CMS öffnen“, „Auf der Website öffnen“; mit Pfeiltasten bedienbar). Die Änderung erscheint sofort und wird bei einem Fehler zurückgenommen.
  • Filter: Volltext, Seite, Autor und „Erledigte: letzte n Tage / alle“.
  • Ein Klick auf die Karte öffnet das Seitenpanel (420–480 px, unter 1100 px Vollbild): Status als Segmentschalter, „Im CMS öffnen“ und „Auf der Website“, Screenshot-Ausschnitt mit Ring, Anhänge als Kacheln, Details (Seite, Block, Autor, Zeit, Gerät, Ansicht, Browser), Verlauf als Chat (Kunde links, Agentur rechts) und unten das Antwortfeld (Strg+Enter sendet).
  • Screenshot und Kacheln öffnen eine Bildansicht mit dem Original aus markup/api/media/{id}, beim Screenshot mit Ring; Pfeiltasten blättern, Escape schließt. Die Kacheln kommen verkleinert (längste Kante 320 px, JPEG) aus admin/markup/attachment/{id}/{imageId}, erzeugt wie die Kartenbilder im Temp-Ordner, nie im Asset-Store.

Sprung in die Vorschau. „Im CMS öffnen“ führt bei Anmerkungen an einem Block auf dessen eigenständige Detailmaske (getCMSEditLink(true)), sonst auf die Seite, jeweils mit ?markup={ID}. Das Admin-Skript schaltet dort die Vorschau in den geteilten Modus und schickt die ID per postMessage in das Vorschau-iframe. In der Vorschau (CMSPreview=1) zeigt das Overlay keinen Einstiegsknopf, sondern gleich alle Punkte mit den Aktionen der Agentur, und öffnet die gewünschte Anmerkung. Links steht das echte Block-Formular, rechts die Seite mit dem Verlauf.

Deep-Links. {Seiten-URL}?markup={ID} öffnet auf der Website die Anmerkung, scrollt hin und zeigt den Verlauf. So arbeiten „Auf der Website“ im Board und im Reiter sowie die Links in den Mails.

API

Das Overlay spricht mit /markup/api/…, das Board mit admin/markup/…. Beide antworten ausschließlich mit JSON und Cache-Control: no-store.

RouteZweck
GET markup/api/bootstrap?url=session und comments in einer Antwort (Feld comments); damit startet das Overlay
GET markup/api/session?url=Member, Rechte, Sprache, CSRF-Token, Grenzwerte, Seite und Block-Anker zur URL
GET markup/api/comments?url=Anmerkungen zur (normalisierten) URL mit Antworten
POST markup/api/commentsAnlegen: JSON oder Multipart (data als JSON, attachments[])
POST markup/api/comments/{id}Eigenen Text bearbeiten, {body}
POST markup/api/comments/{id}/screenshotScreenshot nachreichen (screenshot, optional data.marker) oder Scheitern melden (data.error); nur der Autor, einmal, bis 5 Minuten nach Anlage
POST markup/api/comments/{id}/statusStatus setzen, {status}
POST markup/api/comments/{id}/deleteLöschen
POST markup/api/comments/{id}/repliesAntworten, {body}
GET markup/api/media/{id}Screenshot oder Anhang, nur wenn die Anmerkung sichtbar ist
GET admin/markup/boardKarten des Boards, Filter page, author, q, all=1
POST admin/markup/status/{id}Status aus dem Board, {status}
POST admin/markup/reply/{id}Antwort der Agentur, {body}
GET admin/markup/thumbnail/{id}JPEG 640×400 um den Punkt, sonst 404
GET admin/markup/attachment/{commentId}/{imageId}Anhang verkleinert (JPEG, längste Kante 320 px), nur Anhänge dieser Anmerkung, sonst 404

Auth. Session des angemeldeten Members, keine API-Keys. Ohne Anmeldung gibt es 401, ohne MARKUP_COMMENT/MARKUP_MANAGE 403; die Board-Routen verlangen MARKUP_MANAGE. Anmerkungen, die der Member nicht sehen darf, auch weil er die zugehörige Seite nicht sehen darf, gibt es für ihn nicht: 404 statt 403.

CSRF. Jeder POST braucht den Header X-Securityid mit dem Token aus GET session (securityToken.header und securityToken.value), sonst 403 invalid_token. Ein gesetzter Origin-Header muss zur eigenen Website passen, sonst 403 invalid_origin. CORS gibt es nicht.

Fehlerformat. Immer {"error": {"code": "…", "message": "…"}} mit passendem Status: 400 invalid_json, 404 not_found, 405 method_not_allowed (mit Allow), 409 screenshot_exists bzw. screenshot_window_closed, 422 für Eingabefehler (invalid_body, invalid_url, invalid_position, too_many_files, invalid_type …) und 429 rate_limited (mit Retry-After). Unerwartete Ausnahmen werden geloggt und als 500 internal_error ohne Details beantwortet.

Mails und Cron

Kundenmails gehen sofort beim Speichern an den Autor der Anmerkung: bei einer Antwort der Agentur („Neue Antwort auf deine Anmerkung #n“) und wenn die Agentur sie auf Erledigt setzt („Deine Anmerkung #n wurde erledigt“). Eigene Antworten und selbst abgehakte Anmerkungen lösen keine Mail aus. Absender ist Email.admin_email des Projekts. Scheitert der Versand, wird das nur geloggt; das Speichern schlägt dadurch nie fehl.

Sammelmail und Aufräumen sind Tasks, nur per CLI aufrufbar, nicht im Browser:

  • sake tasks:markup-digest schickt alle Anmerkungen und Kunden-Antworten, die der Agentur noch nicht gemeldet wurden, als eine Mail an den Empfänger aus den Einstellungen (Reiter „Anmerkungen“), gruppiert nach Seite, mit Links ins CMS und auf die Website. Screenshots werden nur erwähnt, nicht eingebettet, denn sie sind geschützt. Ohne Empfänger in den Einstellungen oder ohne Neues verschickt der Task nichts; --dry-run nennt den konfigurierten Empfänger oder den Hinweis, ihn einzutragen.
  • sake tasks:markup-cleanup löscht Screenshot und Anhänge erledigter Anmerkungen, deren „Erledigt“ länger als media_retention_days zurückliegt. Text, Position und Verankerung bleiben.

Beide kennen --dry-run: nur anzeigen, nichts senden, löschen oder markieren.

# Sammelmail an die Agentur, werktags morgens
0 7 * * 1-5 www-data cd /pfad/zum/projekt && php8.4 vendor/bin/sake tasks:markup-digest >> /var/log/markup-digest.log 2>&1

# Bilder erledigter Anmerkungen aufräumen, nachts
30 2 * * * www-data cd /pfad/zum/projekt && php8.4 vendor/bin/sake tasks:markup-cleanup >> /var/log/markup-cleanup.log 2>&1

Die Tasks als Webserver-Benutzer laufen lassen: Der Cleanup löscht Dateien, die der Webserver angelegt hat. Läuft kein Cron, bleiben Anmerkungen ungemeldet und alte Bilder liegen. Die Kundenmails hängen nicht am Cron.

Aufbewahrung und Datenschutz

Was gespeichert wird: der Text, die Seiten-URL (ohne Tracking- und Vorschau-Parameter), die Verankerung (Block, Selektor-Pfad, Textausschnitt an der Stelle, Relativposition), Fenstergröße, Pixeldichte, Scrollposition, Browser (User-Agent), Autor und Zeitpunkte. Dazu der Screenshot des sichtbaren Ausschnitts und bis zu max_attachments Bildanhänge.

Geschützte Ablage. Bilder liegen im Asset-Store unter markup/JJJJ/MM/ und werden sofort nach dem Schreiben geschützt; der Ordner steht auf „Nur diese Benutzer“ ohne Gruppen. Der reguläre Weg zum Bild ist GET markup/api/media/{id}, das die Rechte der Anmerkung prüft. Das Modul nutzt Upload nicht, deshalb greifen Projekt-Extensions, die jeden Upload veröffentlichen, hier nicht. Die Kartenbilder des Boards entstehen nur im Temp-Ordner (TEMP_PATH/markup-thumbs/), nie in assets/.

Frist. Screenshots und Anhänge löscht der Cleanup-Task media_retention_days (90) Tage nach „Erledigt“ und setzt MediaPurgedAt. Wird eine Anmerkung gelöscht, gehen Antworten, Screenshot und Anhänge sofort mit.

Zu beachten: Screenshots zeigen die Ansicht eines angemeldeten Members, im Shop also unter Umständen Warenkorb oder Kontodaten. Das gehört in die Datenschutzhinweise gegenüber dem Kunden.

Sicherheit

  • Anmeldung und Recht sind Pflicht. Rechte nur über die can*()-Methoden, fremde oder nicht sichtbare Datensätze ergeben 404.
  • CSRF-Token-Header bei jedem POST, Origin-Prüfung, kein CORS.
  • Rate-Limit rate_limit_per_hour je Member und Stunde, ein Zähler für Anlegen, Bearbeiten, Status, Löschen und Antworten (im Board: Antworten). Das Nachreichen des Screenshots zählt nicht mit; es ist ohnehin auf einmal je Anmerkung und fünf Minuten begrenzt.
  • Text nur als Plain Text, überall escaped ausgegeben (Overlay, Board, Reiter, Mails).
  • Uploads: nur echte HTTP-Uploads (is_uploaded_file), MIME per finfo aus dem Inhalt (JPEG, PNG, WebP, GIF), Größen- und Pixel-Limit vor dem Dekodieren, serverseitig neu kodiert (EXIF und GPS fallen weg, GIF wird PNG), geschützt abgelegt.
  • URLs nur von der eigenen Origin, höchstens 2083 Zeichen.
  • Block-Zuordnung nur für Klassen aus target_classes, die der Member sehen darf.
  • Overlay im Shadow DOM, ohne Inline-Daten im HTML.

Entwicklung

Bundles. client/build.mjs (esbuild) baut nach client/dist/, das eingecheckt ist:

  • overlay.js — das Overlay, Vanilla TypeScript im Shadow DOM mit eigener kleiner CSS.
  • snapdom.js@zumer/snapdom allein. Wird erst beim Start des Setzen-Modus nachgeladen, aus demselben Verzeichnis wie overlay.js und mit derselben ?m=-Query.
  • admin.js und admin.css — Board und Vorschau-Anbindung. Ohne eigenes React: Das esbuild-Plugin adminGlobals biegt react, react-dom, lib/Config, lib/ReactRouteRegister usw. auf die Globals von silverstripe/admin 3 um. Imports ohne Mapping lassen den Build scheitern.
npm install
npm run build        # client/dist neu bauen
npm run dev          # dasselbe im Watch-Modus
npm run typecheck
npm test             # vitest mit jsdom: Verankerung, API-Client, i18n, Board-Logik …
npm run mock         # Overlay ohne SilverStripe: http://localhost:4455/ (?as=agency, &CMSPreview=1)

Die Texte von Overlay und Board liegen im Bundle (client/src/overlay/i18n.ts, client/src/admin/i18n.ts, Deutsch und Englisch), die PHP-Texte in lang/de.yml und lang/en.yml.

PHP-Tests laufen in einem Projekt, in dem das Modul installiert ist:

SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit vendor/tietge/silverstripe-markup/tests

phpcs.xml.dist (PSR-12) und phpstan.neon.dist (Level 5) liegen im Modul.

Performance. Der Start des Overlays ist auf frühe Sichtbarkeit ausgelegt:

  • overlay.js wird async eingebunden (zusätzlich defer als Rückfall) und über <link rel="preload" as="script" fetchpriority="high"> im Head früh geladen. Das Skript wartet also nicht mehr hinter dem Theme-JS bis kurz vor DOMContentLoaded.
  • Beim Ausführen startet sofort genau ein Request, GET bootstrap (Session und Anmerkungen, priority: 'high'). Der Knopf wird ohne Warten auf die Antwort gerendert, der Zähler folgt. Fehlt die Berechtigung (401/403), verschwindet er wieder.
  • Rechte, Token, Sprache und Grenzwerte (nicht die Anmerkungen, nicht die Anker) liegen 5 Minuten in sessionStorage. Auf Folgeseiten im selben Tab ist der Setzen-Modus damit schon bedienbar, bevor bootstrap antwortet; die Antwort erneuert den Eintrag. Schreibende Aufrufe warten, bis bootstrap den aktuellen Token geliefert hat.
  • snapdom.js wird nach dem Start im Leerlauf per <link rel="prefetch"> (Safari: fetch mit niedriger Priorität) in den Cache geholt und erst im Setzen-Modus ausgeführt.
  • Serverseitig lädt die Anmerkungsliste Ziele, Screenshots, Anhänge, Antworten und Autoren mit je einer Query (ghee, Startseite mit 12 Blöcken und 5 Anmerkungen: 23 statt 60 Queries für Session und Liste zusammen).

Gemessen auf dem Dev-Rechner (headless Chromium, ghee, Median aus 5 Läufen, Rechner unter Last): Der Knopf erscheint 60–250 ms vor DOMContentLoaded statt 290–380 ms danach, die fertige App (Anker, Marker, Zähler) steht 5–65 ms nach DOMContentLoaded statt 280–380 ms danach. Der Setzen-Modus ist nach dem Klick in 10–15 ms aktiv. Auf einer gedrosselten Leitung (150 ms, 1,6 Mbit/s) liegt snapdom.js beim ersten Klick nach 0,1 s statt 0,6 s bereit.

Hosting. Beides liefert das Modul nicht selbst, sondern der Webserver: Kompression (ghee: gzip über mod_deflate, overlay.js 25 KB statt 78 KB, snapdom.js 84 KB statt 244 KB; Brotli wäre kleiner, mod_brotli ist dort nicht aktiv) und lange Cache-Header für /resources/…?m=… (ghee: public, max-age=31536000, immutable über die Projekt-.htaccess). Ohne sie lädt jeder Seitenaufruf das Overlay erneut.

Pfad-Repository. Beim Entwickeln im Projekt als Composer-Pfad-Repository mit symlink: true legt vendor-expose die Ressourcen eines Moduls außerhalb des Projekts unter public/resources/<kurzname> ab statt unter public/resources/vendor/tietge/silverstripe-markup. Dann findet das Overlay snapdom.js nicht. Abhilfe nur für die Entwicklung, ein zusätzlicher Symlink: public/resources/vendor/tietge/silverstripe-markup/client/dist -> vendor/tietge/silverstripe-markup/client/dist. Bei Installation über VCS tritt das nicht auf.

Grenzen und bekannte Einschränkungen

  • Screenshot ist Best-Effort. Er entsteht im Browser (snapdom, SVG-foreignObject). Videos und iframes fehlen im Bild, externe SVG-Sprites setzt ein Plugin ein. Scheitert die Aufnahme oder dauert sie länger als screenshot_timeout_ms, bleibt die Anmerkung gültig und ScreenshotError ist gesetzt. Verlässlich sind die Metadaten. Das Bild zeigt den sichtbaren Ausschnitt beim Setzen des Punkts bei jeder Scrollposition, mit angeheftetem Header und fester Bottom-Navigation an ihrer sichtbaren Stelle. Während die Seite geklont wird (typisch unter einer Sekunde nach dem Tippen), hält das Overlay die Scrollposition fest. Nicht nachgebildet werden backdrop-filter (Unschärfe hinter dem Header), laufende Animationen (das Bild zeigt den Zwischenstand) und Scroll-Container innerhalb der Seite, die selbst fixiert sind. Bei sehr langen Seiten mit vielen Elementen dauert die Aufnahme länger, weil snapdom das ganze Dokument klont; Elemente außerhalb des sichtbaren Bereichs werden dabei nur als leere Platzhalter übernommen.
  • Bilder von fremden Domains ohne CORS-Freigabe kann der Browser nicht ins Bild übernehmen; sie bleiben im Screenshot leer.
  • Kein Setzen per Tastatur. Punkte und Bereiche setzt man mit Maus oder Finger. Lesen, Antworten und alle Aktionen im Verlauf gehen per Tastatur.
  • Verankerung am Element, nicht am Wort. Der Punkt hängt am tiefsten getroffenen Inhaltselement (Überschrift, Absatz, Bild, Button, Link, Listeneintrag …) und liegt relativ zu dessen Box. Beim Vergrößern oder Verkleinern des Fensters bleibt er auf dem Element; bricht ein Absatz anders um, kann er ein Wort daneben liegen. Der Selektor-Pfad bevorzugt stabile ids, data-*-Attribute und Klassen ohne responsive Präfixe, Zustände oder Utility-Werte; nth-of-type nur, wo nichts anderes eindeutig ist. Themes, die für Handy und Desktop getrennte Elemente rendern (etwa lg:hidden/max-lg:hidden), finden Punkte aus der anderen Ansicht über den gespeicherten Text wieder; ohne Text-Treffer sitzen sie ungefähr im Block. Elemente, die es nur in einer Ansicht gibt (mobile Tab-Bar, Off-Canvas-Menü), stehen in der anderen unter „Nicht im Bild“ mit dem Hinweis auf die Ansicht, „Anzeigen“ hilft dort nicht.
  • Slider und Akkordeons. „Anzeigen“ kennt Swiper (.swiper mit el.swiper, auch loop) und <details>; andere Bibliotheken nur über registerRevealer. Reine CSS-Animationen (Laufband) lösen keine Neubewertung aus, der Punkt folgt erst beim nächsten Scrollen.
  • Verwaiste Punkte. Wird der Block umgebaut oder gelöscht, landet die Anmerkung in der Leiste „Nicht mehr gefunden“. Im Board und im Reiter bleibt sie vollständig erhalten.
  • Ein Mandant. Kunden sehen alle Anmerkungen; eingrenzen nur per Extension.