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

Issues

Type:silverstripe-vendormodule

pkg:composer/tietge/silverstripe-markup

Statistics

Installs: 15

Dependents: 0

Suggesters: 0

1.0.2 2026-09-29 07:09 UTC

This package is auto-updated.

Last update: 2026-09-29 07:21:12 UTC


README

English · Deutsch

tietge/silverstripe-markup

Version 1.0 · SilverStripe 6 · PHP ≥ 8.3

Client feedback directly on the website, in the style of markup.io. Logged-in clients place a point (on desktop also an area) on the live page, describe the change and attach images. Each comment is anchored to the page and the Elemental block and stores a protected screenshot of the client's view. The agency works through the comments in the CMS: Kanban board, tab on page and block, badge in the site tree and a jump into the CMS preview with the comment open. No guest access, no inline editing, no external services.

Screenshots

Comment mode on the website: highlighted paragraph, newly placed point and the open form “What should be different here?”
Placing a point on the website.
Kanban board in the CMS with the columns Open, In progress, Question and Resolved
Board in the CMS with four status columns.
Comment on a phone: point on a paragraph, bottom sheet with text field and the buttons Photo and Image
Bottom sheet on phones.
Open point on the website with status “In progress”, client text, image attachment, agency reply and client response
Thread with status, attachment and replies.
Site tree with count badges on Home and Services, block list with badges on the affected blocks
Badges in site tree and block list.
Board with open side panel: status switch, screenshot crop with ring, attachment tile, details and thread
Side panel with screenshot, attachment, details and thread.

Features

  • Overlay (Shadow DOM, loaded only for members with permission, never in the cached HTML): point or area on desktop, point on touch/below 768 px with bottom sheet and separate “Photo”/“Image” buttons; image attachments (default max. 3).
  • Screenshot of the visible viewport (snapdom, in the browser), captured at the moment of the click, with the marker drawn in; stored protected.
  • Anchoring to Elemental block + selector path + text snippet, with fallbacks (text search, approximate position in the block). Points that are hidden (slider, <details>, other breakpoint) are listed under “Out of view” with a “Show” button; nothing is lost silently (“No longer found”).
  • Statuses Open, In progress, Question, Resolved. Clients can edit/delete their own open comments and mark them “Resolved for me”.
  • CMS: board at admin/markup (drag & drop, filters, side panel with reply field, image viewer), read-only tab “Comments (n)” on pages and blocks, badges in site tree and Elemental block list, “Open in CMS” opens the preview with the comment.
  • Deep links {page URL}?markup={ID}, independent of device and host; invite link that logs the client in and lands on the website (?markup=welcome).
  • Digest mail to the agency, customer notifications (bundled or immediate), automatic deletion of images after resolution.
  • UI in German and English (overlay: member locale, otherwise <html lang>; board: CMS locale).

Installation

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

Requires silverstripe/framework ^6, silverstripe/cms ^6, silverstripe/admin ^3, silverstripe/assets ^3. dnadesign/silverstripe-elemental is optional; without it comments are anchored to the page only and there is no block tab. Source: https://git.innomedia.de/Tietge/silverstripe-markup, changes in CHANGELOG.md (German).

Then set up the cron tasks. Enter the digest recipient under Settings › “Comments” (SiteConfig.MarkupAgencyEmail, visible with MARKUP_MANAGE only); without it no agency digest is sent.

Configuration

Defaults:

Tietge\Markup\Config:
  enabled: true                  # false: no overlay, /markup/api/… returns 404
  max_body_length: 4000          # characters per comment or reply
  max_attachments: 3             # image attachments per comment
  max_upload_bytes: 8388608      # per file (screenshot or attachment), 8 MB
  max_pixels: 40000000           # width × height, checked before decoding
  attachment_max_edge: 2000      # longest edge of an attachment after re-encoding
  screenshot_max_edge: 2000      # longest edge of a screenshot
  screenshot_timeout_ms: 8000    # capture time limit in the browser
  rate_limit_per_hour: 60        # write requests per member and hour, 0 = no limit
  media_retention_days: 90       # delete images this many days after “Resolved”, 0 = never
  resolved_visible_days: 30      # show resolved comments on the board this long, 0 = all
  customer_notifications: digest # digest (bundled, via cron) or immediate
  target_classes:                # allowed classes for block anchoring (incl. subclasses)
    - DNADesign\Elemental\Models\BaseElement

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

