Ich habe noch nie über dieses Tool aus technischer Perspektive geschrieben, obwohl es seit einer Weile fertig ist. Zeit, das nachzuholen, mit allem, was beim Bauen schiefging und was ich daraus mitgenommen habe.
Mein KI-Kennzeichnung Tool setzt sichtbare EU-Icons oder ein Textlabel in Bilder, wie es Artikel 50 Absatz 4 der KI-Verordnung für veröffentlichte KI-Inhalte verlangt. Mein Dreh: echte EU-Icons der Kommission statt nachgebauter Grafiken, Stapelverarbeitung und eine zweisprachige Seite, die auf die digitalhandwerk-Entität einzahlt. Zielort ist watermark.digitalhandwerk.rocks, gebaut komplett clientseitig mit Canvas-API, ohne Server, ohne Upload, ohne Build-Prozess. Ein FTP-Ordner reicht.
Was die Seite technisch ist
Kein Framework, kein npm, kein Build-Schritt. Der Ordner enthält index.html für die deutsche Fassung, en/index.html für die englische mit eigenem Schema, icons.js mit den sechs offiziellen EU-SVGs als base64-Data-URIs, einen lokal gehosteten fonts/-Ordner mit Inter Tight, DM Mono und Fraunces Italic, dazu Wortmarke, Autorenbild und Social-Vorschau als einzelne Bilddateien. .htaccess, robots.txt, sitemap.xml und llms.txt regeln Sicherheit und Crawling.
CSS und JavaScript sind in beiden Sprachfassungen zeichengleich, nur Pfade und eine Anker-ID unterscheiden sich. Ein Diff-Skript prüft das nach jeder Änderung, sonst laufen die zwei Dateien mit der Zeit auseinander, und genau das ist einmal passiert, dazu gleich mehr.
Funktionsumfang
Entweder EU-Icon oder Textlabel, nie beides. Doppelt gekennzeichnet ist nicht klarer, nur voller. Drei Icon-Varianten stehen zur Wahl: vollständig KI-generiert, teilweise bearbeitet, KI war beteiligt. Position lässt sich auf vier Ecken setzen, Labelfarbe hell oder dunkel, Hintergrund dunkel, hell oder ganz ohne, Größe zwischen 2 und 15 Prozent der Bildbreite, dazu Deckkraft. Export geht als PNG, JPEG oder WebP.
Die Stapelverarbeitung erlaubt mehrere Bilder auf einmal, mit Vorschau-Auswahl per Dateiname, alle mit denselben Einstellungen. Der Download läuft als sequenzielle Einzeldateien mit 300 ms Pause dazwischen, kein ZIP. Ein Store-ZIP wäre rund 40 Zeilen selbstgeschriebenes Binärformat oder eine zusätzliche Dependency gewesen, der Aufwand stand bei kleinen Stapeln nicht im Verhältnis zum Nutzen.
Maschinenlesbare Herkunft: IPTC DigitalSourceType
Das sichtbare Label ist die eine Hälfte, die andere ist IPTC DigitalSourceType als XMP direkt in der Bilddatei. Der Wert folgt der Icon-Wahl: vollständig KI-generiert wird zu trainedAlgorithmicMedia, teilweise bearbeitet zu compositeWithTrainedAlgorithmicMedia, KI beteiligt zu compositeSynthetic. Alle drei Werte sind überschreibbar, WebP kann das Tool nicht, das zeigt die UI sichtbar an.
Technisch passiert das nach dem Canvas-Export direkt im Byte-Container: bei JPEG als APP1-Segment hinter dem JFIF-Block, bei PNG als iTXt-Chunk hinter dem IHDR mit selbst gerechnetem CRC32. Rund 90 Zeilen, keine Bibliothek. Verifiziert habe ich das mit Pillow gegen die echten Browser-Bytes, nicht gegen meinen eigenen Code, sonst prüft man nur, ob der eigene Fehler konsistent ist.
Und dabei bin ich auch auf den unangenehmsten Bug der ganzen Entwicklung gestoßen: Die JPEG-Segmentlänge zählt sich selbst mit, ein Segment endet bei 4 + len, nicht bei 4 + len - 2. In einer früheren Fassung war der Abzug drin und hat das XMP mitten in den JFIF-Block geschrieben. Die Datei blieb lesbar, kein Tool fand aber die Metadaten. Genau dieser Bug wäre ohne den Pillow-Abgleich gegen echte Browser-Bytes durchgerutscht, weil die Datei ja funktionierte, nur eben ohne den Inhalt, den sie tragen sollte.
Die inhaltliche Präzisierung, die den Unterschied macht
Das Tool bedient bewusst nur Artikel 50 Absatz 4, die Offenlegungspflicht für Betreiber, die KI-Inhalte veröffentlichen. Dafür reicht eine klare, wahrnehmbare Kennzeichnung. Absatz 2 betrifft Anbieter von KI-Systemen und verlangt eine maschinenlesbare Markierung, etwa über C2PA Content Credentials, ein sichtbares Label erfüllt das ausdrücklich nicht. Die meisten Zusammenfassungen im Netz werfen beide Absätze zusammen, genau diese Unterscheidung ist der inhaltliche Kern der Seite.
Dazu die Klarstellung, dass Absatz 4 auf Deepfakes zielt, nicht auf jedes KI-Bild. Eine abstrakte KI-Textur fällt nicht darunter, eine KI-Person, die wie ein Teamfoto aussieht, schon.
Wie das IPTC-Feld in der Praxis ankommt
Im IPTC-Test vom März 2026 werteten Instagram und Pinterest das Feld aus, LinkedIn liest stattdessen C2PA, TikTok und YouTube erkannten nichts. Geprüft wurde dabei Material aus Metas eigenem Generator, ob ein nachträglich gesetztes Feld genauso behandelt wird, sagt der Test nicht. Meine erste eigene Beobachtung: Instagram labelt nicht automatisch, dort greift nur der manuelle Schalter.
Wichtig: Das Feld ist keine C2PA-Signatur und belegt keine Herkunftskette, es erfüllt die Anbieterpflicht aus Absatz 2 nicht, es ist eine Angabe des Veröffentlichenden. Wer den falschen Wert wählt, hinterlegt eine falsche Aussage in maschinenlesbarer Form. Und die Zuordnung ist eine Falle: compositeWithTrainedAlgorithmicMedia heißt laut IPTC „Edited using Generative AI“, also mit KI bearbeitet, nicht „KI-Teile kombiniert“. Dafür gibt es compositeSynthetic, der Name legt das Gegenteil nahe.
Technische Grundentscheidungen
Canvas-API, keine Dependency. Bild rein, Label drauf, toBlob raus. Nichts verlässt den Rechner, das ist das Verkaufsargument gegenüber jedem serverbasierten Tool.
Icons als base64 in icons.js. Aus dem offiziellen SVG-ZIP der Kommission gezogen, nicht nachgebaut. width und height sind aus der viewBox eingesetzt, sonst rastert Firefox auf 300×150.
Fonts lokal, kopiert aus dem Elementor-Font-Ordner der Hauptseite. Kein Google-CDN, DSGVO-sauber und optisch identisch.
FAQ als natives <details>, kein Accordion-JavaScript.
Matomo (self hosted), gleiche SiteId für beide Sprachen, mit disableCookies vor trackPageView. Nachgemessen: keine _pk_-Cookies.
Ein echter Sicherheitsfund
Dateinamen gingen per innerHTML in die Vorschau-Liste. Ein Name wie <img onerror=…>.png hätte Code ausgeführt, Self-XSS, von außen nicht auslösbar, aber real. Behoben über createElement plus textContent, gegengetestet.
Die .htaccess setzt Verzeichnisauflistung aus, nosniff, frame-ancestors nur für eigene Domains, Referrer- und Permissions-Policy sowie Caching. Vor der Domain steht Cloudflare, läuft der Origin unter nginx, wird die Datei ignoriert und die Header müssen in die Serverkonfiguration.
Crawling ist ausdrücklich erwünscht: robots.txt erlaubt alles und listet GPTBot, ClaudeBot, PerplexityBot, Google-Extended und weitere einzeln auf, sitemap.xml deckt beide Sprachen mit hreflang ab, llms.txt fasst die fachlichen Kernaussagen maschinenlesbar zusammen.
SEO und E-E-A-T
Zwei echte URLs statt JS-Umschaltung, verbunden über hreflang de/en/x-default in beide Richtungen und im Schema über workTranslation/translationOfWork. Das JSON-LD nutzt für Person und LocalBusiness dieselben @id-Werte wie die Hauptseite, damit Google und LLMs die Entität konsolidieren, dazu eine sichtbare Autorenbox, weil Schema ohne sichtbaren Beleg wenig trägt.
FAQ-Rich-Results hat Google am 07.05.2026 abgeschaltet, das FAQPage-Markup bleibt gültig und schadet nicht, sein Wert liegt heute bei AI Overviews und LLM-Crawlern, nicht mehr im SERP-Snippet.
Bewusst nicht gebaut: Video
Am 25.08.2026 geprüft und verworfen. WebCodecs bräuchte eigenes MP4-Muxing und läuft nicht auf Firefox Android, ffmpeg.wasm hat ein hartes 2-GB-Limit und kann auf iOS Safari schon bei 200 MB abstürzen. Für 4K-Kundenvideos müsste die Verarbeitung auf einen Server, und damit wäre das „kein Upload“-Versprechen weg. Der Aufwand stand nicht im Verhältnis.
Was mich beim Bauen erwischt hat
Ein paar Fehler, an denen ich länger hing als geplant, für alle, die Ähnliches vorhaben:
- Die drei EU-Icons haben unterschiedliche Seitenverhältnisse, 1:1, 3,16:1, 3:1. Quadratisch gezeichnet werden zwei davon gequetscht. Breite immer aus
naturalWidth/naturalHeightableiten, nicht aus einem fixen Wert. clamp()ohne Leerzeichen um das Plus,1.4rem+2.4vwstatt1.4rem + 2.4vw, ist ungültiges CSS und reißt die gesamtefont-Shorthand mit. Die Headline fällt still auf die geerbte Schrift zurück, ohne Fehlermeldung im Browser.- Beim Übersetzen wurden nur die HTML-Texte erfasst. Die UI-Strings im JavaScript blieben deutsch, die englische Seite zeigte „2 Bilder herunterladen“. Die Lösung war eine kleine Textkonstante mit beiden Sprachen, gesteuert über
<html lang>, damit das Skript in beiden Dateien identisch bleibt. - Media Queries lassen sich nicht testen, indem man die Breite eines Elements per DevTools ändert. Sie hängen am echten Viewport. Ein ganzer Messdurchlauf war dadurch wertlos, bevor ich das gemerkt habe.
- Emoji-Flaggen rendern unter Windows nicht als Flagge, sondern als Buchstabenpaar. Der Sprachwechsler nutzt deshalb Inline-SVG, zusammen unter 600 Byte.
- Python schreibt im Textmodus unter Windows CRLF. Ein einziger Python-Edit stellte alle Zeilenenden der englischen Datei um und brach den Diff-Check zwischen den Sprachfassungen.
Was noch fehlt
Bei Hintergrund „ohne“ gibt es keinen Schatten hinter Icon oder Text, auf unruhigen Motiven kann das Label untergehen, ein weicher Schatten wären drei Zeilen. Ein ZIP-Download für größere Stapel steht noch aus, falls die Einzeldateien im Alltag nerven. WebP-Metadaten fehlen ebenfalls, dafür müsste der RIFF-Container auf VP8X umgebaut werden, falls das Format im Alltag wichtiger wird als jetzt.
