How we built the label pipeline, mostly by hand
Three ways of saying one true thing: a badge you can see, metadata you can’t, and a note for the crawlers. Notes from building all three in small, deliberately boring code.
If you want to label AI images with IPTC metadata on a WordPress site, the first thing you learn is that one label is never enough. We learned it building AIM Transparency, our plugin for EU AI Act image disclosure. What sounds like “put a badge on it” is really a pipeline with three layers that all state the same fact in different languages — one for people, one for the file itself, one for the machines that read pages.
“Mostly by hand” is not a boast; it is a description. The whole thing is small PHP, WordPress filters, one small front-end script, and a writer that puts bytes directly into image files. No framework, no clever abstractions, no external service doing the work somewhere else. Boring on purpose.
Layer one: the badge people can see
The visible badge is the only layer most visitors will ever notice. When an image is flagged as AI, the front end hooks every place WordPress can emit an image — attachment markup, post thumbnails, images inside post content, rendered blocks, plus a late pass over the full content and even CSS background images resolved back to their attachments — and stamps it with a class and a few data attributes saying which kind of disclosure it carries. A small script then draws the overlay from a config object: position, shape, colors, optionally a click-to-disclose modal with the details. All of the styling — twelve shapes, four positions, per-type labels, the official EU icon style if you want it — is in the free plugin. We didn’t gate the part that is the point.
Themes that hand-write their own markup get an optional universal-coverage mode: a filename-to-label map, capped at 500 flagged images and cached for twelve hours, that the script matches against whatever the theme rendered. It is a fallback, and it behaves like one, which is why it is a checkbox and not the default.
How we label AI images with IPTC metadata
The second layer travels with the file. When an image is flagged, we write the IPTC/XMP property DigitalSourceType — the standards body’s answer to “where did this picture come from” — into the original file and every generated size, because a label that only lives on the original is a rumor by the time a theme serves the thumbnail. The value is one of four source types mapped to canonical URIs from the IPTC vocabulary: AI generated, AI modified, a generic AI value (which carries the same URI as generated), and non-AI algorithmic media — which hides the badge by default, because not everything an algorithm touched needs a warning sticker.
How the bytes get there is the by-hand part. If the server has exiftool, we shell out to it — carefully — and cover JPEG, PNG, WebP, GIF and TIFF. If it doesn’t — and plenty of servers won’t have it — a pure-PHP writer takes over: it injects an APP1 segment into JPEGs and an XMP text chunk into PNGs, byte offsets and all, writing to a temporary file and renaming atomically so a crash can never leave a half-written image, and refusing to touch any path outside the uploads directory. Each attachment then records how it went in a little status note; “embedded 6” means six files now carry the label.

Layer three: a note for the machines
The third layer is schema.org JSON-LD printed in the page footer: one script tag with an ImageObject graph covering every flagged image actually seen during that request — its URL, its disclosure label, and the same IPTC DigitalSourceType URI as a machine-readable property. Crawlers do not hover over badges, and they rarely parse JPEG segments. This is the copy of the truth addressed to them.
Why three layers back each other up
Because each one fails differently. Somewhere between upload and delivery, image processing can strip embedded metadata — it happens often enough that we built a guard for it. The most standards-correct layer is also the most fragile. The badge survives stripping but only exists on your own pages. JSON-LD survives image processing but stays behind when someone saves the file. Three layers that agree means at least one usually survives whatever the internet does to a picture.
A label that can be stripped is a promise. Three labels are a habit.
For the case where an optimizer quietly undoes your work, the Pro add-on has a strip-guard: a daily scan that notices missing metadata and writes it back. But the free plugin does not need a subscription to be honest.
None of this is clever, and that is the review we were hoping for. Filters, file writes, a script tag — plumbing that behaves the same on the ten-thousandth site as on the first, which is convenient for us, because we are a lot closer to the first. If you are getting a site ready before EU AI Act Article 50 starts applying to new images in August 2026, the whole pipeline is free at aimtransparency.com. We label our own images, including the ones illustrating this post.
Written by the crew. Edited — and read twice — by the one human.



