Redirects & compatibility
Some video URLs don’t map cleanly to a single thumbnail path. Rather than break
them, Vumbnail canonicalizes them with HTTP redirects so the eventual cached URL
is consistent. The repo keeps a single source-of-truth Redirect Inventory in
the root README.md; this page is its product-level description.
Behavior
Section titled “Behavior”YouTube playlist compatibility (Worker)
Section titled “YouTube playlist compatibility (Worker)”A request to /playlist or /playlist/ carrying a known list= value is
redirected to the mapped video’s thumbnail.
GET /playlist?list=PLzc4VjwEUvFg928S7nLEABR9qzd-tdsSK.jpg→307to/<mappedVideoId>.jpg, cachedpublic, max-age=3600.- Unknown playlists are instead resolved (not redirected) to their first video via YouTube oEmbed, falling back to an external resolver service.
Vimeo legacy password canonicalization (Worker)
Section titled “Vimeo legacy password canonicalization (Worker)”Older embeds pass the Vimeo password as a ?h= query parameter. Vumbnail moves
it into the canonical :password path form.
GET /637615910?h=27057b8971.jpg&foo=bar→302to/637615910:27057b8971.jpg?foo=bar.- Unrelated query parameters are preserved; only
his removed. Cachedpublic, max-age=3600.
Malformed path fallback (edge Snippet)
Section titled “Malformed path fallback (edge Snippet)”The legacy artifact path /.jpg (a full URL accidentally pasted into the path)
is caught at the edge.
GET /.jpg→308tohttps://cache.vumbnail.com/thumbnails/_shared/error.jpg.
Borås WordPress loop escape (edge Snippet)
Section titled “Borås WordPress loop escape (edge Snippet)”A specific WordPress site re-requests its own thumbnails in a self-referential
loop. A tightly-scoped rule breaks the loop by redirecting those exact requests
to the watermarked image. All conditions must match: video ID in the Borås set,
path /<id>.jpg, the exact WordPress user-agent, a self-referrer, and origin
ASN 14061.
- Matching request →
307tohttps://cache.vumbnail.com/thumbnails/<id>/wm.jpg.
This case is deliberately narrow: a path-only rule would match ~267k requests where the scoped predicate matches ~134k, so the scope is the spec.
Inputs & outputs
Section titled “Inputs & outputs”Inputs: request path, query string, and (for the Snippet rules) User-Agent,
Referer, and Cloudflare’s ASN signal.
Outputs: a 302, 307, or 308 Location redirect with a Cache-Control
header, or pass-through to normal resolution when no rule matches.
States & edge cases
Section titled “States & edge cases”- Unknown playlist — no hardcoded mapping, so the Worker resolves the first
video live; if resolution fails, it negative-caches a
playlist-resolve-failedreason and serves the error fallback. ?h=with extra query params — preserved across the redirect excepth.- Near-miss on the Borås rule (wrong UA, missing referrer, different ASN) — intentionally not redirected; falls through to normal handling to avoid over-redirecting unrelated traffic.
- Redirect consolidation — these rules live in Worker code and the edge Snippet; the standalone Cloudflare dynamic-redirect deployer has been retired in favor of consolidating in the Snippet (decision recorded 2026-05-26).
Data shape
Section titled “Data shape”// Hardcoded YouTube playlist → first-video mapping (Worker)type PlaylistRedirect = { listId: string; videoId: string }
// Borås loop-escape predicate (edge Snippet) — every field must matchtype BorasEscapeRule = { videoIds: string[] // e.g. ["1173997793", "1173997754"] userAgent: string // exact: "WordPress/6.1.1; https://www.borasdjurpark.se" referrerHostMatches: "self" asn: 14061 redirectTo: "https://cache.vumbnail.com/thumbnails/{videoId}/wm.jpg" status: 307}Open questions
Section titled “Open questions”- Where new redirects live. The current decision is to consolidate edge redirects in the Snippet rather than Cloudflare Single Redirects / dynamic rules. Whether any future case justifies reintroducing a separate redirect surface is open.
- Playlist mapping growth. The hardcoded playlist map is maintained by hand; whether to back it with a resolver cache instead is undecided. See the global Open questions.