Tietge\Markup\Service\MediaStore:
  folder: 'markup'               # folder in the asset store
  quality: 85                    # JPEG/WebP when re-encoding

Tietge\Markup\Admin\ThumbnailRenderer:
  width: 640                     # card image on the board
  height: 400
  min_crop_width: 800            # minimum crop width within the screenshot
  attachment_edge: 320           # longest edge of attachment tiles in the panel
  quality: 82

Disable per environment:

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

Custom slider libraries can register a revealer so that “Show” can bring a hidden point into view (Swiper and <details> are built in):

window.tietgeMarkup?.registerRevealer((el) => {
  const slide = el.closest('.my-slide');
  if (!slide) return false;
  mySlider.goTo([...slide.parentElement.children].indexOf(slide));
  return true; // may also return a Promise (wait for the animation)
});

window.tietgeMarkup exists as soon as overlay.js runs; the app fires tietge-markup:ready on window when it is ready.

Permissions and groups

PermissionForAllows
MARKUP_COMMENT (“Comments: create”)ClientSee, create and reply to comments; edit own (while open), delete own (open, no replies), resolve own
MARKUP_MANAGE (“Comments: manage”)AgencySee everything, set status, reply, delete; board, tabs, site tree badges, Settings tab “Comments”

ADMIN includes both; CMS_ACCESS_LeftAndMain alone does not grant board access. db:build creates the group “Website comments” (code markup-feedback) with MARKUP_COMMENT once; it is identified by its code and may be renamed. Clients need an account in this group, otherwise the invite link logs them in without an overlay.

All checks go through canView(), canEdit(), canDelete(), canSetStatus() and canReply() on Tietge\Markup\Model\MarkupComment. Single tenant: every client sees all comments; restrict via an extension implementing canView().

The invite link is available under Settings › “Comments” (home page), in the “Comments (n)” tab of each page (that page) and via “Copy invite link” on the board; in code Tietge\Markup\Service\CommentLinks::inviteLink(?SiteTree $page = null).

Cron

Both tasks are CLI-only and support --dry-run. Run them as the web server user.

# Digest mails (agency and, with digest, clients), weekday mornings
0 7 * * 1-5 www-data cd /path/to/project && php8.4 vendor/bin/sake tasks:markup-digest >> /var/log/markup-digest.log 2>&1

# Delete images of resolved comments, nightly
30 2 * * * www-data cd /path/to/project && php8.4 vendor/bin/sake tasks:markup-cleanup >> /var/log/markup-cleanup.log 2>&1
  • tasks:markup-digest: one mail to the agency with all unreported comments and client replies, grouped by page; with customer_notifications: digest one mail per client with agency replies and resolved comments. --only=agency|customers limits the run.
  • tasks:markup-cleanup: deletes screenshot and attachments media_retention_days after “Resolved”; text and anchoring remain.
  • customer_notifications: immediate sends client mails on save instead (new reply, resolved). Own replies and self-resolved comments never trigger a mail. Sender is Email.admin_email; failed sends are logged and never break saving.

Without cron: no agency digest, no client mails (with digest), no image cleanup.

API

JSON only, Cache-Control: no-store. Session auth; 401 without login, 403 without permission, 404 for comments the member may not see (also when the page is not viewable). Every POST needs the X-Securityid header (token from GET session) and a matching Origin if set; no CORS. Errors: {"error": {"code": "…", "message": "…"}} (400 invalid_json, 404, 405 with Allow, 409 screenshot_exists/screenshot_window_closed, 422 validation, 429 rate_limited with Retry-After, 500 internal_error).

