Version 2026-10-11. Prepared by Silvr for publishing partners.
The Silvr overlay is one publisher-installed script tag that loads a small Silvr loader. Every heavy or secret part (image fetch, recognition, caching, quotas, result rendering) runs behind a Silvr-hosted service. The publisher adds the tag (directly or through their tag manager), allow-lists two Silvr hosts in their Content Security Policy, and is done. Finding images, placing the + over a specific watch, and showing the result all happen in Silvr-controlled code.
Why this shape:
One pass through the system:
POST /v1/detect).object-fit, carousel transform).GET /v1/item/{id}); the proxy returns the SKU match and similar items as a ready-to-insert HTML fragment, which the loader shows in a drawer beside the image.The in-browser part is a small JavaScript loader that Silvr serves and configures.
Silvr delivers the overlay as an injected script because it is the only option that is reliable on pages Silvr does not control, fast once cached, and installable in under an hour. Injection through your own CDN worker is an alternative install path with the same behaviour, and a CMS pre-render that stores results at publish time is an optional accelerator, not a requirement.
What activation looks like for a publisher.
pk_live_example_8f3a. The key is safe to expose: it only works from those origins and is rate-limited.<script async src="https://cdn.silvrshop.com/v1/loader.js" data-silvr-key="pk_live_example_8f3a"></script>https://api.silvrshop.com to script-src and connect-src in their Content Security Policy, and https://cdn.silvrshop.com to img-src if product thumbnails are served from Silvr.enabled: false) takes effect within a minute, with no deploy on either side.The same tag works on every site. Differences between sites (image URL shapes, video player, section paths, CSS stacking) live in the per-publisher config that the loader downloads, never in the tag.
Where the Silvr proxy sits. One service, six jobs: serve the loader and per-publisher config; accept detection requests from the browser and fetch the images itself; hold the only credential for the recognition service and call it; cache results per image and enforce the publisher's quota; render the result panel as HTML fragments; run the pre-processing worker that warms the cache for new articles and video keyframes. A reader's browser never talks to the recognition service and never sees its key: nothing secret is stored on a client.
The browser finds the images and places the markers; Silvr's detector on the proxy finds the items inside each image and Silvr's recognition service names them. Nothing in the browser looks at pixels. The loader sends image URLs (or, for video, one captured frame), the proxy fetches the image, detects the items, recognizes each item, and stores the boxes normalized so they can be mapped onto the image however the page happens to display it.
Step 1. Discovery: which images qualify. The loader walks the document once at start and then relies on observers. An element is a candidate when all of these hold, each threshold a config value:
<img> (including inside <picture>), a <video> with a poster, or a <video> itself.article img, figure img, .gallery img, video) and none of the exclude selectors (default [data-ad] *, .ad *, [aria-hidden="true"] *, header *, footer *, nav *). Ads inside iframes are invisible to the loader anyway.data: URI or an SVG, and the image has finished loading (complete and naturalWidth above 0). A blurred low-quality placeholder fails this test until the real image replaces it.Ads are never candidates, as images or as video. Ads reach a page three ways and each is excluded. Programmatic ads in iframes (Google Ad Manager and most networks) are invisible to the loader, which cannot see inside another origin's frame. Native and house ads rendered in the page itself (Taboola, Outbrain, sponsored blocks, the publisher's own promotions) are excluded by selector: the defaults cover [data-ad] *, [id^="div-gpt-ad"] *, .ad *, .OUTBRAIN *, [class*="taboola"] *, [class*="sponsored"] * and images wrapped in links to known ad hosts, and each publisher adds its own ad container selectors through the integration checklist. Video ad breaks inside the publisher's player (pre-roll or mid-roll through Google IMA or the player's ad plugin) are skipped: the loader listens for the player's ad-start and ad-end events where the player exposes them, and otherwise does not capture while the element's source differs from the content source. A reader who pauses during an ad sees no affordance. The selectors are verified on your real pages before launch.
Step 2. Lifecycle: knowing when an image appears, changes or leaves. Three browser observers cover every surface in scope:
IntersectionObserver with a root margin of one viewport height tells the loader when a candidate is about to scroll into view, and gives the visible portion (intersectionRect) for clipping markers inside carousels.MutationObserver on document.body catches images inserted later (lazy loaders, infinite scroll, lightboxes) and attribute changes to src and srcset (slideshows that reuse one <img>).ResizeObserver on each tracked image, plus load events, catch layout changes and the moment a lazy image gets its real source.The loader never sets src itself and never wraps or moves the publisher's elements. It only reads.
Step 3. Canonical key and request. The loader reads currentSrc, which is the variant the browser chose from srcset, and sends it with the element's natural size. The proxy normalizes the URL into an image key by stripping size and quality parameters it knows (for Arc XP resizer URLs: width, height, quality, smart; other resizers are added per publisher), and keys the cache on that plus a rounded aspect ratio. Ten readers on ten screen sizes therefore hit one cache entry for one photo. As a safety net the proxy also indexes results by a perceptual hash of the fetched image, so an unknown resizer still dedupes. A cached result is re-checked once a day with a lightweight request that asks only whether the file changed; if it did, the image is analysed again.
Step 4. Localization happens server-side. The proxy fetches the largest same-aspect variant of the image the reader sees (Arc resizer width raised, Contentful ?w=1600), finds every fashion item in it with Silvr's vision service, and stores each item's box and pin anchor as fractions of the analyzed image. Every item gets its own box and its own recognition, so a photo with a blazer, a bag and boots yields three pins, not one; up to five pins are kept per image. Fetching server-side avoids two browser problems: cross-origin images cannot be read from a canvas unless the publisher's CDN sends CORS headers (the tainted-canvas rule), and a 2 MB photo should not be re-uploaded from every reader's phone. Because the analyzed variant has the same aspect ratio as the displayed one, the normalized boxes map straight onto the element.
Step 5. Mapping a box onto the displayed image. Because detection ran on the same variant the reader sees, the fetched aspect ratio equals the element's natural aspect ratio, and the only transform left is the CSS one. For an element with rect R (getBoundingClientRect), natural size (nw, nh) and a normalized box (bx, by, bw, bh):
object-fit: fill or unset: drawn region equals R.object-fit: cover: scale s is the larger of R.width/nw and R.height/nh; drawn size is (nw·s, nh·s); the offset is (R.width minus drawn width) times the object-position fraction, default one half. contain uses the smaller scale.intersectionRect.Carousel transforms need no special handling because getBoundingClientRect already includes them. When currentSrc changes to a variant with a different crop (a <picture> that switches art direction at a breakpoint), the key changes and the loader requests that variant; the old markers are dropped.
Step 6. Drawing. One host element is appended to document.body, position: fixed, full viewport, pointer-events: none, with a Shadow DOM root so publisher CSS cannot leak in or out. Each tracked image gets a layer inside the host, repositioned on scroll and resize with requestAnimationFrame throttling. Only the affordance, the markers and the panel accept pointer events, so the overlay never blocks a click on the publisher's page or on an ad. Markers sit at each item's pin anchor (the item's centre of mass, falling back to the box centre). The layout is designed for up to five markers on one image: when two anchors fall within 32 px, the later marker shifts along the top edge of its box; every marker keeps a 44 px touch target; and on screens narrower than 480 px with four or more markers, the drawer opens on an "All items" list first so the reader can pick by name instead of by position. The affordance shows the item count, for example "Shop this image, 5", so the reader knows what to expect. The layer's z-index is a config value, default just below the maximum, so it sits above lightboxes and sticky headers. The pill, the markers and the drawer are labelled buttons that work without a mouse: Tab reaches the pill, Enter opens it, the arrow keys move between markers, Escape closes the drawer, and a screen reader announces the item count and each item's name.
Per-surface handling.
| Surface | What is different | What the loader does | What pre-processing adds |
|---|---|---|---|
| Static HTML images | Nothing | Discovery at start, request when within one viewport | Results are already cached, so the affordance appears on first paint |
| Lazy-loaded images | Element exists but has a placeholder or no source until near the viewport; JavaScript lazy loaders swap data-src into src | Waits for load with a real naturalWidth; keys on the final currentSrc; re-evaluates on src or srcset mutation | Same as static; the crawler scrolls the page so lazy images load before extraction |
| Carousels and slideshows | Slides sit in a transformed track or one <img> swaps its src; some libraries clone the first and last slides; hidden slides may be aria-hidden | Treats each distinct image key once, so clones cost nothing; tracks only slides with intersection above one half; hides markers during the slide transition and redraws on transitionend; cancels stale in-flight requests when a swapped src changes; prefetches the neighbours of the visible slide | All slides warmed, so swiping feels instant |
| Galleries with a lightbox | Thumbnails are too small to qualify; the full image is inserted into a fixed, high z-index container on click | MutationObserver picks up the inserted image; the fixed host and configurable z-index keep markers above the lightbox | Full-size variants warmed from the gallery's image list |
| In-page video players (Arc's player, video.js and similar) | A <video> element is in the DOM; frames change; the poster is a normal image | Shows Shop This Video from the poster; on activation pauses the video, draws the current frame to an offscreen canvas at 1280 px long side, posts the JPEG with the video key and the time rounded to half a second; maps boxes with videoWidth and videoHeight the same way as images; hides markers on seek or play | Keyframe extraction server-side (ffmpeg scene cuts plus one frame every two seconds, deduped by perceptual hash) gives the item count and timestamps for a timeline strip, and covers Safari, where native HLS taints the canvas |
| Third-party iframe players (YouTube, Vimeo) | The frame contents are a different origin; nothing inside can be read | Thumbnail only via oEmbed, or nothing | Only if the publisher provides the source file |
Two notes on video. First, frame capture works in Chrome, Firefox and Edge for any player that feeds the element through Media Source Extensions, which is how HLS plays in those browsers; it works in Safari only when the element carries crossorigin="anonymous" and the CDN sends CORS headers, which is a one-attribute change for the publisher. Second, the overlay does not track items across frames while the video plays. Markers appear on a paused frame, which is the same model the editorial demo uses (Shop This Video, a count badge, a timeline). Third, animated images (GIF, animated WebP) follow the video model: the proxy extracts their frames server-side, and the overlay offers a frame picker over a still, so an item that passes by in the animation can still be shopped.
Where pre-processing on page rollout helps. New article URLs come from three sources, in order of lift: polling the publisher's news sitemap or RSS, so new pages are processed within minutes of publication (no publisher work), a server-to-server call from the publisher's CMS on publish (POST /v1/preprocess), and the first reader's visit, which warms the cache for everyone after. The worker renders the page with Playwright, scrolls to trigger lazy loading, extracts candidates with the same rules the loader uses (the rules are one JSON spec shared by both), runs detect and identify, and stores the results. On a pre-processed page the cold path never runs for a reader, hover is instant because identification already happened, and recognition calls spread over the day instead of following traffic spikes. If a publisher grants read access to the Arc XP Content API, the worker can take the story's image list from JSON instead of rendering the page, which also avoids paywall truncation.
Three parts belong to Silvr: the loader in the page, the Silvr proxy, and the pre-processing worker. The browser only ever talks to Silvr's proxy, and the proxy is the only thing that talks to Silvr's recognition service.
system architecture · 3 new parts, 5 existing, what calls what
The accented boxes are the parts Silvr runs; every arrow is a call, and the only path to the recognition service runs through the proxy.
The page loads the script from the CDN and sends image URLs to the proxy; the proxy fetches the images, calls the recognition service with its credential, and keeps every answer in its cache. The worker warms the same cache from the publisher's feeds before readers arrive.
Request flow for one image.
POST /v1/detect with up to 10 image URLs.pending immediately, and finishes in the background: fetch the largest variant, find every fashion item, recognize each one, curate, store. This typically completes within a minute. Nothing is shown to the reader for that image unless they are still on it when the result lands.GET /v1/item/{item_id}. The proxy serves the top match and its alternates from the stored pins, rendered as an HTML fragment; the loader opens the drawer. No live recognition call happens here.sendBeacon to POST /v1/events.Target budgets: affordance visible within 300 ms of an image entering the viewport when warm; drawer open within 400 ms warm. There is no live cold path for readers: a cold image returns pending, the work runs in the background (typically within a minute), and the result is cached for the next reader. Pre-processing from the publisher's sitemap within minutes of publication keeps cold images rare. A cold result that lands while the reader is still on the image is drawn then.
Draft API contract. All browser-facing routes take the publishable key and check the request's Origin against the publisher's registered domains. Responses carry X-Silvr-Quota-Remaining. The generated OpenAPI page is the reference for every route and field.
| Route | Called by | Purpose | Cache |
|---|---|---|---|
GET /v1/loader.js | Publisher page | The loader; lazy-loads the overlay and panel modules | CDN, 5 min, then immutable hashed modules |
GET /v1/config?key= | Loader | Per-publisher policy, selectors, ad exclusions, UI options, tracking level, kill switch | CDN, 60 s |
POST /v1/detect | Loader | Batch of image URLs in, boxes per image out | Cache per image key, 7 days |
GET /v1/image/{image_key} | Loader, CDN | Cached detect result for one image; a CORS simple request | CDN plus cache, 7 days |
POST /v1/detect-frame | Loader | One captured video frame (multipart JPEG) with video key and time | Cache per video key and half-second, 7 days |
GET /v1/item/{item_id} | Loader | SKU match, similar items, affiliate links, HTML fragment | Cache, 24 h (prices change) |
POST /v1/events | Loader | Interaction events: anonymous counts or full session, per the publisher's choice | none |
POST /v1/preprocess | Publisher CMS or Silvr's own poller, server to server with a secret key | Enqueue page, image or video URLs for warming | queue |
Example detect request and response:
POST /v1/detect
{
"key": "pk_live_example_8f3a",
"page_url": "https://www.example-publisher.com/lifestyle/style/2026/10/09/fall-tailoring/",
"images": [
{"id": "c1", "url": "https://www.example-publisher.com/resizer/ABC123.jpg?width=1200", "nw": 1200, "nh": 800},
{"id": "c2", "url": "https://www.example-publisher.com/resizer/DEF456.jpg?width=960", "nw": 960, "nh": 1200}
]
}
200 OK
{
"results": [
{"id": "c1", "image_key": "img_7c1e9a", "status": "ok", "items": [
{"item_id": "it_5f2a", "category": "blazer", "confidence": 0.93, "box": {"x": 0.41, "y": 0.22, "w": 0.18, "h": 0.30}},
{"item_id": "it_5f2b", "category": "waistcoat", "confidence": 0.88, "box": {"x": 0.44, "y": 0.35, "w": 0.12, "h": 0.20}}
]},
{"id": "c2", "image_key": "img_90aa01", "status": "pending", "retry_after_ms": 800}
],
"quota": {"remaining_minute": 582}
}
Status values: ok, no_items, pending, skipped (quota or allow-list), error. Boxes are fractions of the fetched image, origin top-left. An item response:
GET /v1/item/it_5f2a?key=pk_live_example_8f3a
{
"item_id": "it_5f2a",
"match": {"brand": "The Frankie Shop", "title": "Gelso Oversized Blazer - Dark Grey Melange", "price": {"amount": 425, "currency": "USD"}, "image": "https://cdn.silvrshop.com/p/9a1f.jpg", "url": "https://affiliate-redirect.silvr.fashion/out?u=https%3A%2F%2Fthefrankieshop.com%2Fproducts%2Fgelso-blazer&source=overlay_example&page=https%3A%2F%2Fwww.example-publisher.com%2Flifestyle%2Fstyle%2F2026%2F10%2F09%2Ffall-tailoring%2F&img=img_7c1e9a&item=it_5f2a&session=s_4e1c2a", "confidence": 0.91},
"similar": [{"brand": "...", "title": "...", "price": {"amount": 235, "currency": "USD"}, "image": "...", "url": "https://affiliate-redirect.silvr.fashion/out?u=...&source=overlay_example&img=img_7c1e9a&item=it_5f2b&session=s_4e1c2a"}],
"html": "<section class=\"silvr-item\">...</section>"
}
Every url goes through Silvr's affiliate redirect. The page, img, item and session parameters are stored with the click by Silvr's affiliate redirect, so clicks and conversions report per article and per image; under counts tracking only source is sent.
The drawer ends with an affiliate disclosure line: the publisher's own wording when they provide one, Silvr's default line otherwise.
The publisher config the loader downloads:
{
"publisher": "example", "enabled": true,
"trigger": "viewport", "batch_window_ms": 150, "prefetch_viewports": 1, "max_batch": 10, "max_images_per_page": 40, "min_rendered_px": 200,
"selectors": {"include": ["article img", "figure img", ".gallery img", "video"], "exclude": ["header *", "footer *", "nav *"]},
"ads": {"exclude": ["[data-ad] *", "[id^=\"div-gpt-ad\"] *", ".ad *", ".OUTBRAIN *", "[class*=\"taboola\"] *", "[class*=\"sponsored\"] *", ".wp-ad *"]},
"path_allowlist": ["/lifestyle/", "/style/"],
"path_denylist": ["/lifestyle/2026/10/08/some-sensitive-story/", "*/live-updates/*"],
"video": {"mode": "pause_to_shop", "ad_events": "ima"},
"ui": {"affordance_label": "Shop this image", "panel": "drawer", "z_index": 2147483000},
"quota": {"per_minute": 600, "per_day": 200000},
"events": "full",
"consent": {"wait_for": "tcf"},
"version": "2026-10-10T22:00:00Z"
}
An events call under full tracking. Under counts the same call carries only t, image_key and item_id, with no session and no page URL.
POST /v1/events
{
"key": "pk_live_example_8f3a",
"session": "s_4e1c2a",
"page_url": "https://www.example-publisher.com/lifestyle/style/2026/10/09/fall-tailoring/",
"events": [
{"t": "affordance_shown", "image_key": "img_7c1e9a", "ts": 1760133600123},
{"t": "activated", "image_key": "img_7c1e9a", "ts": 1760133602410},
{"t": "marker_click", "image_key": "img_7c1e9a", "item_id": "it_5f2a", "ts": 1760133603902},
{"t": "outbound_click", "image_key": "img_7c1e9a", "item_id": "it_5f2a", "ts": 1760133607550}
]
}
Where the per-page request policy, batching, caching and quotas sit. The policy is not settled, so every knob is a config value with a default, enforced in one place:
| Knob | Default | Enforced in | Effect |
|---|---|---|---|
trigger | viewport | Loader | page sends all candidates at load; manual sends on hover |
batch_window_ms, max_batch | 150 ms, 10 | Loader | Coalesces neighbouring images into one request |
prefetch_viewports | 1 | Loader | How far ahead of the reader to ask |
max_images_per_page | 40 | Loader and proxy | Hard cap per page view |
ads.exclude | Default ad selectors plus the publisher's own | Loader and worker | Ads are never candidates, as images or video |
path_allowlist, path_denylist, page meta opt-out | empty, empty, none | Loader and worker | Section scoping and single-page exclusions for sensitive content |
| Detect cache TTL | 7 days | Proxy cache, CDN for GET /v1/image | Per image key plus perceptual-hash index |
| Identify cache TTL | 24 h | Proxy | Prices and availability change |
Negative cache for no_items | 24 h | Proxy | Non-fashion photos are not re-sent |
quota.per_minute, quota.per_day | 600, 200,000 | Proxy (token bucket and counter) | 429 with Retry-After; loader goes quiet for the window |
events | full recommended; counts available | Loader and proxy | Full adds session, page, image and item to events and to outbound links |
consent.wait_for | none | Loader | Under full, send nothing until the publisher's consent signal fires |
enabled | true | Loader | Kill switch, effective within 60 s |
| consent.honor_gpc | true | Loader and proxy | A reader's Global Privacy Control signal drops that session to counts; events carry the reason |
Where integration friction sits.
script-src means the tag must carry the page nonce (a tag manager does this) and the loader's dynamic import() relies on strict-dynamic. Send Silvr your current policy before the first test.__tcfapi, the publisher's own event, or none in counts mode). Counts mode sets no cookie, sends no identifier and needs no gating. The affiliate redirect sets its own anonymous cookie on Silvr's redirect domain at click time, for 90 days, as it already does for the app. A reader whose browser sends Global Privacy Control is treated as counts for that session whatever the tracking level, and the events record that this is why.panel option (drawer, popover, sheet) and the z-index are for.Synchronization and latency challenges.
| Challenge | Where it shows | Handling |
|---|---|---|
src swaps while a request is in flight (slideshow) | Loader | Each request is tagged with element and URL; stale results are dropped; AbortController cancels the fetch |
| Layout changes after results arrive | Loader | Marker positions are computed from the live rect at draw time, never stored |
| Scroll jank | Loader | Passive listeners, requestAnimationFrame, transforms only, no layout reads inside the scroll handler |
| Carousel mid-transition | Loader | Markers hidden during the transform transition; redrawn on transitionend or after 600 ms |
| Placeholder versus real image | Loader | Candidates must have naturalWidth above the threshold and a network URL; re-evaluated on load |
| Video time versus the frame that was sent | Loader | The frame carries its time; markers are tied to it and hidden on seek or play |
| Cache stampede on a new article | Proxy | Single-flight lock per image key in the cache; followers get pending or wait |
| Vision service slow or down | Proxy and loader | Background completion with one retry, pending with retry_after_ms; after repeated failures a circuit breaker hides affordances for 60 s |
| Several proxy workers | Proxy | All shared state in the shared cache; only an LRU of hot results in process |
| Config change | Loader | 60 s cache on config; no deploy on either side |
A publisher engineer can complete the install in under an hour, and nothing on this list needs a code release if a tag manager is in use.
Before install, send Silvr:
www.example-publisher.com and a staging host. The key is bound to these.SilvrFetcher/1.0.Install:
<script async src="https://cdn.silvrshop.com/v1/loader.js" data-silvr-key="pk_live_<publisher>_<id>"></script>https://api.silvrshop.com to script-src and connect-src, and https://cdn.silvrshop.com to img-src.data-silvr="off" on any image that must never be shoppable.<meta name="silvr" content="off"> in the head of any article that must never be shoppable, or send Silvr the URL for your deny list.crossorigin="anonymous" to <video> elements if pause-to-shop should work in Safari.What the script does and does not do, for your review:
document.write, eval or synchronous requests.Your controls, all without a deploy: a kill switch effective within a minute; the section allow-list and the page deny list; the per-image and per-page opt-outs; the tracking level; quotas per minute and per day; the z-index and panel style; the affordance label.
Optional accelerators, each one line of work on your side: a server-to-server POST /v1/preprocess from your CMS on publish, so results are warm before the first reader; read access to your Arc XP Content API for a Silvr service account, so warming needs no page rendering; the crossorigin attribute on video elements.
Each row is a case the overlay meets on the surfaces in scope, what happens, and whether it is included today.
| Surface or case | Behaviour | Availability |
|---|---|---|
| Image with shortest rendered side under 200 px, or aspect ratio beyond 3:1 | Ignored | Included |
| Blurred low-quality placeholder before the real image | Wait for load with a real naturalWidth, then evaluate | Included |
src changes after detection (a slideshow reusing one <img>) | New key; old markers dropped; in-flight request cancelled | Included |
| Same photo at several resizer widths across readers | One cache entry (normalized key plus perceptual hash) | Included |
<picture> switches to a different crop at a breakpoint | Re-detect on the new currentSrc; cache keyed with the aspect bucket | Included |
object-fit: cover hides part of the image | Mapping accounts for the drawn region; markers outside it are hidden | Included |
| Carousel clone slides (first and last duplicated) | Deduped by image key; no extra recognition calls | Included |
| Slide mid-transition | Markers hidden; redrawn on transitionend or after 600 ms | Included |
| Gallery thumbnail grid with a lightbox | Thumbnails ignored; the full image is handled when inserted | Included |
| Lightbox or sticky header with a very high z-index | Overlay z-index is configurable and defaults just below the maximum | Included |
| Image inside an ad iframe | Invisible to the loader; untouched | Included |
| Image matching an exclude selector (header, nav, footer, ads) | Skipped | Included |
| Photo with no fashion items | no_items, negative-cached 24 h, no affordance | Included |
| Two items overlapping (a jacket over a shirt) | Second marker shifts to the top edge of its box; 32 px minimum spacing | Included |
| More than 40 candidates on one page | First 40 in viewport order; the rest skipped | Included |
| Quota exhausted | 429 with Retry-After; loader goes quiet; no reader-visible error | Included |
| Recognition service timeout or error | pending with one retry, then silent for that image; circuit breaker after repeated failures | Included |
| Page not in the publisher's path allow-list | Loader exits after reading config | Included |
Per-image opt-out data-silvr="off" | Skipped | Included |
| Client-side navigation or back/forward cache restore | MutationObserver picks up new content; page_url read from location; re-attach on pageshow | Included |
| Touch device | Tap replaces hover; panel opens as a bottom sheet | Included |
| Reader prefers reduced motion | No dim or slide animation | Included |
| Video poster before play | Treated as an image with a Shop This Video label | Planned |
| In-page video paused by the reader or by activation | Current frame captured at 1280 px and detected; markers tied to that time | Planned |
| Video playing | No markers while playing | Planned |
| Safari with native HLS (tainted canvas) | Needs crossorigin="anonymous" on the element; otherwise poster only | Planned |
| Server-side keyframes with a timeline strip and item count | Worker extracts and detects keyframes for publisher-hosted files | Later |
| Third-party iframe player (YouTube, Vimeo) | Thumbnail at most; frames cannot be read | Not supported |
CSS background-image photos | Not detected | Later |
| Consent platform classifies Silvr as a vendor | Loader waits for the consent signal before sending anything | Planned |
| Wire-service photo licensed for editorial use only | Publisher excludes by section or per image; policy is the publisher's | Included |
| Publisher CMS pre-render (Arc XP block storing boxes at publish time) | Zero recognition calls at read time | Later |
| AMP pages, native apps, email, reader mode | Nothing injected | Not supported |
| Display ad as image or video (ad iframe, native ad unit, house promotion, in-player ad break) | Never a candidate: iframe isolation, ad selectors, and the player's ad-start and ad-end events | Included |
| Page excluded by the publisher's deny list or a page-level meta tag | Loader exits after reading config and the page head | Included |
| Publisher chooses full session tracking | Nothing sent until the consent signal fires; outbound links carry page, image, item and session into the affiliate redirect | Included |
| Image not yet in the cache (cold) | No affordance; the proxy answers pending, runs detection and recognition in the background (typically within a minute), and caches the result for the next reader | Included |
| Reader's browser sends Global Privacy Control | Treated as counts for that session even under full tracking; the events record the reason | Included |
| Publisher's own commerce modules (roundups with their own affiliate links) | Shoppable by default; excluded only when the publisher names the module selectors, like the page and image opt-outs | Included |
| Photo replaced at the same URL | Cached result re-checked once a day with a lightweight request; analysed again only if the file changed | Included |
| Keyboard-only reader or screen reader | Pill, markers and drawer are labelled buttons: Tab, Enter, the arrow keys, Escape | Included |
| Article behind a hard paywall (warming) | Worker uses the publisher's crawler access or content API when granted; otherwise the page's structured data, which covers hero photos but not galleries | Planned |
| Two boxes on the same item resolving to the same product | Merged into one pin; two people wearing the same coat stay two pins | Included |
| Pinch-zoom on a phone | Markers follow the zoomed view | Included |
| Printing the page | Overlay hidden | Included |
| Animated image (GIF, animated WebP) | Follows the video model: frames extracted server-side, a frame picker over a still | Planned |
| Image wrapped in a link | The pill and markers sit in Silvr's own layer, so a click on them never follows the publisher's link; a click on the photo itself still does | Included |
| Browser asks for reduced data | Same one-screen prefetch of pins; drawer thumbnails load only on activation | Included |
| Affiliate disclosure in the drawer | Publisher's own wording when provided, Silvr's default line otherwise | Included |