- TypeScript 47.9%
- PHP 44.2%
- CSS 5.3%
- JavaScript 1.8%
- Scheme 0.5%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .idea | ||
| _config | ||
| client | ||
| docs | ||
| lang | ||
| src | ||
| templates/Tietge/Markup/Email | ||
| tests/php | ||
| .gitattributes | ||
| .gitignore | ||
| composer.json | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| phpcs.xml.dist | ||
| phpstan.neon.dist | ||
| phpunit.xml.dist | ||
| README.md | ||
| vitest.config.mjs | ||
tietge/silverstripe-markup
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
Das Paket liegt auf Packagist, ein eigener Repository-Eintrag ist nicht nötig. Quelle ist das Tietge-Forgejo: https://git.innomedia.de/Tietge/silverstripe-markup
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
| Recht | Für | Darf |
|---|---|---|
MARKUP_COMMENT („Anmerkungen: erstellen“) |
Kunde | Anmerkungen sehen, setzen und beantworten; eigene bearbeiten (solange offen), löschen (offen und ohne Antwort) und selbst abhaken |
MARKUP_MANAGE („Anmerkungen: verwalten“) |
Agentur | Alles 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 (overflow ≠ visible) 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) ausadmin/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.
| Route | Zweck |
|---|---|
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/comments |
Anlegen: JSON oder Multipart (data als JSON, attachments[]) |
POST markup/api/comments/{id} |
Eigenen Text bearbeiten, {body} |
POST markup/api/comments/{id}/screenshot |
Screenshot nachreichen (screenshot, optional data.marker) oder Scheitern melden (data.error); nur der Autor, einmal, bis 5 Minuten nach Anlage |
POST markup/api/comments/{id}/status |
Status setzen, {status} |
POST markup/api/comments/{id}/delete |
Löschen |
POST markup/api/comments/{id}/replies |
Antworten, {body} |
GET markup/api/media/{id} |
Screenshot oder Anhang, nur wenn die Anmerkung sichtbar ist |
GET admin/markup/board |
Karten 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-digestschickt 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-runnennt den konfigurierten Empfänger oder den Hinweis, ihn einzutragen.sake tasks:markup-cleanuplöscht Screenshot und Anhänge erledigter Anmerkungen, deren „Erledigt“ länger alsmedia_retention_dayszurü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_hourje 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 perfinfoaus 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/snapdomallein. Wird erst beim Start des Setzen-Modus nachgeladen, aus demselben Verzeichnis wieoverlay.jsund mit derselben?m=-Query.admin.jsundadmin.css— Board und Vorschau-Anbindung. Ohne eigenes React: Das esbuild-PluginadminGlobalsbiegtreact,react-dom,lib/Config,lib/ReactRouteRegisterusw. 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.jswirdasynceingebunden (zusätzlichdeferals 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 vorDOMContentLoaded.- 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, bevorbootstrapantwortet; die Antwort erneuert den Eintrag. Schreibende Aufrufe warten, bisbootstrapden aktuellen Token geliefert hat. snapdom.jswird nach dem Start im Leerlauf per<link rel="prefetch">(Safari:fetchmit 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 alsscreenshot_timeout_ms, bleibt die Anmerkung gültig undScreenshotErrorist 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 werdenbackdrop-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-typenur, wo nichts anderes eindeutig ist. Themes, die für Handy und Desktop getrennte Elemente rendern (etwalg: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 (
.swipermitel.swiper, auchloop) und<details>; andere Bibliotheken nur überregisterRevealer. 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.