RoutePurpose
GET markup/api/bootstrap?url=session and comments in one response (overlay start)
GET markup/api/session?url=Member, permissions, locale, CSRF token, limits, page and block anchors
GET markup/api/comments?url=Comments for the normalised URL (matched by path and query, not host)
GET markup/api/comments/{id}Single comment, if visible
POST markup/api/commentsCreate: JSON or multipart (data as JSON, attachments[])
POST markup/api/comments/{id}Edit own text, {body}
POST markup/api/comments/{id}/screenshotUpload screenshot (screenshot, optional data.marker) or report failure (data.error); author only, once, within 5 minutes
POST markup/api/comments/{id}/statusSet status, {status}
POST markup/api/comments/{id}/deleteDelete
POST markup/api/comments/{id}/repliesReply, {body}
GET markup/api/media/{id}Screenshot or attachment, if the comment is visible
GET admin/markup/boardBoard cards, filters page, author, q, all=1
POST admin/markup/status/{id}Status from the board, {status}
POST admin/markup/reply/{id}Agency reply, {body}
GET admin/markup/thumbnail/{id}JPEG 640×400 around the point, else 404
GET admin/markup/attachment/{commentId}/{imageId}Attachment scaled down (JPEG, 320 px), else 404

Board routes require MARKUP_MANAGE.

Security and privacy

  • Stored: text, page URL (without tracking/preview parameters), anchoring data, viewport, pixel ratio, scroll position, user agent, author, timestamps, screenshot and attachments.
  • Images live under markup/YYYY/MM/ in the asset store, protected immediately (folder “only these users”, no groups), delivered only via markup/api/media/{id} after a permission check. Upload is not used, so project extensions that publish uploads do not apply. Board thumbnails are generated in TEMP_PATH/markup-thumbs/, never in assets/.
  • Uploads: real HTTP uploads only, MIME via finfo (JPEG, PNG, WebP, GIF), size and pixel limits before decoding, re-encoded server-side (EXIF/GPS removed, GIF becomes PNG).
  • Plain text only, escaped everywhere. URLs same-origin only, max. 2083 characters. Block anchoring only for target_classes the member may view.
  • Rate limit per member and hour across create, edit, status, delete and reply (screenshot upload excluded).
  • Deleting a comment removes replies and images immediately.
  • Screenshots show a logged-in member's view (in a shop possibly cart or account data); mention this in the privacy notice for clients.

Limitations

  • Screenshots are best effort: no video/iframes, no cross-origin images without CORS, no backdrop-filter, animations captured mid-state. Metadata is reliable; on failure or timeout the comment is kept with ScreenshotError.
  • Points and areas cannot be placed by keyboard (reading, replying and all thread actions can).
  • Anchored to the element, not the word; a point may shift by a word when text reflows.
  • “Show” supports Swiper and <details>; other libraries need registerRevealer. Pure CSS animations do not trigger re-evaluation.
  • “Open at phone size” uses a popup window of the original width (no touch/DPR emulation). Changed page URLs are not tracked.
  • Single tenant; restrict visibility via extension.

Development

npm install
npm run build        # rebuild client/dist (committed)
npm run dev          # watch mode
npm run typecheck
npm test             # vitest (jsdom)
npm run mock         # overlay without SilverStripe: http://localhost:4455/ (?as=agency, &CMSPreview=1)

SS_PHPUNIT_FLUSH=1 vendor/bin/phpunit vendor/tietge/silverstripe-markup/tests   # in a project

Bundles (esbuild): overlay.js, snapdom.js (prefetched when idle, executed on entering comment mode), admin.js/admin.css (uses the silverstripe/admin globals, no own React). Overlay and board texts are in client/src/overlay/i18n.ts and client/src/admin/i18n.ts, PHP texts in lang/*.yml. phpcs.xml.dist (PSR-12) and phpstan.neon.dist (level 5) are included.

Internals (anchoring, visibility, deep-link flow, screenshot, performance, hosting, path repository): docs/ARCHITECTURE.md (German).